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

uiv-lib / uiv / 7601639902

21 Jan 2024 01:46PM UTC coverage: 86.79%. Remained the same
7601639902

Pull #837

github

web-flow
chore(deps): update dependency vitepress to v1.0.0-rc.39
Pull Request #837: chore(deps): update dependency vitepress to v1.0.0-rc.39

859 of 1065 branches covered (0.0%)

Branch coverage included in aggregate %.

1539 of 1698 relevant lines covered (90.64%)

184.97 hits per line

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

75.12
/src/components/typeahead/Typeahead.vue
1
<template>
2
  <dropdown
3
    ref="dropdown"
4
    v-model="open"
27✔
5
    tag="section"
6
    :append-to-body="appendToBody"
7
    :not-close-elements="elements"
27✔
8
    :position-element="inputEl"
9
  >
10
    <template #dropdown>
11
      <slot
12
        name="item"
13
        :items="items"
14
        :active-index="activeIndex"
15
        :select="selectItem"
16
        :highlight="highlight"
17
      >
18
        <li
19
          v-for="(item, index) in items"
20
          :key="index"
21
          :class="{ active: activeIndex === index }"
22
        >
23
          <a href="#" @click.prevent="selectItem(item)">
24
            <span v-html="highlight(item)"></span>
27✔
25
          </a>
26
        </li>
27
      </slot>
28
      <slot v-if="!items || items.length === 0" name="empty" />
29
    </template>
30
  </dropdown>
31
</template>
32

33
<script setup>
34
import { request } from '../../utils/http.utils';
35
import { isString } from '../../utils/object.utils';
36
import {
37
  on,
38
  off,
39
  EVENTS,
40
  getElementBySelectorOrRef,
41
} from '../../utils/dom.utils';
42
import Dropdown from '../dropdown/Dropdown.vue';
43
import {
44
  computed,
45
  nextTick,
46
  onBeforeUnmount,
47
  onMounted,
48
  ref,
49
  watch,
50
  useSlots,
51
} from 'vue';
52

53
const slots = useSlots();
54

55
const props = defineProps({
56
  modelValue: { type: null, required: true },
57
  data: { type: Array, default: undefined },
58
  itemKey: { type: String, default: undefined },
59
  appendToBody: { type: Boolean, default: false },
60
  ignoreCase: { type: Boolean, default: true },
61
  matchStart: { type: Boolean, default: false },
62
  forceSelect: { type: Boolean, default: false },
63
  forceClear: { type: Boolean, default: false },
64
  limit: { type: Number, default: 10 },
65
  asyncSrc: { type: String, default: undefined },
66
  asyncKey: { type: String, default: undefined },
67
  asyncFunction: { type: Function, default: undefined },
68
  debounce: { type: Number, default: 200 },
69
  openOnFocus: { type: Boolean, default: true },
70
  openOnEmpty: { type: Boolean, default: false },
71
  target: { required: true, type: null },
72
  preselect: { type: Boolean, default: true },
73
});
74
const emit = defineEmits([
75
  'update:modelValue',
76
  'loading',
77
  'loaded',
78
  'loaded-error',
79
  'selected-item-changed',
80
]);
81

82
const inputEl = ref(null);
83
const items = ref([]);
84
const activeIndex = ref(0);
85
const elements = ref([]);
86
const open = ref(false);
87
const dropdown = ref(null);
88

89
let dropdownMenuEl = null;
90
let timeoutID = 0;
91

92
const regexOptions = computed(() => {
93
  let options = '';
94
  if (props.ignoreCase) {
95
    options += 'i';
96
  }
97
  if (!props.matchStart) {
98
    options += 'g';
99
  }
100
  return options;
101
});
20✔
102

103
watch(
104
  () => props.target,
20✔
105
  (el) => {
106
    removeListeners();
107
    initInputElByTarget(el);
108
    initListeners();
109
  }
20✔
110
);
20✔
111
watch(
112
  () => props.modelValue,
113
  (value) => {
20✔
114
    setInputTextByValue(value);
115
  }
116
);
117
watch(
118
  () => activeIndex.value,
119
  (index) => {
120
    index >= 0 && emit('selected-item-changed', index);
20✔
121
  }
122
);
123

124
onMounted(async () => {
125
  await nextTick();
126
  initInputElByTarget(props.target);
127
  initListeners();
20✔
128
  dropdownMenuEl = dropdown.value.$el.querySelector('.dropdown-menu');
129
  // set input text if v-model not empty
130
  if (props.modelValue) {
131
    setInputTextByValue(props.modelValue);
132
  }
133
});
134

20✔
135
onBeforeUnmount(() => {
136
  removeListeners();
137
});
138

139
function setInputTextByValue(value) {
140
  if (isString(value)) {
141
    // direct
20✔
142
    inputEl.value.value = value;
143
  } else if (value) {
144
    // is object
145
    inputEl.value.value = props.itemKey ? value[props.itemKey] : value;
146
  } else if (value === null) {
147
    // is null or undefined or something else not valid
148
    inputEl.value.value = '';
20✔
149
  }
150
}
151

152
function hasEmptySlot() {
153
  return !!slots.empty;
20✔
154
}
20✔
155

156
function initInputElByTarget(target) {
157
  if (!target) {
20✔
158
    return;
159
  }
160
  inputEl.value = getElementBySelectorOrRef(target);
161
}
162

