github.com/hernad/nomad@v1.6.112/ui/app/mixins/searchable.js (about)

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