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

clay / clay-kiln / 5133

pending completion
5133

Pull #1560

circleci

james-owen
reduce fuzzy search results to 10
Pull Request #1560: feat: option for fuzzy matching for autocomplete fields

1246 of 3611 branches covered (34.51%)

Branch coverage included in aggregate %.

7 of 7 new or added lines in 1 file covered. (100.0%)

2171 of 6176 relevant lines covered (35.15%)

6.3 hits per line

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

5.26
/inputs/autocomplete.vue
1
<template>
2
  <ol class="autocomplete" v-if="showMatches && !disabled">
3
    <li v-for="(match, index) in matches">
4
      <item
5
        :index="index"
6
        :focusIndex="activeIndex"
7
        :value="match"
8
        :select="select"
9
        :allowRemove="args.allowRemove"
10
        :remove="removeFromList"
11
        ></item>
12
    </li>
13
  </ol>
14
</template>
15

16
<script>
17
  import _ from 'lodash';
1✔
18
  import item from './autocomplete-item.vue';
1✔
19
  import { getItemIndex, getProp } from '../lib/lists/helpers';
1✔
20
  import Fuse from 'fuse.js';
1✔
21

22
  export default {
23
    props: ['args', 'select', 'query', 'focusIndex', 'updateFocusIndex', 'updateMatches', 'unselect', 'disabled'],
24
    data() {
25
      return {
×
26
        localIndex: null,
27
        prevFocusIndex: null,
28
        listItems: [],
29
        additionalPixels: 0
30
      };
31
    },
32
    computed: {
33
      showMatches() {
34
        return this.query.length >= 2 && this.matches.length;
×
35
      },
36
      matches() {
×
37
        const query = this.query || '';
×
38
        let matches;
×
39

40
        if (this.args.fuzzy) {
×
41
          // fuse returns the index of the match
42
          const elements = new Fuse(this.listItems, { threshold: 0.4 }).search(query);
×
43

44
          matches = elements.map((el) => this.listItems[el]).slice(0, 10);
×
45
        } else {
46
          matches = _.take(_.filter(this.listItems, (option) => {
×
47
            return _.includes(option.toLowerCase(), query.toLowerCase());
×
48
          }), 20);
49
        }
50

51
        this.updateMatches(matches);
×
52

53
        return matches;
×
54
      },
55
      activeIndex() {
56
        var activeIndex,
×
57
          matchesLength = this.matches.length;
58

59
        if (_.isNumber(this.focusIndex)) {
×
60
          if (this.focusIndex < 0) {
×
61
            activeIndex = matchesLength - 1;
×
62
          } else {
63
            activeIndex = this.focusIndex % matchesLength;
×
64
          }
65
        } else if (this.prevFocusIndex && !this.focusIndex) {
×
66
          this.matches = [];
×
67
        }
68

69
        // Update parent with new focus value
70
        this.updateFocusIndex(activeIndex);
×
71
        // Cache the previous to know the direction
72
        this.prevFocusIndex = this.focusIndex;
×
73
        // Return the active index
74
        return activeIndex;
×
75
      }
76
    },
77
    watch: {
78
      matches(val) {
79
        let pixelLength = val ? val.length * 40 : 0;
×
80

81
        // only rachet up the size, never make the size smaller
82
        if (val && this.showMatches && pixelLength > this.additionalPixels) {
×
83
          this.additionalPixels = pixelLength;
×
84
          // when matches change, potentially resize the form
85
          this.$root.$emit('resize-form', pixelLength);
×
86
        }
87
      }
88
    },
89
    methods: {
90
      fetchListItems() {
×
91
        const listName = this.args.list,
×
92
          lists = this.$store.state.lists,
93
          items = _.get(lists, `${listName}.items`);
94
        let promise;
×
95

96
        if (items) {
×
97
          promise = Promise.resolve(items);
×
98
        } else {
99
          promise = this.$store.dispatch('getList', listName).then(() => _.get(lists, `${listName}.items`));
×
100
        }
101

102
        return promise.then((listItems) => {
×
103
          this.listItems = _.map(listItems, item => _.isObject(item) ? item.text : item);
×
104
        });
105
      },
106
      removeFromList(item) {
×
107
        const listName = this.args.list;
×
108

109
        this.unselect();
×
110

111
        return this.$store.dispatch('patchList', {
×
112
          listName: listName,
113
          fn: (items) => {
114
            const stringProperty = getProp(items, 'text'),
×
115
              index = getItemIndex(items, item, stringProperty);
116

117
            if (index !== -1) {
×
118
              return { remove: [items[index]] };
×
119
            }
120
          }
121
        }).then(list => this.listItems = _.map(list, item => _.isObject(item) ? item.text : item));
×
122
      }
123
    },
124
    mounted() {
125
      this.fetchListItems();
×
126
    },
127
    components: {
128
      item
129
    }
130
  };
131
</script>
132

133
<style lang="sass">
134
  @import '../styleguide/colors';
135

136
  .autocomplete {
137
    background-color: $card-bg-color;
138
    box-shadow: 1px 2px 8px $md-grey-600;
139
    display: block;
140
    list-style: none;
141
    margin: 0 0 8px;
142
    padding: 0;
143
    position: absolute;
144
    top: 100%;
145
    width: 100%;
146
    z-index: 1;
147
  }
148
</style>
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