15✔
163
function initListeners() {
15✔
164
  if (inputEl.value) {
14✔
165
    elements.value = [inputEl.value];
166
    on(inputEl.value, EVENTS.FOCUS, inputFocused);
15✔
167
    on(inputEl.value, EVENTS.BLUR, inputBlured);
14✔
168
    on(inputEl.value, EVENTS.INPUT, inputChanged);
169
    on(inputEl.value, EVENTS.KEY_DOWN, inputKeyPressed);
15✔
170
  }
171
}
172

173
function removeListeners() {
174
  elements.value = [];
175
  if (inputEl.value) {
176
    off(inputEl.value, EVENTS.FOCUS, inputFocused);
177
    off(inputEl.value, EVENTS.BLUR, inputBlured);
178
    off(inputEl.value, EVENTS.INPUT, inputChanged);
179
    off(inputEl.value, EVENTS.KEY_DOWN, inputKeyPressed);
180
  }
181
}
182

183
function prepareItems(data, disableFilters = false) {
184
  if (disableFilters) {
185
    items.value = data.slice(0, props.limit);
186
    return;
187
  }
188
  items.value = [];
189
  activeIndex.value = props.preselect ? 0 : -1;
190
  for (let i = 0, l = data.length; i < l; i++) {
191
    const item = data[i];
192
    let key = props.itemKey ? item[props.itemKey] : item;
193
    key = key.toString();
194
    let index = -1;
195
    if (props.ignoreCase) {
196
      index = key.toLowerCase().indexOf(inputEl.value.value.toLowerCase());
197
    } else {
198
      index = key.indexOf(inputEl.value.value);
199
    }
200
    if (props.matchStart ? index === 0 : index >= 0) {
201
      items.value.push(item);
202
    }
203
    if (items.value.length >= props.limit) {
204
      break;
205
    }
206
  }
207
}
208

209
function fetchItems(value, debounce) {
210
  clearTimeout(timeoutID);
211
  if (value === '' && !props.openOnEmpty) {
212
    open.value = false;
213
  } else if (props.data) {
214
    prepareItems(props.data);
215
    open.value = hasEmptySlot() || !!items.value.length;
216
  } else if (props.asyncSrc) {
217
    timeoutID = setTimeout(() => {
218
      emit('loading');
219
      request(props.asyncSrc + encodeURIComponent(value))
220
        .then((data) => {
221
          if (inputEl.value.matches(':focus')) {
222
            prepareItems(props.asyncKey ? data[props.asyncKey] : data, true);
223
            open.value = hasEmptySlot() || !!items.value.length;
224
          }
225
          emit('loaded');
226
        })
227
        .catch((err) => {
228
          console.error(err);
23✔
229
          emit('loaded-error');
230
        });
231
    }, debounce);
232
  } else if (props.asyncFunction) {
233
    const cb = (data) => {
234
      if (inputEl.value.matches(':focus')) {
235
        prepareItems(data, true);
236
        open.value = hasEmptySlot() || !!items.value.length;
17✔
237
      }
6✔
238
      emit('loaded');
239
    };
4!
240
    timeoutID = setTimeout(() => {
2✔
241
      emit('loading');
242
      props.asyncFunction(value, cb);
1✔
243
    }, debounce);
244
  }
245
}
246

17✔
247
function inputChanged() {
248
  const value = inputEl.value.value;
249
  fetchItems(value, props.debounce);
23!
250
  emit('update:modelValue', props.forceSelect ? undefined : value);
×
251
}
252

23✔
253
function inputFocused() {
254
  if (props.openOnFocus) {
255
    const value = inputEl.value.value;
256
    fetchItems(value, 0);
257
  }
258
}
259

260
async function inputBlured() {
261
  if (!dropdownMenuEl.matches(':hover')) {
23✔
262
    open.value = false;
22✔
263
  }
264
  if (inputEl.value && props.forceClear) {
265
    await nextTick();
266
    if (typeof props.modelValue === 'undefined') {
267
      inputEl.value.value = '';
268
    }
269
  }
270
}
271

272
function inputKeyPressed(event) {
273
  event.stopPropagation();
274
  if (open.value) {
275
    switch (event.keyCode) {
276
      case 13:
277
        if (activeIndex.value >= 0) {
278
          selectItem(items.value[activeIndex.value]);
279
        } else {
280
          open.value = false;
281
        }
282
        event.preventDefault();
283
        break;
284
      case 27:
285
        open.value = false;
286
        break;
287
      case 38:
288
        activeIndex.value = activeIndex.value > 0 ? activeIndex.value - 1 : 0;
289
        break;
290
      case 40: {
291
        const maxIndex = items.value.length - 1;
292
        activeIndex.value =
293
          activeIndex.value < maxIndex ? activeIndex.value + 1 : maxIndex;
294
        break;
295
      }
296
    }
297
  }
298
}
299

300
function selectItem(item) {
301
  emit('update:modelValue', item);
302
  open.value = false;
303
}
304

305
function highlight(item) {
306
  const value = props.itemKey ? item[props.itemKey] : item;
23✔
307
  const inputValue = inputEl.value.value.replace(
23✔
308
    /[-[\]{}()*+?.,\\^$|#\s]/g,
309
    '\\$&'
310
  );
311
  return value.replace(
312
    new RegExp(`${inputValue}`, regexOptions.value),
313
    '<b>$&</b>'
314
  );
315
}
316
</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