github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/app/mixins/searchable.js (about)

     1  import Mixin from '@ember/object/mixin';
     2  import { get, computed } from '@ember/object';
     3  import { reads } from '@ember/object/computed';
     4  import Fuse from 'fuse.js';
     5  
     6  /**
     7    Searchable mixin
     8  
     9    Simple search filtering behavior for a list of objects.
    10  
    11    Properties to override:
    12      - searchTerm: the string to use as a query
    13      - searchProps: the props on each object to search
    14      -- exactMatchSearchProps: the props for exact search when props are different per search type
    15      -- regexSearchProps: the props for regex search when props are different per search type
    16      -- fuzzySearchProps: the props for fuzzy search when props are different per search type
    17      - exactMatchEnabled: (true) disable to not use the exact match search type
    18      - fuzzySearchEnabled: (false) enable to use the fuzzy search type
    19      - regexEnabled: (true) disable to disable the regex search type
    20      - listToSearch: the list of objects to search
    21  
    22    Properties provided:
    23      - listSearched: a subset of listToSearch of items that meet the search criteria
    24  */
    25  // eslint-disable-next-line ember/no-new-mixins
    26  export default Mixin.create({
    27    searchTerm: '',
    28    listToSearch: computed(function () {
    29      return [];
    30    }),
    31  
    32    searchProps: null,
    33    exactMatchSearchProps: reads('searchProps'),
    34    regexSearchProps: reads('searchProps'),
    35    fuzzySearchProps: reads('searchProps'),
    36  
    37    // Three search modes
    38    exactMatchEnabled: true,
    39    fuzzySearchEnabled: false,
    40    includeFuzzySearchMatches: false,
    41    regexEnabled: true,
    42  
    43    // Search should reset pagination. Not every instance of
    44    // search will be paired with pagination, but it's still
    45    // preferable to generalize this rather than risking it being
    46    // forgotten on a single page.
    47    resetPagination() {
    48      if (this.currentPage != null) {
    49        this.set('currentPage', 1);
    50      }
    51    },
    52  
    53    fuse: computed(
    54      'fuzzySearchProps.[]',
    55      'includeFuzzySearchMatches',
    56      'listToSearch.[]',
    57      function () {
    58        return new Fuse(this.listToSearch, {
    59          shouldSort: true,
    60          threshold: 0.4,
    61          location: 0,
    62          distance: 100,
    63          tokenize: true,
    64          matchAllTokens: true,
    65          maxPatternLength: 32,
    66          minMatchCharLength: 1,
    67          includeMatches: this.includeFuzzySearchMatches,
    68          keys: this.fuzzySearchProps || [],
    69          getFn(item, key) {
    70            return get(item, key);
    71          },
    72        });
    73      }
    74    ),
    75  
    76    listSearched: computed(
    77      'exactMatchEnabled',
    78      'exactMatchSearchProps.[]',
    79      'fuse',
    80      'fuzzySearchEnabled',
    81      'fuzzySearchProps.[]',
    82      'includeFuzzySearchMatches',
    83      'listToSearch.[]',
    84      'regexEnabled',
    85      'regexSearchProps.[]',
    86      'searchTerm',
    87      function () {
    88        const searchTerm = this.searchTerm.trim();
    89  
    90        if (!searchTerm || !searchTerm.length) {
    91          return this.listToSearch;
    92        }
    93  
    94        const results = [];
    95  
    96        if (this.exactMatchEnabled) {
    97          results.push(
    98            ...exactMatchSearch(
    99              searchTerm,
   100              this.listToSearch,
   101              this.exactMatchSearchProps
   102            )
   103          );
   104        }
   105  
   106        if (this.fuzzySearchEnabled) {
   107          let fuseSearchResults = this.fuse.search(searchTerm);
   108  
   109          if (this.includeFuzzySearchMatches) {
   110            fuseSearchResults = fuseSearchResults.map((result) => {
   111              const item = result.item;
   112              item.set('fuzzySearchMatches', result.matches);
   113              return item;
   114            });
   115          }
   116  
   117          results.push(...fuseSearchResults);
   118        }
   119  
   120        if (this.regexEnabled) {
   121          results.push(
   122            ...regexSearch(searchTerm, this.listToSearch, this.regexSearchProps)
   123          );
   124        }
   125  
   126        return results.uniq();
   127      }
   128    ),
   129  });
   130  
   131  function exactMatchSearch(term, list, keys) {
   132    if (term.length) {
   133      return list.filter((item) => keys.some((key) => get(item, key) === term));
   134    }
   135  }
   136  
   137  function regexSearch(term, list, keys) {
   138    if (term.length) {
   139      try {
   140        const regex = new RegExp(term, 'i');
   141        // Test the value of each key for each object against the regex
   142        // All that match are returned.
   143        return list.filter((item) =>
   144          keys.some((key) => regex.test(get(item, key)))
   145        );
   146      } catch (e) {
   147        // Swallow the error; most likely due to an eager search of an incomplete regex
   148      }
   149      return [];
   150    }
   151  }