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

uiv-lib / uiv / 7229750628

16 Dec 2023 12:21AM CUT coverage: 86.79%. Remained the same
7229750628

push

github

web-flow
chore(deps): update dependency eslint to v8.56.0

859 of 1065 branches covered (0.0%)

Branch coverage included in aggregate %.

1539 of 1698 relevant lines covered (90.64%)

185.22 hits per line

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

73.11
/src/components/select/MultiSelect.vue
1
<template>
2
  <dropdown
3
    ref="dropdown"
4
    v-model="showDropdown"
27✔
5
    :not-close-elements="els"
6
    :append-to-body="appendToBody"
7
    :disabled="disabled"
27✔
8
    :style="containerStyles"
9
    @keydown.esc="showDropdown = false"
10
  >
11
    <div
12
      class="form-control dropdown-toggle clearfix"
13
      :class="selectClasses"
14
      :disabled="disabled ? true : undefined"
15
      tabindex="0"
16
      data-role="trigger"
17
      @focus="$emit('focus', $event)"
18
      @blur="$emit('blur', $event)"
27✔
19
      @keydown.prevent.stop.down="goNextOption"
20
      @keydown.prevent.stop.up="goPrevOption"
21
      @keydown.prevent.stop.enter="selectOption"
22
    >
23
      <div
24
        class="pull-right"
25
        style="display: inline-block; vertical-align: middle"
26
      >
27
        <span>&nbsp;</span>
28
        <span class="caret"></span>
29
      </div>
30
      <div
31
        :class="selectTextClasses"
32
        style="overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap"
33
        v-text="selectedText"
34
      ></div>
35
    </div>
36
    <template #dropdown>
37
      <li v-if="filterable" style="padding: 4px 8px">
38
        <input
39
          ref="filterInput"
40
          v-model="filterInput"
41
          aria-label="Filter..."
42
          class="form-control input-sm"
43
          type="text"
44
          :placeholder="
45
            filterPlaceholder || t('uiv.multiSelect.filterPlaceholder')
46
          "
47
          @keyup.enter="searchClicked"
48
          @keydown.prevent.stop.down="goNextOption"
49
          @keydown.prevent.stop.up="goPrevOption"
50
          @keydown.prevent.stop.enter="selectOption"
51
        />
52
      </li>
53
      <template v-for="(item, i) in groupedOptions">
54
        <li
55
          v-if="item.$group"
56
          :key="i"
57
          class="dropdown-header"
58
          v-text="item.$group"
59
        ></li>
60
        <template v-for="(_item, j) in item.options" :key="`${i}_${j}`">
61
          <li
62
            :class="itemClasses(_item)"
63
            style="outline: 0"
64
            @keydown.prevent.stop.down="goNextOption"
65
            @keydown.prevent.stop.up="goPrevOption"
66
            @keydown.prevent.stop.enter="selectOption"
67
            @click.stop="toggle(_item, $event)"
68
            @mouseenter="currentActive = -1"
69
          >
70
            <a v-if="customOptionsVisible" role="button" style="outline: 0">
71
              <slot name="option" :item="_item" />
72
              <span
73
                v-if="selectedIcon && isItemSelected(_item)"
74
                :class="selectedIconClasses"
75
              ></span>
76
            </a>
77
            <a
78
              v-else-if="isItemSelected(_item)"
79
              role="button"
80
              style="outline: 0"
81
            >
82
              <b>{{ _item[labelKey] }}</b>
83
              <span v-if="selectedIcon" :class="selectedIconClasses"></span>
84
            </a>
85
            <a v-else role="button" style="outline: 0">
86
              <span>{{ _item[labelKey] }}</span>
87
            </a>
88
          </li>
89
        </template>
90
      </template>
91
    </template>
92
  </dropdown>
93
</template>
94

95
<script>
96
import { t } from '../../locale';
97
import Dropdown from '../dropdown/Dropdown.vue';
98
import { onlyUnique } from '../../utils/array.utils';
99

