| 
 | 1 | +import Component from '@glimmer/component';  | 
 | 2 | +import { service } from '@ember/service';  | 
 | 3 | +import { LinkTo } from '@ember/routing';  | 
 | 4 | +import { array } from '@ember/helper';  | 
 | 5 | +import { htmlSafe } from '@ember/template';  | 
 | 6 | +import eq from 'ember-truth-helpers/helpers/eq';  | 
 | 7 | +import { on } from '@ember/modifier';  | 
 | 8 | +import Search from './search';  | 
 | 9 | + | 
 | 10 | +const SearchResultGroupHeader = <template>  | 
 | 11 | +  <div class='search-results--group-header' ...attributes>  | 
 | 12 | +    {{yield}}  | 
 | 13 | +  </div>  | 
 | 14 | +</template>;  | 
 | 15 | + | 
 | 16 | +const SearchResultLinkContents = <template>  | 
 | 17 | +  <span class='screen-reader-text'>{{@groupName}}</span>  | 
 | 18 | +  {{htmlSafe @result._highlightResult.hierarchy.lvl2.value}}  | 
 | 19 | +  {{! Do these ever display in API Docs? }}  | 
 | 20 | +  {{#if @result._highlightResult.hierarchy.lvl3}}  | 
 | 21 | +    <span aria-hidden='true'> › </span>  | 
 | 22 | +    {{htmlSafe @result._highlightResult.hierarchy.lvl3.value}}  | 
 | 23 | +  {{/if}}  | 
 | 24 | +  {{#if @result._highlightResult.hierarchy.lvl4}}  | 
 | 25 | +    <span aria-hidden='true'> › </span>  | 
 | 26 | +    {{htmlSafe @result._highlightResult.hierarchy.lvl4.value}}  | 
 | 27 | +  {{/if}}  | 
 | 28 | +</template>;  | 
 | 29 | + | 
 | 30 | +const SearchResult = class SearchResult extends Component {  | 
 | 31 | +  @service router;  | 
 | 32 | + | 
 | 33 | +  get module() {  | 
 | 34 | +    if (this.args.result?.project) {  | 
 | 35 | +      return this.args.result?.project;  | 
 | 36 | +    }  | 
 | 37 | +    let module = this.args.result?.module;  | 
 | 38 | +    if (module.includes('ember-data')) {  | 
 | 39 | +      return 'ember-data';  | 
 | 40 | +    }  | 
 | 41 | +    return 'ember';  | 
 | 42 | +  }  | 
 | 43 | + | 
 | 44 | +  get version() {  | 
 | 45 | +    let versionTag = (this.args.result?._tags ?? []).find(  | 
 | 46 | +      (_tag) => _tag.indexOf('version:') > -1,  | 
 | 47 | +    );  | 
 | 48 | +    let versionSegments = versionTag.replace('version:', '').split('.');  | 
 | 49 | +    return `${versionSegments[0]}.${versionSegments[1]}`;  | 
 | 50 | +  }  | 
 | 51 | + | 
 | 52 | +  urlForClass = (result) => {  | 
 | 53 | +    return `${this.router.urlFor('project-version.classes.class', this.module, this.version, result.class)}#${result.name}`;  | 
 | 54 | +  };  | 
 | 55 | + | 
 | 56 | +  <template>  | 
 | 57 | +    <div class='search-results--result'>  | 
 | 58 | +      <div class='search-results--subcategory-column' aria-hidden='true'>  | 
 | 59 | +        {{#if (eq @groupPosition 0)}}  | 
 | 60 | +          {{@groupName}}  | 
 | 61 | +        {{/if}}  | 
 | 62 | +      </div>  | 
 | 63 | +      <div class='search-results--content'>  | 
 | 64 | +        {{#if @result.static}}  | 
 | 65 | +          <LinkTo  | 
 | 66 | +            @route='project-version.functions.function'  | 
 | 67 | +            @models={{array  | 
 | 68 | +              this.module  | 
 | 69 | +              this.version  | 
 | 70 | +              @result.class  | 
 | 71 | +              @result.name  | 
 | 72 | +            }}  | 
 | 73 | +            {{on 'click' @closeMenu}}  | 
 | 74 | +            data-test-search-result  | 
 | 75 | +          >  | 
 | 76 | +            <SearchResultLinkContents  | 
 | 77 | +              @result={{@result}}  | 
 | 78 | +              @groupName={{@groupName}}  | 
 | 79 | +            />  | 
 | 80 | +          </LinkTo>  | 
 | 81 | +        {{else}}  | 
 | 82 | +          <a href='{{this.urlForClass @result}}' data-test-search-result>  | 
 | 83 | +            <SearchResultLinkContents  | 
 | 84 | +              @result={{@result}}  | 
 | 85 | +              @groupName={{@groupName}}  | 
 | 86 | +            />  | 
 | 87 | +          </a>  | 
 | 88 | +        {{/if}}  | 
 | 89 | +      </div>  | 
 | 90 | +    </div>  | 
 | 91 | +  </template>  | 
 | 92 | +};  | 
 | 93 | + | 
 | 94 | +const SearchResults = class SearchBox extends Component {  | 
 | 95 | +  get groupedResults() {  | 
 | 96 | +    let results = this.args.results;  | 
 | 97 | +    if (!results.length) {  | 
 | 98 | +      return {};  | 
 | 99 | +    }  | 
 | 100 | + | 
 | 101 | +    const lvl0Group = results.reduce((previous, current) => {  | 
 | 102 | +      // Remap all lowercase usages of 'guides' to 'Guides'  | 
 | 103 | +      let lvl0 = current?.hierarchy?.lvl0;  | 
 | 104 | +      // If lvl0 doesn't exist in the resulting object, create the array  | 
 | 105 | +      if (!previous[lvl0]) {  | 
 | 106 | +        previous[lvl0] = [];  | 
 | 107 | +      }  | 
 | 108 | +      // Insert the current item into the resulting object.  | 
 | 109 | +      previous[lvl0].push(current);  | 
 | 110 | +      return previous;  | 
 | 111 | +    }, {});  | 
 | 112 | + | 
 | 113 | +    /*  | 
 | 114 | +    lvl0Group = {  | 
 | 115 | +      lvl0key: algoliaHit  | 
 | 116 | +    }  | 
 | 117 | +    */  | 
 | 118 | +    // https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/js/#response-information  | 
 | 119 | +    // Iterate over every lvl0 group, group by lvl1  | 
 | 120 | +    return Object.keys(lvl0Group).reduce((lvl0Result, lvl0Key) => {  | 
 | 121 | +      // Inject lvl1 grouped results into lvl0  | 
 | 122 | +      lvl0Result[lvl0Key] = lvl0Group[lvl0Key].reduce(  | 
 | 123 | +        (lvl1Result, lvl1Item) => {  | 
 | 124 | +          // lvl1 is sometimes null. Normalise to a string.  | 
 | 125 | +          const lvl1Value = lvl1Item?.hierarchy?.lvl1;  | 
 | 126 | +          const lvl1Key = lvl1Value ? lvl1Value : lvl0Key;  | 
 | 127 | + | 
 | 128 | +          if (!lvl1Result[lvl1Key]) {  | 
 | 129 | +            lvl1Result[lvl1Key] = [];  | 
 | 130 | +          }  | 
 | 131 | + | 
 | 132 | +          lvl1Result[lvl1Key].push(lvl1Item);  | 
 | 133 | +          return lvl1Result;  | 
 | 134 | +        },  | 
 | 135 | +        {},  | 
 | 136 | +      );  | 
 | 137 | + | 
 | 138 | +      return lvl0Result;  | 
 | 139 | +    }, {});  | 
 | 140 | +  }  | 
 | 141 | + | 
 | 142 | +  <template>  | 
 | 143 | +    {{#each-in this.groupedResults as |lvl0section _lvl0results|}}  | 
 | 144 | +      <SearchResultGroupHeader aria-hidden='true'>  | 
 | 145 | +        {{lvl0section}}  | 
 | 146 | +      </SearchResultGroupHeader>  | 
 | 147 | + | 
 | 148 | +      {{#each-in _lvl0results as |lvl1section _lvl1results|}}  | 
 | 149 | +        {{#each _lvl1results as |result index|}}  | 
 | 150 | +          <SearchResult  | 
 | 151 | +            @result={{result}}  | 
 | 152 | +            @groupName={{lvl1section}}  | 
 | 153 | +            @groupPosition={{index}}  | 
 | 154 | +            @closeMenu={{@closeMenu}}  | 
 | 155 | +            data-test-search-result  | 
 | 156 | +          />  | 
 | 157 | +        {{/each}}  | 
 | 158 | +      {{/each-in}}  | 
 | 159 | +    {{/each-in}}  | 
 | 160 | +  </template>  | 
 | 161 | +};  | 
 | 162 | + | 
 | 163 | +export default class ApiSearch extends Component {  | 
 | 164 | +  @service('search') searchService;  | 
 | 165 | +  @service('project') projectService;  | 
 | 166 | + | 
 | 167 | +  <template>  | 
 | 168 | +    <Search @searchService={{this.searchService}}>  | 
 | 169 | +      <:searchInputScreenReaderLabel>Search v{{this.projectService.version}}  | 
 | 170 | +        of the API Docs. Results will update as you type.</:searchInputScreenReaderLabel>  | 
 | 171 | +      <:instructions>Type to search v{{this.projectService.version}}  | 
 | 172 | +        of the API Docs</:instructions>  | 
 | 173 | +      <:results as |results closeMenu|>  | 
 | 174 | +        <SearchResults @results={{results}} @closeMenu={{closeMenu}} />  | 
 | 175 | +      </:results>  | 
 | 176 | +    </Search>  | 
 | 177 | +  </template>  | 
 | 178 | +}  | 
0 commit comments