• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Voral / vs-vue3-select / #3

28 Dec 2023 06:01AM UTC coverage: 95.829% (+0.4%) from 95.402%
#3

push

Alexander Vorobyev
chore(release): 1.3.0

262 of 285 branches covered (0.0%)

Branch coverage included in aggregate %.

1783 of 1849 relevant lines covered (96.43%)

39.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.89
/src/components/Select.vue
1
<style>
1✔
2
@import '../css/vue-select.css';
1✔
3
</style>
1✔
4

1✔
5
<template>
1✔
6
  <div :dir="dir" class="v-select" :class="stateClasses">
1✔
7
    <slot name="header" v-bind="scope.header"/>
1✔
8
    <div
1✔
9
        :id="`vs${uid}__combobox`"
1✔
10
        ref="toggle"
1✔
11
        class="vs__dropdown-toggle"
1✔
12
        role="combobox"
1✔
13
        :aria-expanded="dropdownOpen.toString()"
1✔
14
        :aria-owns="`vs${uid}__listbox`"
1✔
15
        :aria-label="ariaLabel"
1✔
16
        v-click-outside="clickOutside"
1✔
17
        @mousedown="toggleDropdown($event)"
1✔
18
    >
1✔
19
      <div ref="selectedOptions" class="vs__selected-options">
1✔
20
        <slot
1✔
21
            v-for="(option, i) in selectedValue"
1✔
22
            name="selected-option-container"
1✔
23
            :option="normalizeOptionForSlot(option)"
1✔
24
            :deselect="deselect"
1✔
25
            :multiple="multiple"
1✔
26
            :disabled="disabled"
1✔
27
        >
1✔
28
          <span :key="getOptionKey(option)" class="vs__selected">
1✔
29
            <slot
1✔
30
                name="selected-option"
1✔
31
                v-bind="normalizeOptionForSlot(option)"
1✔
32
            >
1✔
33
              {{ getOptionLabel(option) }}
1✔
34
            </slot>
1✔
35
            <button
1✔
36
                v-if="multiple"
1✔
37
                :ref="(el) => (deselectButtons[i] = el)"
1✔
38
                :disabled="disabled"
1✔
39
                type="button"
1✔
40
                class="vs__deselect"
1✔
41
                :title="`Deselect ${getOptionLabel(option)}`"
1✔
42
                :aria-label="`Deselect ${getOptionLabel(option)}`"
1✔
43
                @click="deselect(option)"
1✔
44
            >
1✔
45
              <component :is="childComponents.Deselect"/>
1✔
46
            </button>
1✔
47
          </span>
1✔
48
        </slot>
1✔
49
        <div class="vs__input-box">
1✔
50
          <slot name="typeahead" v-bind="scope.typeahead">
1✔
51
            <div class="vs__search_position vs__search_complete"
1✔
52
                 v-if="scope.typeahead.canCompleteSearch && scope.typeahead.completedText!==''">
3✔
53
              {{ scope.typeahead.completedText }}
3✔
54
            </div>
1✔
55
          </slot>
1✔
56
          <slot name="search" v-bind="scope.search">
1✔
57
            <input
1✔
58
                class="vs__search vs__search_position"
1✔
59
                v-bind="scope.search.attributes"
1✔
60
                v-on="scope.search.events"
1✔
61
            />
1✔
62
          </slot>
1✔
63
        </div>
1✔
64
      </div>
1✔
65

1✔
66
      <div ref="actions" class="vs__actions">
1✔
67
        <button
1✔
68
            v-show="showClearButton"
1✔
69
            ref="clearButton"
1✔
70
            :disabled="disabled"
1✔
71
            type="button"
1✔
72
            class="vs__clear"
1✔
73
            title="Clear Selected"
1✔
74
            aria-label="Clear Selected"
1✔
75
            @click="clearSelection"
1✔
76
        >
1✔
77
          <component :is="childComponents.Deselect"/>
1✔
78
        </button>
1✔
79

1✔
80
        <slot name="open-indicator" v-bind="scope.openIndicator">
1✔
81
          <component
1✔
82
              :is="childComponents.OpenIndicator"
339✔
83
              v-if="!noDrop"
339✔
84
              v-bind="scope.openIndicator.attributes"
339✔
85
          />
1✔
86
        </slot>
1✔
87

1✔
88
        <slot name="spinner" v-bind="scope.spinner">
1✔
89
          <div v-show="mutableLoading" class="vs__spinner">Loading...</div>
1✔
90
        </slot>
1✔
91
      </div>
1✔
92
    </div>
1✔
93
    <transition :name="transition">
1✔
94
      <ul
1✔
95
          v-if="dropdownOpen"
1✔
96
          :id="`vs${uid}__listbox`"
1✔
97
          ref="dropdownMenu"
1✔
98
          :key="`vs${uid}__listbox`"
1✔
99
          v-append-to-body
1✔
100
          class="vs__dropdown-menu"
1✔
101
          role="listbox"
1✔
102
          tabindex="-1"
1✔
103
          @mousedown.prevent="onMousedown"
1✔
104
          @mouseup="onMouseUp"
1✔
105
      >
1✔
106
        <slot name="list-header" v-bind="scope.listHeader"/>
1✔
107
        <li
1✔
108
            v-for="(option, index) in filteredOptions"
1✔
109
            :id="`vs${uid}__option-${index}`"
1✔
110
            :key="getOptionKey(option)"
1✔
111
            role="option"
1✔
112
            :class="{
1✔
113
            'vs__dropdown-option': !isOptGroupOption(option),
1✔
114
            'vs__dropdown-optgroup-option': isOptGroupOption(option),
1✔
115
            'vs__dropdown-option--deselect':
1✔
116
              isOptionDeselectable(option) && index === typeAheadPointer,
1✔
117
            'vs__dropdown-option--selected': isOptionSelected(option),
1✔
118
            'vs__dropdown-option--highlight':
1✔
119
              (!isOptGroupOption(option) && index === typeAheadPointer) ||
1✔
120
              isOptionSelected(option),
1✔
121
            'vs__dropdown-option--disabled': !selectable(option),
1✔
122
          }"
1✔
123
            :aria-selected="index === typeAheadPointer ? true : null"
1✔
124
            @mouseover="selectable(option) ? updateTypeAheadPointer(index) : null"
1✔
125
            @mouseout="updateTypeAheadPointer(-1)"
1✔
126
            @click.prevent.stop="selectable(option) ? select(option) : null"
1✔
127
            @touchstart="
1✔
128
            selectable(option) ? updateTypeAheadPointer(index) : null
1✔
129
          "
1✔
130
        >
1✔
131
          <div v-if="option.optgroup" class="">
1✔
132
            {{ option.optgroup }}
3✔
133
          </div>
1✔
134
          <slot v-else name="option" v-bind="normalizeOptionForSlot(option)">
1✔
135
            {{ getOptionLabel(option) }}
1✔
136
          </slot>
1✔
137
        </li>
1✔
138
        <li v-if="filteredOptions.length === 0" class="vs__no-options">
1✔
139
          <slot name="no-options" v-bind="scope.noOptions">
1✔
140
            Sorry, no matching options.
1✔
141
          </slot>
1✔
142
        </li>
1✔
143
        <slot name="list-footer" v-bind="scope.listFooter"/>
1✔
144
      </ul>
1✔
145
      <ul
1✔
146
          v-else