100
export default {
101
  components: { Dropdown },
102
  props: {
19✔
103
    modelValue: { type: Array, required: true },
104
    options: { type: Array, required: true },
105
    labelKey: { type: String, default: 'label' },
106
    valueKey: { type: String, default: 'value' },
107
    limit: { type: Number, default: 0 },
108
    size: { type: String, default: undefined },
109
    placeholder: { type: String, default: undefined },
110
    split: { type: String, default: ', ' },
111
    disabled: { type: Boolean, default: false },
19✔
112
    appendToBody: { type: Boolean, default: false },
19✔
113
    block: { type: Boolean, default: false },
114
    collapseSelected: { type: Boolean, default: false },
115
    filterable: { type: Boolean, default: false },
116
    filterAutoFocus: { type: Boolean, default: true },
32✔
117
    filterFunction: { type: Function, default: undefined },
10✔
118
    filterPlaceholder: { type: String, default: undefined },
2✔
119
    selectedIcon: { type: String, default: 'glyphicon glyphicon-ok' },
120
    itemSelectedClass: { type: String, default: undefined },
8✔
121
  },
40✔
122
  emits: [
123
    'focus',
124
    'blur',
22✔
125
    'visible-change',
126
    'update:modelValue',
127
    'change',
128
    'limit-exceed',
116✔
129
    'search',
130
  ],
131
  data() {
132
    return {
31✔
133
      showDropdown: false,
116✔
134
      els: [],
135
      filterInput: '',
136
      currentActive: -1,
137
    };
138
  },
31✔
139
  computed: {
140
    containerStyles() {
141
      return {
19✔
142
        width: this.block ? '100%' : '',
143
      };
144
    },
145
    filteredOptions() {
146
      if (this.filterable && this.filterInput) {
6✔
147
        if (this.filterFunction) {
148
          return this.filterFunction(this.filterInput);
149
        } else {
150
          const filterInput = this.filterInput.toLowerCase();
151
          return this.options.filter(
152
            (v) =>
53✔
153
              v[this.valueKey].toString().toLowerCase().indexOf(filterInput) >=
154
                0 ||
155
              v[this.labelKey].toString().toLowerCase().indexOf(filterInput) >=
156
                0
157
          );
154✔
158
        }
31✔
159
      } else {
70✔
160
        return this.options;
70✔
161
      }
162
    },
163
    groupedOptions() {
164
      return this.filteredOptions
53✔
165
        .map((v) => v.group)
31✔
166
        .filter(onlyUnique)
31✔
167
        .map((v) => ({
9✔
168
          options: this.filteredOptions.filter((option) => option.group === v),
9✔
169
          $group: v,
9✔
170
        }));
171
    },
22✔
172
    flattenGroupedOptions() {
173
      return [].concat(...this.groupedOptions.map((v) => v.options));
174
    },
22✔
175
    selectClasses() {
176
      return {
177
        [`input-${this.size}`]: this.size,
178
      };
18✔
179
    },
180
    selectedIconClasses() {
181
      return {
182
        [this.selectedIcon]: true,
183
        'pull-right': true,
184
      };
11✔
185
    },
11✔
186
    selectTextClasses() {
11✔
187
      return {
11✔
188
        'text-muted': this.modelValue.length === 0,
3✔
189
      };
3✔
190
    },
191
    labelValue() {
192
      const optionsByValue = this.options.map((v) => v[this.valueKey]);
193
      return this.modelValue.map((v) => {
194
        const index = optionsByValue.indexOf(v);
195
        return index >= 0 ? this.options[index][this.labelKey] : v;
19✔
196
      });
197
    },
198
    selectedText() {
199
      if (this.modelValue.length) {
200
        const labelValue = this.labelValue;
201
        if (this.collapseSelected) {
202
          let str = labelValue[0];
203
          str +=
204
            labelValue.length > 1
×
205
              ? `${this.split}+${labelValue.length - 1}`
×
206
              : '';
207
          return str;
×
208
        } else {
209
          return labelValue.join(this.split);
210
        }
×
211
      } else {
×
212
        return this.placeholder || this.t('uiv.multiSelect.placeholder');
213
      }
×
214
    },
215
    customOptionsVisible() {
216
      return !!this.$slots.option || !!this.$slots.option;
×
217
    },
×
218
  },
×
219
  watch: {
×
220
    showDropdown(v) {
×
221
      // clear filter input when dropdown toggles
×
222
      this.filterInput = '';
223
      this.currentActive = -1;
224
      this.$emit('visible-change', v);
225
      if (v && this.filterable && this.filterAutoFocus) {
367✔
226
        this.$nextTick(() => {
227
          this.$refs.filterInput.focus();
228
        });
229
      }
367✔
230
    },
15✔
231
  },
232
  mounted() {
367✔
233
    this.els = [this.$el];
234
  },
235
  methods: {
382✔
236
    t,
237
    goPrevOption() {
238
      if (!this.showDropdown) {
38✔
239
        return;
2✔
240
      }
241
      this.currentActive > 0
36✔
242
        ? this.currentActive--
36✔
243
        : (this.currentActive = this.flattenGroupedOptions.length - 1);
36✔
244
    },
4✔
245
    goNextOption() {
4✔
246
      if (!this.showDropdown) {
4✔
247
        return;
248
      }
32✔
249
      this.currentActive < this.flattenGroupedOptions.length - 1
13✔
250
        ? this.currentActive++
13✔
251
        : (this.currentActive = 0);
13✔
252
    },
13✔
253
    selectOption() {
19✔
254
      const index = this.currentActive;
17✔
255
      const options = this.flattenGroupedOptions;
17✔
256
      if (!this.showDropdown) {
17✔
257
        this.showDropdown = true;
17✔
258
      } else if (index >= 0 && index < options.length) {
259
        this.toggle(options[index]);
2✔
260
      }
261
    },
262
    itemClasses(item) {
263
      const result = {
264
        disabled: item.disabled,
×
265
        active: this.currentActive === this.flattenGroupedOptions.indexOf(item),
266
      };
267
      if (this.itemSelectedClass) {
268
        result[this.itemSelectedClass] = this.isItemSelected(item);
27✔
269
      }
270
      return result;
27✔
271
    },
272
    isItemSelected(item) {
273
      return this.modelValue.indexOf(item[this.valueKey]) >= 0;
27✔
274
    },
27✔
275
    toggle(item) {
27✔
276
      if (item.disabled) {
27✔
277
        return;
278
      }
279
      const value = item[this.valueKey];
280
      const index = this.modelValue.indexOf(value);
281
      if (this.limit === 1) {
282
        const newValue = index >= 0 ? [] : [value];
283
        this.$emit('update:modelValue', newValue);
284
        this.$emit('change', newValue);
285
      } else {
27✔
286
        if (index >= 0) {
27✔
287
          const newVal = this.modelValue.slice();
288
          newVal.splice(index, 1);
289
          this.$emit('update:modelValue', newVal);
290
          this.$emit('change', newVal);
291
        } else if (this.limit === 0 || this.modelValue.length < this.limit) {
292
          const newVal = this.modelValue.slice();
27✔
293
          newVal.push(value);
27✔
294
          this.$emit('update:modelValue', newVal);
27✔
295
          this.$emit('change', newVal);
27✔
296
        } else {
297
          this.$emit('limit-exceed');
298
        }
299
      }
300
    },
301
    searchClicked() {
302
      this.$emit('search', this.filterInput);
27✔
303
    },
304
  },
305
};
306
</script>
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