1✔
147
          :id="`vs${uid}__listbox`"
1✔
148
          role="listbox"
1✔
149
          style="display: none; visibility: hidden"
1✔
150
      ></ul>
1✔
151
    </transition>
1✔
152
    <slot name="footer" v-bind="scope.footer"/>
1✔
153
  </div>
1✔
154
</template>
1✔
155

1✔
156
<script>
1✔
157
import pointerScroll from '@/mixins/pointerScroll.js'
1✔
158
import typeAheadPointer from '@/mixins/typeAheadPointer.js'
1✔
159
import ajax from '@/mixins/ajax.js'
1✔
160
import childComponents from '@/components/childComponents.js'
1✔
161
import appendToBody from '@/directives/appendToBody.js'
1✔
162
import clickOutside from '@/directives/clickOutside.js'
1✔
163
import sortAndStringify from '@/utility/sortAndStringify.js'
1✔
164
import uniqueId from '@/utility/uniqueId.js'
1✔
165

1✔
166
/**
1✔
167
 * @name VueSelect
1✔
168
 */
1✔
169
export default {
1✔
170
  components: {...childComponents},
1✔
171

1✔
172
  directives: {appendToBody, clickOutside},
1✔
173

1✔
174
  mixins: [pointerScroll, typeAheadPointer, ajax],
1✔
175

1✔
176
  compatConfig: {
1✔
177
    MODE: 3,
1✔
178
  },
1✔
179

1✔
180
  emits: [
1✔
181
    'open',
1✔
182
    'close',
1✔
183
    'update:modelValue',
1✔
184
    'search',
1✔
185
    'search:compositionstart',
1✔
186
    'search:compositionend',
1✔
187
    'search:keydown',
1✔
188
    'search:blur',
1✔
189
    'search:focus',
1✔
190
    'search:input',
1✔
191
    'option:created',
1✔
192
    'option:selecting',
1✔
193
    'option:selected',
1✔
194
    'option:deselecting',
1✔
195
    'option:deselected',
1✔
196
  ],
1✔
197

1✔
198
  props: {
1✔
199
    /**
1✔
200
     * Contains the currently selected value. Very similar to a
1✔
201
     * `value` attribute on an <input>. You can listen for changes
1✔
202
     * with the 'input' event.
1✔
203
     * @type {Object|String|Array|null}
1✔
204
     */
1✔
205
    // eslint-disable-next-line vue/require-default-prop,vue/require-prop-types
1✔
206
    modelValue: {},
1✔
207

1✔
208
    /**
1✔
209
     * Trim options on paste from clipboard in multiple mode
1✔
210
     * @type  Boolean
1✔
211
     * @since v1.3
1✔
212
     */
1✔
213
    pasteTrim: {
1✔
214
      type: Boolean,
1✔
215
      default: true
1✔
216
    },
1✔
217

1✔
218
    /**
1✔
219
     * String separator for paste from clipboard in multiple mode
1✔
220
     * @type  String
1✔
221
     * @since v1.3
1✔
222
     */
1✔
223
    pasteSeparator: {
1✔
224
      type: String,
1✔
225
      default: ''
1✔
226
    },
1✔
227
    /**
1✔
228
     * An object with any custom components that you'd like to overwrite
1✔
229
     * the default implementation of in your app. The keys in this object
1✔
230
     * will be merged with the defaults.
1✔
231
     * @see https://vue3-select.va-soft.ru/styling/components
1✔
232
     * @type {Function}
1✔
233
     */
1✔
234
    components: {
1✔
235
      type: Object,
1✔
236
      default: () => ({}),
1✔
237
    },
1✔
238

1✔
239
    /**
1✔
240
     * An array of strings or objects to be used as dropdown choices.
1✔
241
     * If you are using an array of objects, vue-select will look for
1✔
242
     * a `label` key (ex. [{label: 'This is Foo', value: 'foo'}]). A
1✔
243
     * custom label key can be set with the `label` prop.
1✔
244
     * @type {Array}
1✔
245
     */
1✔
246
    options: {
1✔
247
      type: Array,
1✔
248
      default() {
1✔
249
        return []
26✔
250
      },
1✔
251
    },
1✔
252

1✔
253
    /**
1✔
254
     * Disable the entire component.
1✔
255
     * @type {Boolean}
1✔
256
     */
1✔
257
    disabled: {
1✔
258
      type: Boolean,
1✔
259
      default: false,
1✔
260
    },
1✔
261

1✔
262
    /**
1✔
263
     * Can automatically select the highlighted option
1✔
264
     * @type {Boolean}
1✔
265
     * @since v1.2.0
1✔
266
     */
1✔
267
    autoSelect: {
1✔
268
      type: Boolean,
1✔
269
      default: false,
1✔
270
    },
1✔
271

1✔
272
    /**
1✔
273
     * Can automatically complete search text
1✔
274
     * @since v1.2.0
1✔
275
     * @type {Boolean}
1✔
276
     */
1✔
277
    completeSearch: {
1✔
278
      type: Boolean,
1✔
279
      default: false,
1✔
280
    },
1✔
281

1✔
282
    /**
1✔
283
     * Can the user clear the selected property.
1✔
284
     * @type {Boolean}
1✔
285
     */
1✔
286
    clearable: {
1✔
287
      type: Boolean,
1✔
288
      default: true,
1✔
289
    },
1✔
290

1✔
291
    /**
1✔
292
     * Can the user deselect an option by clicking it from
1✔
293
     * within the dropdown.
1✔
294
     * @type {Boolean}
1✔
295
     */
1✔
296
    deselectFromDropdown: {
1✔
297
      type: Boolean,
1✔
298
      default: false,
1✔
299
    },
1✔
300

1✔
301
    /**
1✔
302
     * Sets the value of the 'aria-label' for the search `<input>`.
1✔
303
     * @type {String}
1✔
304
     */
1✔
305
    ariaLabel: {
1✔
306
      type: String,
1✔
307
      default: 'Search for option',
1✔
308
    },
1✔
309

1✔
310
    /**
1✔
311
     * Enable/disable filtering the options.
1✔
312
     * @type {Boolean}
1✔
313
     */
1✔
314
    searchable: {
1✔
315
      type: Boolean,
1✔
316
      default: true,
1✔
317
    },
1✔
318

1✔
319
    /**
1✔
320
     * Equivalent to the `multiple` attribute on a `<select>` input.
1✔
321
     * @type {Boolean}
1✔
322
     */
1✔
323
    multiple: {
1✔
324
      type: Boolean,
1✔
325
      default: false,
1✔
326
    },
1✔
327

1✔
328
    /**
1✔
329
     * Equivalent to the `placeholder` attribute on an `<input>`.
1✔
330
     * @type {String}
1✔
331
     */
1✔
332
    placeholder: {
1✔
333
      type: String,
1✔
334
      default: '',
1✔
335
    },
1✔
336

1✔
337
    /**
1✔
338
     * Sets a Vue transition property on the `.vs__dropdown-menu`.
1✔
339
     * @type {String}
1✔
340
     */
1✔
341
    transition: {
1✔
342
      type: String,
1✔
343
      default: 'vs__fade',
1✔
344
    },
1✔
345

1✔
346
    /**
1✔
347
     * Enables/disables clearing the search text when an option is selected.
1✔
348
     * @type {Boolean}
1✔
349
     */
1✔
350
    clearSearchOnSelect: {
1✔
351
      type: Boolean,
1✔
352
      default: true,
1✔
353
    },
1✔
354

1✔
355
    /**
1✔
356
     * Close a dropdown when an option is chosen. Set to false to keep the dropdown
1✔
357
     * open (useful when combined with multi-select, for example)
1✔
358
     * @type {Boolean}
1✔
359
     */
1✔
360
    closeOnSelect: {
1✔
361
      type: Boolean,
1✔
362
      default: true,
1✔
363
    },
1✔
364

1✔
365
    /**
1✔
366
     * Tells vue-select what key to use when generating option
1✔
367
     * labels when each `option` is an object.
1✔
368
     * @type {String}
1✔
369
     */
1✔
370
    label: {
1✔
371
      type: String,
1✔
372
      default: 'label',
1✔
373
    },
1✔
374

1✔
375
    /**
1✔
376
     * Value of the 'autocomplete' field of the input
1✔
377
     * element.
1✔
378
     * @type {String}
1✔
379
     */
1✔
380
    autocomplete: {
1✔
381
      type: String,
1✔
382
      default: 'off',
1✔
383
    },
1✔
384

1✔
385
    /**
1✔
386
     * When working with objects, the reduce
1✔
387
     * prop allows you to transform a given
1✔
388
     * object to only the information you
1✔
389
     * want passed to a v-model binding
1✔
390
     * or @input event.
1✔
391
     */
1✔
392
    reduce: {
1✔
393
      type: Function,
1✔
394
      default: (option) => option,
1✔
395
    },
1✔
396

1✔
397
    /**
1✔
398
     * Decides whether an option is selectable or not. Not selectable options
1✔
399
     * are displayed but disabled and cannot be selected.
1✔
400
     *
1✔
401
     * @type {Function}
1✔
402
     * @param {Object|String} option
1✔
403
     * @return {Boolean}
1✔
404
     */
1✔
405
    selectable: {
1✔
406
      type: Function,
1✔
407
      default: (option) => true,
1✔
408
    },
1✔
409

1✔
410
    /**
1✔
411
     * Callback to generate the label text. If {option}
1✔
412
     * is an object, returns option[this.label] by default.
1✔
413
     *
1✔
414
     * Label text is used for filtering comparison and
1✔
415
     * displaying. If you only need to adjust the
1✔
416
     * display, you should use the `option` and
1✔
417
     * `selected-option` slots.
1✔
418
     *
1✔
419
     * @type {Function}
1✔
420
     * @param  {Object || String} option
1✔
421
     * @return {String}
1✔
422
     */
1✔
423
    getOptionLabel: {
1✔
424
      type: Function,
1✔
425
      default(option) {
1✔
426
        if (typeof option === 'object') {
495✔
427
          if (!option.hasOwnProperty(this.label)) {
112✔
428
            return console.warn(
2✔
429
                `[vs-vue3-select warn]: Label key "option.${this.label}" does not` +
2✔
430
                ` exist in options object ${JSON.stringify(option)}.\n` +
2✔
431
                'https://vue3-select.va-soft.ru/api/props/#getoptionlabel'
2✔
432
            )
2✔
433
          }
2✔
434
          return option[this.label]
110✔
435
        }
110✔
436
        return option
383✔
437
      },
1✔
438
    },
1✔
439

1✔
440
    /**
1✔
441
     * Generate a unique identifier for each option. If `option`
1✔
442
     * is an object and `option.hasOwnProperty('id')` exists,
1✔
443
     * `option.id` is used by default, otherwise the option
1✔
444
     * will be serialized to JSON.
1✔
445
     *
1✔
446
     * If you are supplying a lot of options, you should
1✔
447
     * provide your own keys, as JSON.stringify can be
1✔
448
     * slow with lots of objects.
1✔
449
     *
1✔
450
     * The result of this function *must* be unique.
1✔
451
     *
1✔
452
     * @type {Function}
1✔
453
     * @param  {Object || String} option
1✔
454
     * @return {String}
1✔
455
     */
1✔
456
    getOptionKey: {
1✔
457
      type: Function,
1✔
458
      default(option) {
1✔
459
        if (option === null || typeof option !== 'object') {
998✔
460
          return option
794✔
461
        }
794✔
462
        return option.hasOwnProperty('id') ? option.id : sortAndStringify(option)
998✔
463
      },
1✔
464
    },
1✔
465

1✔
466
    /**
1✔
467
     * Enable/disable creating options from searchEl.
1✔
468
     * @type {Boolean}
1✔
469
     */
1✔
470
    taggable: {
1✔
471
      type: Boolean,
1✔
472
      default: false,
1✔
473
    },
1✔
474

1✔
475
    /**
1✔
476
     * Set the tabindex for the input field.
1✔
477
     * @type {Number}
1✔
478
     */
1✔
479
    tabindex: {
1✔
480
      type: Number,
1✔
481
      default: null,
1✔
482
    },
1✔
483

1✔
484
    /**
1✔
485
     * When true, newly created tags will be added to
1✔
486
     * the options list.
1✔
487
     * @type {Boolean}
1✔
488
     */
1✔
489
    pushTags: {
1✔
490
      type: Boolean,
1✔
491
      default: false,
1✔
492
    },
1✔
493

1✔
494
    /**
1✔
495
     * When true, existing options will be filtered
1✔
496
     * by the search text. Should not be used in conjunction
1✔
497
     * with taggable.
1✔
498
     * @type {Boolean}
1✔
499
     */
1✔
500
    filterable: {
1✔
501
      type: Boolean,
1✔
502
      default: true,
1✔
503
    },
1✔
504

1✔
505
    /**
1✔
506
     * Callback to determine if the provided option should
1✔
507
     * match the current search text. Used to determine
1✔
508
     * if the option should be displayed.
1✔
509
     * @type   {Function}
1✔
510
     * @param  {Object || String} option
1✔
511
     * @param  {String} label
1✔
512
     * @param  {String} search
1✔
513
     * @return {Boolean}
1✔
514
     */
1✔
515
    filterBy: {
1✔
516
      type: Function,
1✔
517
      default(option, label, search) {
1✔
518
        return (
87✔
519
            (label || '')
87!
520
                .toLocaleLowerCase()
87✔
521
                .indexOf(search.toLocaleLowerCase()) > -1
87✔
522
        )
87✔
523
      },
1✔
524
    },
1✔
525

1✔
526
    /**
1✔
527
     * Callback to filter results when search text
1✔
528
     * is provided. Default implementation loops
1✔
529
     * each option, and returns the result of
1✔
530
     * this.filterBy.
1✔
531
     * @type   {Function}
1✔
532
     * @param  {Array} list of options
1✔
533
     * @param  {String} search text
1✔
534
     * @param  {Object} vSelect instance
1✔
535
     * @return {Boolean}
1✔
536
     */
1✔
537
    filter: {
1✔
538
      type: Function,
1✔
539
      default(options, search) {
1✔
540
        return options.filter((option) => {
46✔
541
          let label = this.getOptionLabel(option)
93✔
542
          if (typeof label === 'number') {
93✔
543
            label = label.toString()
3✔
544
          }
3✔
545
          return this.filterBy(option, label, search)
93✔
546
        })
46✔
547
      },
1✔
548
    },
1✔
549

1✔
550
    /**
1✔
551
     * User defined function for adding Options
1✔
552
     * @type {Function}
1✔
553
     */
1✔
554
    createOption: {
1✔
555
      type: Function,
1✔
556
      default(option) {
1✔
557
        return typeof this.optionList[0] === 'object'
34✔
558
            ? {[this.label]: option}
34✔
559
            : option
34✔
560
      },
1✔
561
    },
1✔
562

1✔
563
    /**
1✔
564
     * When false, updating the options will not reset the selected value. Accepts
1✔
565
     * a `boolean` or `function` that returns a `boolean`. If defined as a function,
1✔
566
     * it will receive the params listed below.
1✔
567
     *
1✔
568
     * @type {Boolean|Function}
1✔
569
     * @param {Array} newOptions
1✔
570
     * @param {Array} oldOptions
1✔
571
     * @param {Array} selectedValue
1✔
572
     */
1✔
573
    resetOnOptionsChange: {
1✔
574
      default: false,
1✔
575
      validator: (value) => ['function', 'boolean'].includes(typeof value),
1✔
576
    },
1✔
577

1✔
578
    /**
1✔
579
     * If search text should clear on blur
1✔
580
     * @return {Boolean} True when single and clearSearchOnSelect
1✔
581
     */
1✔
582
    clearSearchOnBlur: {
1✔
583
      type: Function,
1✔
584
      default: function ({clearSearchOnSelect, multiple}) {
1✔
585
        return clearSearchOnSelect;
8✔
586
      },
1✔
587
    },
1✔
588

1✔
589
    /**
1✔
590
     * Disable the dropdown entirely.
1✔
591
     * @type {Boolean}
1✔
592
     */
1✔
593
    noDrop: {
1✔
594
      type: Boolean,
1✔
595
      default: false,
1✔
596
    },
1✔
597

1✔
598
    /**
1✔
599
     * Sets the id of the input element.
1✔
600
     * @type {String}
1✔
601
     * @default {null}
1✔
602
     */
1✔
603
    // eslint-disable-next-line vue/require-default-prop
1✔
604
    inputId: {
1✔
605
      type: String,
1✔
606
    },
1✔
607

1✔
608
    /**
1✔
609
     * Sets RTL support. Accepts 'ltr', 'rtl', 'auto'.
1✔
610
     * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir
1✔
611
     * @type {String}
1✔
612
     * @default 'auto'
1✔
613
     */
1✔
614
    dir: {
1✔
615
      type: String,
1✔
616
      default: 'auto',
1✔
617
    },
1✔
618

1✔
619
    /**
1✔
620
     * Keycodes that will select the current option.
1✔
621
     * @type Array
1✔
622
     */
1✔
623
    selectOnKeyCodes: {
1✔
624
      type: Array,
1✔
625
      default: () => [13],
1✔
626
    },
1✔
627

1✔
628
    /**
1✔
629
     * Query Selector used to find the search input
1✔
630
     * when the 'search' scoped slot is used.
1✔
631
     *
1✔
632
     * Must be a valid CSS selector string.
1✔
633
     *
1✔
634
     * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector
1✔
635
     * @type {String}
1✔
636
     */
1✔
637
    searchInputQuerySelector: {
1✔
638
      type: String,
1✔
639
      default: '[type=search]',
1✔
640
    },
1✔
641

1✔
642
    /**
1✔
643
     * Used to modify the default keydown events map
1✔
644
     * for the search input. Can be used to implement
1✔
645
     * custom behaviour for key presses.
1✔
646
     */
1✔
647

1✔
648
    mapKeydown: {
1✔
649
      type: Function,
1✔
650
      /**
1✔
651
       * @param map {Object}
1✔
652
       * @param vm {VueSelect}
1✔
653
       * @return {Object}
1✔
654
       */
1✔
655
      default: (map, vm) => map,
1✔
656
    },
1✔
657

1✔
658
    /**
1✔
659
     * Append the dropdown element to the end of the body
1✔
660
     * and size/position it dynamically. Use it if you have
1✔
661
     * overflow or z-index issues.
1✔
662
     * @type {Boolean}
1✔
663
     */
1✔
664
    appendToBody: {
1✔
665
      type: Boolean,
1✔
666
      default: false,
1✔
667
    },
1✔
668

1✔
669
    /**
1✔
670
     * When `appendToBody` is true, this function is responsible for
1✔
671
     * positioning the dropdown list.
1✔
672
     *
1✔
673
     * If a function is returned from `calculatePosition`, it will
1✔
674
     * be called when the dropdown list is removed from the DOM.
1✔
675
     * This allows for any garbage collection you may need to do.
1✔
676
     *
1✔
677
     * @see http://vue3-select.va-soft.ru/guide/positioning.html
1✔
678
     */
1✔
679
    calculatePosition: {
1✔
680
      type: Function,
1✔
681
      /**
1✔
682
       * @param dropdownList {HTMLUListElement}
1✔
683
       * @param component {Vue} current instance of vue select
1✔
684
       * @param width {string} calculated width in pixels of the dropdown menu
1✔
685
       * @param top {string} absolute position top value in pixels relative to the document
1✔
686
       * @param left {string} absolute position left value in pixels relative to the document
1✔
687
       * @return {function|void}
1✔
688
       */
1✔
689
      default(dropdownList, component, {width, top, left}) {
1✔
690
        dropdownList.style.top = top
×
691
        dropdownList.style.left = left
×
692
        dropdownList.style.width = width
×
693
      },
1✔
694
    },
1✔
695

1✔
696
    /**
1✔
697
     * Determines whether the dropdown should be open.
1✔
698
     * Receives the component instance as the only argument.
1✔
699
     *
1✔
700
     * @return boolean
1✔
701
     */
1✔
702
    dropdownShouldOpen: {
1✔
703
      type: Function,
1✔
704
      default({noDrop, open, mutableLoading}) {
1✔
705
        return noDrop ? false : open && !mutableLoading
278✔
706
      },
1✔
707
    },
1✔
708

1✔
709
    /**
1✔
710
     * A unique identifier used to generate IDs in HTML.
1✔
711
     * Must be unique for every instance of the component.
1✔
712
     */
1✔
713
    uid: {
1✔
714
      type: [String, Number],
1✔
715
      default: () => uniqueId(),
1✔
716
    },
1✔
717
  },
1✔
718

1✔
719
  data() {
1✔
720
    return {
180✔
721
      search: '',
180✔
722
      open: false,
180✔
723
      isComposing: false,
180✔
724
      pushedTags: [],
180✔
725
      // eslint-disable-next-line vue/no-reserved-keys
180✔
726
      _value: [], // Internal value managed by Vue Select if no `value` prop is passed
180✔
727
      deselectButtons: [],
180✔
728
      pasteProcessed: false,
180✔
729
      pasteBuffer: []
180✔
730
    }
180✔
731
  },
1✔
732

1✔
733
  computed: {
1✔
734
    getFirstSelectable() {
1✔
735
      let i = 0, cnt = this.filteredOptions.length;
4✔
736
      for (; i < cnt; ++i) {
4✔
737
        if (this.selectable(this.filteredOptions[i])) {
6✔
738
          return this.filteredOptions[i];
3✔
739
        }
3✔
740
      }
6✔
741
      return null;
1✔
742
    },
1✔
743
    autocompleteText() {
1✔
744
      if (!this.open || this.search === '' || this.filteredOptions.length === 0 || !this.completeSearch) {
283✔
745
        return '';
279✔
746
      }
279✔
747
      const option = this.getFirstSelectable;
4✔
748
      if (option === null) {
18✔
749
        return '';
1✔
750
      }
1✔
751
      const label = this.getOptionLabel(option).replace(new RegExp(this.search, 'i'), this.search);
3✔
752
      return label.startsWith(this.search) ? label : '';
283!
753
    },
1✔
754
    isReducingValues() {
1✔
755
      return this.$props.reduce !== this.$options.props.reduce.default
54✔
756
    },
1✔
757

1✔
758
    /**
1✔
759
     * Determine if the component needs to
1✔
760
     * track the state of values internally.
1✔
761
     * @return {boolean}
1✔
762
     */
1✔
763
    isTrackingValues() {
1✔
764
      return typeof this.modelValue === 'undefined' || this.isReducingValues
184✔
765
    },
1✔
766

1✔
767
    /**
1✔
768
     * The options that are currently selected.
1✔
769
     * @return {Array}
1✔
770
     */
1✔
771
    selectedValue() {
1✔
772
      let value = this.modelValue
398✔
773
      if (this.isTrackingValues) {
398✔
774
        // Vue select has to manage value internally
359✔
775
        value = this.$data._value
359✔
776
      }
359✔
777

398✔
778
      if (value !== undefined && value !== null && value !== '') {
398✔
779
        return Array.prototype.concat(value)
260✔
780
      }
260✔
781

138✔
782
      return []
138✔
783
    },
1✔
784

1✔
785
    /**
1✔
786
     * The options available to be chosen
1✔
787
     * from the dropdown, including any
1✔
788
     * tags that have been pushed.
1✔
789
     *
1✔
790
     * @return {Array}
1✔
791
     */
1✔
792
    optionList() {
1✔
793
      return this.options.concat(this.pushTags ? this.pushedTags : [])
194✔
794
    },
1✔
795

1✔
796
    /**
1✔
797
     * Find the search input DOM element.
1✔
798
     * @returns {HTMLInputElement}
1✔
799
     */
1✔
800
    searchEl() {
1✔
801
      return this.$slots['search']
58✔
802
          ? this.$refs.selectedOptions.querySelector(
58!
803
              this.searchInputQuerySelector
×
804
          )
×
805
          : this.$refs.search
58✔
806
    },
1✔
807

1✔
808
    /**
1✔
809
     * The object to be bound to the $slots.search slot.
1✔
810
     * @returns {Object}
1✔
811
     */
1✔
812
    scope() {
1✔
813
      const listSlot = {
345✔
814
        search: this.search,
345✔
815
        loading: this.loading,
345✔
816
        searching: this.searching,
345✔
817
        filteredOptions: this.filteredOptions,
345✔
818
      }
345✔
819
      return {
345✔
820
        typeahead: {
345✔
821
          search: this.search,
345✔
822
          completedText: this.autocompleteText,
345✔
823
          searching: this.searching,
345✔
824
          canCompleteSearch: this.completeSearch
345✔
825
        },
345✔
826
        search: {
345✔
827
          attributes: {
345✔
828
            disabled: this.disabled,
345✔
829
            placeholder: this.searchPlaceholder,
345✔
830
            tabindex: this.tabindex,
345✔
831
            readonly: !this.searchable,
345✔
832
            id: this.inputId,
345✔
833
            'aria-autocomplete': 'list',
345✔
834
            'aria-labelledby': `vs${this.uid}__combobox`,
345✔
835
            'aria-controls': `vs${this.uid}__listbox`,
345✔
836
            ref: 'search',
345✔
837
            type: 'search',
345✔
838
            autocomplete: this.autocomplete,
345✔
839
            value: this.search,
345✔
840
            ...(this.dropdownOpen && this.filteredOptions[this.typeAheadPointer]
345✔
841
                ? {
345✔
842
                  'aria-activedescendant': `vs${this.uid}__option-${this.typeAheadPointer}`,
45✔
843
                }
45✔
844
                : {}),
345✔
845
          },
345✔
846
          events: {
345✔
847
            compositionstart: () => (this.isComposing = true),
345✔
848
            compositionend: () => (this.isComposing = false),
345✔
849
            keydown: this.onSearchKeyDown,
345✔
850
            blur: this.onSearchBlur,
345✔
851
            focus: this.onSearchFocus,
345✔
852
            input: event => this.search = event.target.value,
345✔
853
            paste: this.onPaste
345✔
854
          },
345✔
855
        },
345✔
856
        spinner: {
345✔
857
          loading: this.mutableLoading,
345✔
858
        },
345✔
859
        noOptions: {
345✔
860
          search: this.search,
345✔
861
          loading: this.mutableLoading,
345✔
862
          searching: this.searching,
345✔
863
        },
345✔
864
        openIndicator: {
345✔
865
          attributes: {
345✔
866
            ref: 'openIndicator',
345✔
867
            role: 'presentation',
345✔
868
            class: 'vs__open-indicator',
345✔
869
          },
345✔
870
        },
345✔
871
        listHeader: listSlot,
345✔
872
        listFooter: listSlot,
345✔
873
        header: {
345✔
874
          ...listSlot,
345✔
875
          deselect: this.deselect,
345✔
876
          id: this.inputId,
345✔
877
          selectedValue: this.selectedValue,
345✔
878
          open: this.open
345✔
879
        },
345✔
880
        footer: {...listSlot, deselect: this.deselect},
345✔
881
      }
345✔
882
    },
1✔
883

1✔
884
    /**
1✔
885
     * Returns an object containing the child components
1✔
886
     * that will be used throughout the component. The
1✔
887
     * `component` prop can be used to overwrite the defaults.
1✔
888
     *
1✔
889
     * @return {Object}
1✔
890
     */
1✔
891
    childComponents() {
1✔
892
      return {
180✔
893
        ...childComponents,
180✔
894
        ...this.components,
180✔
895
      }
180✔
896
    },
1✔
897

1✔
898
    /**
1✔
899
     * Holds the current state of the component.
1✔
900
     * @return {Object}
1✔
901
     */
1✔
902
    stateClasses() {
1✔
903
      return {
317✔
904
        'vs--open': this.dropdownOpen,
317✔
905
        'vs--single': !this.multiple,
317✔
906
        'vs--multiple': this.multiple,
317✔
907
        'vs--searching': this.searching && !this.noDrop,
317✔
908
        'vs--searchable': this.searchable && !this.noDrop,
317✔
909
        'vs--unsearchable': !this.searchable,
317✔
910
        'vs--loading': this.mutableLoading,
317✔
911
        'vs--disabled': this.disabled,
317✔
912
        'vs--no-drop': this.noDrop,
317✔
913
      }
317✔
914
    },
1✔
915

1✔
916
    /**
1✔
917
     * Return the current state of the
1✔
918
     * search input
1✔
919
     * @return {Boolean} True if non empty value
1✔
920
     */
1✔
921
    searching() {
1✔
922
      return !!this.search
251✔
923
    },
1✔
924

1✔
925
    /**
1✔
926
     * Return the current state of the
1✔
927
     * dropdown menu.
1✔
928
     * @return {Boolean} True if open
1✔
929
     */
1✔
930
    dropdownOpen() {
1✔
931
      return this.dropdownShouldOpen(this)
279✔
932
    },
1✔
933

1✔
934
    /**
1✔
935
     * Return the placeholder string if it's set
1✔
936
     * & there is no value selected.
1✔
937
     * @return {String} Placeholder text
1✔
938
     */
1✔
939
    searchPlaceholder() {
1✔
940
      return this.isValueEmpty && this.placeholder
237✔
941
          ? this.placeholder
237✔
942
          : undefined
237✔
943
    },
1✔
944

1✔
945
    /**
1✔
946
     * The currently displayed options, filtered
1✔
947
     * by the search elements value. If tagging
1✔
948
     * true, the search text will be prepended
1✔
949
     * if it doesn't already exist.
1✔
950
     *
1✔
951
     * @return {array}
1✔
952
     */
1✔
953
    filteredOptions() {
1✔
954
      const optionList = this.normalizeOptGroups(
262✔
955
          Array.prototype.concat(this.optionList)
262✔
956
      )
262✔
957

262✔
958
      if (!this.filterable && !this.taggable) {
262✔
959
        return optionList
1✔
960
      }
1✔
961

261✔
962
      const options = this.search.length
261✔
963
          ? this.filter(optionList, this.search, this)
262✔
964
          : optionList
262✔
965
      if (this.taggable && this.search.length) {
262✔
966
        const createdOption = this.createOption(this.search)
18✔
967
        if (!this.optionExists(createdOption)) {
18✔
968
          options.unshift(createdOption)
15✔
969
        }
15✔
970
      }
18✔
971
      return options
261✔
972
    },
1✔
973

1✔
974
    /**
1✔
975
     * Check if there aren't any options selected.
1✔
976
     * @return {Boolean}
1✔
977
     */
1✔
978
    isValueEmpty() {
1✔
979
      return this.selectedValue.length === 0
240✔
980
    },
1✔
981

1✔
982
    /**
1✔
983
     * Determines if the clear button should be displayed.
1✔
984
     * @return {Boolean}
1✔
985
     */
1✔
986
    showClearButton() {
1✔
987
      return (
254✔
988
          !this.multiple && this.clearable && !this.open && !this.isValueEmpty
254✔
989
      )
254✔
990
    },
1✔
991
  },
1✔
992

1✔
993
  watch: {
1✔
994
    /**
1✔
995
     * Maybe reset the value
1✔
996
     * when options change.
1✔
997
     * Make sure selected option
1✔
998
     * is correct.
1✔
999
     * @return {[type]} [description]
1✔
1000
     */
1✔
1001
    options(newOptions, oldOptions) {
1✔
1002
      const shouldReset = () =>
11✔
1003
          typeof this.resetOnOptionsChange === 'function'
9✔
1004
              ? this.resetOnOptionsChange(
9✔
1005
                  newOptions,
5✔
1006
                  oldOptions,
5✔
1007
                  this.selectedValue
5✔
1008
              )
5✔
1009
              : this.resetOnOptionsChange
11✔
1010

11✔
1011
      if (!this.taggable && shouldReset()) {
11✔
1012
        this.clearSelection()
4✔
1013
      }
4✔
1014

11✔
1015
      if (this.modelValue && this.isTrackingValues) {
11✔
1016
        this.setInternalValueFromOptions(this.modelValue)
1✔
1017
      }
1✔
1018
    },
1✔
1019

1✔
1020
    /**
1✔
1021
     * Make sure to update internal
1✔
1022
     * value if prop changes outside
1✔
1023
     */
1✔
1024
    modelValue: {
1✔
1025
      immediate: true,
1✔
1026
      handler(val) {
1✔
1027
        if (this.isTrackingValues) {
184✔
1028
          this.setInternalValueFromOptions(val)
145✔
1029
        }
145✔
1030
      },
1✔
1031
    },
1✔
1032

1✔
1033
    /**
1✔
1034
     * Always reset the value when
1✔
1035
     * the multiple prop changes.
1✔
1036
     * @return {void}
1✔
1037
     */
1✔
1038
    multiple() {
1✔
1039
      this.clearSelection()
×
1040
    },
1✔
1041

1✔
1042
    open(isOpen) {
1✔
1043
      this.$emit(isOpen ? 'open' : 'close')
80✔
1044
    },
1✔
1045
  },
1✔
1046

1✔
1047
  created() {
1✔
1048
    this.mutableLoading = this.loading
180✔
1049
  },
1✔
1050

1✔
1051
  methods: {
1✔
1052
    onPaste(event) {
1✔
1053
      if (this.pasteSeparator === '' || !this.multiple) {
10✔
1054
        return;
1✔
1055
      }
1✔
1056
      let paste = (event.clipboardData || window.clipboardData).getData("text");
10!
1057
      const tags = paste.split(this.pasteSeparator);
10✔
1058
      this.pasteProcessed = true;
10✔
1059
      tags.forEach(tag => {
10✔
1060
        if (tag !== null && tag !== '') {
21✔
1061
          const created = this.createOption(this.pasteTrim ? tag.trim() : tag);
18✔
1062
          this.select(created);
18✔
1063
        }
18✔
1064
      });
10✔
1065
      this.pasteProcessed = false;
10✔
1066
      let option = this.selectedValue.concat(this.pasteBuffer)
10✔
1067
      this.updateValue(option)
10✔
1068
      this.pasteBuffer = [];
10✔
1069
      event.preventDefault();
10✔
1070
    },
1✔
1071
    clickOutside() {
1✔
1072
      if (this.open) {
×
1073
        this.open = false
×
1074
      }
×
1075
    },
1✔
1076
    /**
1✔
1077
     * Make sure tracked value is
1✔
1078
     * one option if possible.
1✔
1079
     * @param  {Object|String} value
1✔
1080
     * @return {void}
1✔
1081
     */
1✔
1082
    setInternalValueFromOptions(value) {
1✔
1083
      if (Array.isArray(value)) {
146✔
1084
        this.$data._value = value.map((val) =>
2✔
1085
            this.findOptionFromReducedValue(val)
2✔
1086
        )
2✔
1087
      } else {
146✔
1088
        this.$data._value = this.findOptionFromReducedValue(value)
144✔
1089
      }
144✔
1090
    },
1✔
1091

1✔
1092
    /**
1✔
1093
     * Select or deselect a given option.
1✔
1094
     * Allow deselect if clearable or if not the only selected option.
1✔
1095
     * @param  {Object|String} option
1✔
1096
     * @return {void}
1✔
1097
     */
1✔
1098
    select(option) {
1✔
1099
      this.processSelect(option)
65✔
1100
      this.onAfterSelect(option)
65✔
1101
    },
1✔
1102

1✔
1103
    processSelect(option) {
1✔
1104
      this.$emit('option:selecting', option)
66✔
1105
      if (!this.isOptionSelected(option)) {
66✔
1106
        if (this.taggable && !this.optionExists(option)) {
59✔
1107
          /* @TODO: could we use v-model instead of push-tags? */
24✔
1108
          this.$emit('option:created', option)
24✔
1109
          this.pushTag(option)
24✔
1110
        }
24✔
1111
        if (!this.pasteProcessed) {
59✔
1112
          if (this.multiple) {
41✔
1113
            option = this.selectedValue.concat(option)
19✔
1114
          }
19✔
1115
          this.updateValue(option)
41✔
1116
        } else {
59✔
1117
          this.pasteBuffer = this.pasteBuffer.filter((val) => {
18✔
1118
            return !this.optionComparator(val, option)
10✔
1119
          })
18✔
1120
          this.pasteBuffer.push(option)
18✔
1121
          this.$emit('option:selected', option)
18✔
1122
        }
18✔
1123
      } else if (
66✔
1124
          this.deselectFromDropdown &&
7✔
1125
          (this.clearable || (this.multiple && this.selectedValue.length > 1))
2!
1126
      ) {
7✔
1127
        this.deselect(option)
1✔
1128
      }
1✔
1129
    },
1✔
1130

1✔
1131
    /**
1✔
1132
     * De-select a given option.
1✔
1133
     * @param  {Object|String} option
1✔
1134
     * @return {void}
1✔
1135
     */
1✔
1136
    deselect(option) {
1✔
1137
      this.$emit('option:deselecting', option)
13✔
1138
      this.updateValue(
13✔
1139
          this.selectedValue.filter((val) => {
13✔
1140
            return !this.optionComparator(val, option)
8✔
1141
          })
13✔
1142
      )
13✔
1143
      this.$emit('option:deselected', option)
13✔
1144
    },
1✔
1145

1✔
1146
    /**
1✔
1147
     * Clears the currently selected value(s)
1✔
1148
     * @return {void}
1✔
1149
     */
1✔
1150
    clearSelection() {
1✔
1151
      this.updateValue(this.multiple ? [] : null)
5!
1152
    },
1✔
1153

1✔
1154
    /**
1✔
1155
     * Called from this.select after each selection.
1✔
1156
     * @param  {Object|String} option
1✔
1157
     * @return {void}
1✔
1158
     */
1✔
1159
    onAfterSelect(option) {
1✔
1160
      if (this.closeOnSelect) {
65✔
1161
        this.open = !this.open
64✔
1162
        this.searchEl.blur()
64✔
1163
      }
64✔
1164
      if (this.clearSearchOnSelect) {
65✔
1165
        this.search = ''
65✔
1166
      }
65✔
1167
    },
1✔
1168

1✔
1169
    /**
1✔
1170
     * Accepts a selected value, updates local
1✔
1171
     * state when required, and triggers the
1✔
1172
     * input event.
1✔
1173
     *
1✔
1174
     * @emits input
1✔
1175
     * @param value
1✔
1176
     */
1✔
1177
    updateValue(value) {
1✔
1178
      if (typeof this.modelValue === 'undefined') {
71✔
1179
        // Vue select has to manage value
56✔
1180
        this.$data._value = value
56✔
1181
      }
56✔
1182

71✔
1183
      if (value !== null) {
71✔
1184
        if (Array.isArray(value)) {
63✔
1185
          value = value.map((val) => this.reduce(val))
43✔
1186
        } else {
61✔
1187
          value = this.reduce(value)
20✔
1188
        }
20✔
1189
      }
63✔
1190

71✔
1191
      this.$emit('update:modelValue', value)
71✔
1192
    },
1✔
1193

1✔
1194
    /**
1✔
1195
     * Toggle the visibility of the dropdown menu.
1✔
1196
     * @param  {Event} event
1✔
1197
     * @return {void}
1✔
1198
     */
1✔
1199
    toggleDropdown(event) {
1✔
1200
      const targetIsNotSearch = event.target !== this.searchEl
9✔
1201
      if (targetIsNotSearch) {
9✔
1202
        event.preventDefault()
9✔
1203
      }
9✔
1204

9✔
1205
      //  don't react to click on deselect/clear buttons,
9✔
1206
      //  they dropdown state will be set in their click handlers
9✔
1207
      const ignoredButtons = [
9✔
1208
        ...(this.deselectButtons || []),
9!
1209
        ...([this.$refs['clearButton']] || []),
9!
1210
      ]
9✔
1211

9✔
1212
      if (
9✔
1213
          this.searchEl === undefined ||
9✔
1214
          ignoredButtons
9✔
1215
              .filter(Boolean)
9✔
1216
              .some((ref) => ref.contains(event.target) || ref === event.target)
9✔
1217
      ) {
9!
1218
        event.preventDefault()
×
1219
        return
×
1220
      }
×
1221

9✔
1222
      if (this.open && targetIsNotSearch) {
9✔
1223
        this.searchEl.blur()
1✔
1224
      } else if (this.open && !targetIsNotSearch && !this.searchable) {
9!
1225
        this.open = false
×
1226
      } else if (!this.disabled) {
8✔
1227
        this.open = true
7✔
1228
        this.searchEl.focus()
7✔
1229
      }
7✔
1230
    },
1✔
1231

1✔
1232
    /**
1✔
1233
     * Check if the given option is currently selected.
1✔
1234
     * @param  {Object|String}  option
1✔
1235
     * @return {Boolean}        True when selected | False otherwise
1✔
1236
     */
1✔
1237
    isOptionSelected(option) {
1✔
1238
      return this.selectedValue.some((value) =>
444✔
1239
          this.optionComparator(value, option)
231✔
1240
      )
444✔
1241
    },
1✔
1242

1✔
1243
    /**
1✔
1244
     * Check if the given option is optgroup label
1✔
1245
     * @param  {Object|String}  option
1✔
1246
     * @return {Boolean}
1✔
1247
     */
1✔
1248
    isOptGroupOption(option) {
1✔
1249
      return !!option.optgroup
417✔
1250
    },
1✔
1251

1✔
1252
    /**
1✔
1253
     *  Can the current option be removed via the dropdown?
1✔
1254
     */
1✔
1255
    isOptionDeselectable(option) {
1✔
1256
      return this.isOptionSelected(option) && this.deselectFromDropdown
139✔
1257
    },
1✔
1258

1✔
1259
    /**
1✔
1260
     * Determine if two option objects are matching.
1✔
1261
     *
1✔
1262
     * @param a {Object}
1✔
1263
     * @param b {Object}
1✔
1264
     * @returns {boolean}
1✔
1265
     */
1✔
1266
    optionComparator(a, b) {
1✔
1267
      return this.getOptionKey(a) === this.getOptionKey(b)
357✔
1268
    },
1✔
1269

1✔
1270
    /**
1✔
1271
     * Finds an option from the options
1✔
1272
     * where a reduced value matches
1✔
1273
     * the passed in value.
1✔
1274
     *
1✔
1275
     * @param value {Object}
1✔
1276
     * @returns {*}
1✔
1277
     */
1✔
1278
    findOptionFromReducedValue(value) {
1✔
1279
      const predicate = (option) =>
150✔
1280
          JSON.stringify(this.reduce(option)) === JSON.stringify(value)
296✔
1281

150✔
1282
      const matches = [...this.options, ...this.pushedTags].filter(predicate)
150✔
1283

150✔
1284
      if (matches.length === 1) {
150✔
1285
        return matches[0]
15✔
1286
      }
15✔
1287
      /**
135✔
1288
       * This second loop is needed to cover an edge case where `taggable` + `reduce`
135✔
1289
       * were used in conjunction with a `create-option` that doesn't create a
135✔
1290
       * unique reduced value.
135✔
1291
       * @see https://github.com/sagalbot/vue-select/issues/1089#issuecomment-597238735
135✔
1292
       */
135✔
1293
      return (
135✔
1294
          matches.find((match) =>
135✔
1295
              this.optionComparator(match, this.$data._value)
×
1296
          ) || value
150✔
1297
      )
150✔
1298
    },
1✔
1299

1✔
1300
    /**
1✔
1301
     * Delete the value on Delete keypress when there is no
1✔
1302
     * text in the search input, & there's tags to delete
1✔
1303
     * @return {this.value}
1✔
1304
     */
1✔
1305
    maybeDeleteValue() {
1✔
1306
      if (
5✔
1307
          !this.searchEl.value.length &&
5✔
1308
          this.selectedValue &&
5✔
1309
          this.selectedValue.length &&
5✔
1310
          this.clearable
5✔
1311
      ) {
5✔
1312
        let value = null
3✔
1313
        if (this.multiple) {
3✔
1314
          value = [
1✔
1315
            ...this.selectedValue.slice(0, this.selectedValue.length - 1),
1✔
1316
          ]
1✔
1317
        }
1✔
1318
        this.updateValue(value)
3✔
1319
      }
3✔
1320
    },
1✔
1321

1✔
1322
    /**
1✔
1323
     * Determine if an option exists
1✔
1324
     * within this.optionList array.
1✔
1325
     *
1✔
1326
     * @param  {Object || String} option
1✔
1327
     * @return {boolean}
1✔
1328
     */
1✔
1329
    optionExists(option) {
1✔
1330
      return this.optionList.some((_option) =>
59✔
1331
          this.optionComparator(_option, option)
101✔
1332
      )
59✔
1333
    },
1✔
1334

1✔
1335
    /**
1✔
1336
     * Ensures that options are always
1✔
1337
     * passed as objects to scoped slots.
1✔
1338
     * @param option
1✔
1339
     * @return {*}
1✔
1340
     */
1✔
1341
    normalizeOptionForSlot(option) {
1✔
1342
      return typeof option === 'object' ? option : {[this.label]: option}
415✔
1343
    },
1✔
1344

1✔
1345
    /**
1✔
1346
     * If push-tags is true, push the
1✔
1347
     * given option to `this.pushedTags`.
1✔
1348
     *
1✔
1349
     * @param  {Object || String} option
1✔
1350
     * @return {void}
1✔
1351
     */
1✔
1352
    pushTag(option) {
1✔
1353
      this.pushedTags.push(option)
24✔
1354
    },
1✔
1355

1✔
1356
    /**
1✔
1357
     * If there is any text in the search input, remove it.
1✔
1358
     * Otherwise, blur the search input to close the dropdown.
1✔
1359
     * @return {void}
1✔
1360
     */
1✔
1361
    onEscape() {
1✔
1362
      if (!this.search.length) {
2✔
1363
        this.searchEl.blur()
1✔
1364
      } else {
1✔
1365
        this.search = ''
1✔
1366
      }
1✔
1367
    },
1✔
1368

1✔
1369
    /**
1✔
1370
     * Close the dropdown on blur.
1✔
1371
     * @emits  {search:blur}
1✔
1372
     * @return {void}
1✔
1373
     */
1✔
1374
    onSearchBlur() {
1✔
1375
      this.open = false
9✔
1376
      if (this.autoSelect) {
9✔
1377
        this.typeAheadSelect(false);
2✔
1378
      }
2✔
1379
      if (this.mousedown && !this.searching) {
9!
1380
        this.mousedown = false
×
1381
      } else {
9✔
1382
        const {clearSearchOnSelect, multiple} = this
9✔
1383
        if (this.clearSearchOnBlur({clearSearchOnSelect, multiple})) {
9✔
1384
          this.search = ''
7✔
1385
        }
7✔
1386
        this.$emit('search:blur')
9✔
1387
        return
9✔
1388
      }
9!
1389
      // Fixed bug where no-options message could not be closed
×
1390
      if (this.search.length === 0 && this.options.length === 0) {
9!
1391
        this.$emit('search:blur')
×
1392
      }
×
1393
    },
1✔
1394

1✔
1395
    /**
1✔
1396
     * Open the dropdown on focus.
1✔
1397
     * @emits  {search:focus}
1✔
1398
     * @return {void}
1✔
1399
     */
1✔
1400
    onSearchFocus() {
1✔
1401
      this.open = true
16✔
1402
      this.$emit('search:focus')
16✔
1403
    },
1✔
1404

1✔
1405
    /**
1✔
1406
     * Event-Handler to help workaround IE11 (probably fixes 10 as well)
1✔
1407
     * firing a `blur` event when clicking
1✔
1408
     * the dropdown's scrollbar, causing it
1✔
1409
     * to collapse abruptly.
1✔
1410
     * @see https://github.com/sagalbot/vue-select/issues/106
1✔
1411
     * @return {void}
1✔
1412
     */
1✔
1413
    onMousedown() {
1✔
1414
      this.mousedown = true
×
1415
    },
1✔
1416

1✔
1417
    /**
1✔
1418
     * Event-Handler to help workaround IE11 (probably fixes 10 as well)
1✔
1419
     * @see https://github.com/sagalbot/vue-select/issues/106
1✔
1420
     * @return {void}
1✔
1421
     */
1✔
1422
    onMouseUp() {
1✔
1423
      this.mousedown = false
×
1424
    },
1✔
1425

1✔
1426
    /**
1✔
1427
     * Search <input> KeyBoardEvent handler.
1✔
1428
     * @param e {KeyboardEvent}
1✔
1429
     * @return {Function}
1✔
1430
     */
1✔
1431
    onSearchKeyDown(e) {
1✔
1432
      const preventAndSelect = (e) => {
37✔
1433
        e.preventDefault()
24✔
1434
        return !this.isComposing && this.typeAheadSelect()
24✔
1435
      }
24✔
1436

37✔
1437
      const defaults = {
37✔
1438
        //  backspace
37✔
1439
        8: (e) => this.maybeDeleteValue(),
37✔
1440
        //  esc
37✔
1441
        27: (e) => this.onEscape(),
37✔
1442
        //  up.prevent
37✔
1443
        38: (e) => {
37✔
1444
          e.preventDefault()
3✔
1445
          return this.typeAheadUp()
3✔
1446
        },
37✔
1447
        //  down.prevent
37✔
1448
        40: (e) => {
37✔
1449
          e.preventDefault()
3✔
1450
          return this.typeAheadDown()
3✔
1451
        },
37✔
1452
      }
37✔
1453

37✔
1454
      this.selectOnKeyCodes.forEach(
37✔
1455
          (keyCode) => (defaults[keyCode] = preventAndSelect)
37✔
1456
      )
37✔
1457

37✔
1458
      const handlers = this.mapKeydown(defaults, this)
37✔
1459

37✔
1460
      if (typeof handlers[e.keyCode] === 'function') {
37✔
1461
        return handlers[e.keyCode](e)
37✔
1462
      }
37✔
1463
    },
1✔
1464
    /**
1✔
1465
     * optgroups
1✔
1466
     * @param {*} options
1✔
1467
     */
1✔
1468
    normalizeOptGroups(options) {
1✔
1469
      return options
262✔
1470
          .map((item) => {
262✔
1471
            if (
514✔
1472
                item.groupLabel &&
514✔
1473
                item.groupOptions &&
514✔
1474
                item.groupOptions instanceof Array
514✔
1475
            ) {
514✔
1476
              return [{optgroup: item.groupLabel}].concat(item.groupOptions)
3✔
1477
            } else {
514✔
1478
              return [item]
511✔
1479
            }
511✔
1480
          })
262✔
1481
          .reduce((arr, group) => {
262✔
1482
            return arr.concat(group)
514✔
1483
          }, [])
262✔
1484
    },
1✔
1485
  },
1✔
1486
}
1✔
1487
</script>
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc