فهرست منبع

feat(docs): Group search results by section

Michael Bromley 7 سال پیش
والد
کامیت
cae3602a19
5فایلهای تغییر یافته به همراه74 افزوده شده و 19 حذف شده
  1. 2 2
      .gitignore
  2. 56 11
      docs/assets/scripts/search-widget.ts
  3. 11 2
      docs/assets/styles/_search-widget.scss
  4. 4 3
      docs/layouts/searchindex/searchindex.html
  5. 1 1
      docs/webpack.config.ts

+ 2 - 2
.gitignore

@@ -394,8 +394,8 @@ server/dist
 server/test-emails
 server/src/email/preview/output
 docs/resources/_gen/*
-docs/static/main.js
-docs/static/main.css
+docs/static/main.js*
+docs/static/main.css*
 docs/public
 docs/content/docs/configuration/*
 !docs/content/docs/configuration/_index.md

+ 56 - 11
docs/assets/scripts/search-widget.ts

@@ -1,12 +1,15 @@
 import fuzzy, { FilterResult } from 'fuzzy';
 
+type Section = 'guides' | 'config' | 'gql';
 interface IndexItem {
+    section: Section;
     title: string;
     headings: string[];
     url: string;
 }
 
 interface DenormalizedItem {
+    section: Section;
     title: string;
     heading: string;
     url: string;
@@ -24,7 +27,7 @@ const KeyCode = {
  */
 export class SearchWidget {
 
-    private readonly MAX_RESULTS = 8;
+    private readonly MAX_RESULTS = 7;
     private searchIndex: Promise<DenormalizedItem[]> | undefined;
     private results: Array<FilterResult<DenormalizedItem>> = [];
     private selectedIndex = -1;
@@ -53,7 +56,7 @@ export class SearchWidget {
                     const selected = this.autocompleteDiv.querySelector('li.selected a') as HTMLAnchorElement;
                     if (selected) {
                         selected.click();
-                        return;
+                        this.results = [];
                     }
                     break;
                 case KeyCode.ESCAPE:
@@ -63,6 +66,11 @@ export class SearchWidget {
             }
             this.render();
         });
+
+        this.wrapperDiv.addEventListener('click', () => {
+            this.results = [];
+            this.render();
+        });
     }
 
     toggleActive() {
@@ -72,15 +80,30 @@ export class SearchWidget {
         }
     }
 
+    /**
+     * Groups the results by section and renders as a list
+     */
     private render() {
-        const listItems = this.results
-            .map((result, i) => {
-                const { title, heading, url } = result.original;
+        const sections: Section[] = ['guides', 'gql', 'config'];
+        let html = '';
+        let i = 0;
+        for (const sec of sections) {
+            const matches = this.results.filter(r => r.original.section === sec);
+            if (matches.length) {
+                const sectionName = sec === 'guides' ? 'Guides' : sec === 'gql' ? 'GraphQL API' : 'Configuration';
+                html += `<li class="section">${sectionName}</li>`;
+            }
+            html += matches.map((result) => {
+                const { section, title, heading, url } = result.original;
                 const anchor = heading !== title ? '#' + heading.toLowerCase().replace(/\s/g, '-') : '';
                 const inner = `<div class="title">${title}</div><div class="heading">${result.string}</div>`;
-                return `<li class="${i === this.selectedIndex ? 'selected' : ''}"><a href="${url + anchor}">${inner}</a></li>`;
-            });
-        this.listElement.innerHTML = listItems.join('\n');
+                const selected = i === this.selectedIndex ? 'selected' : '';
+                i++;
+                return `<li class="${selected}"><a href="${url + anchor}">${inner}</a></li>`;
+            }).join('\n');
+        }
+
+        this.listElement.innerHTML = html;
     }
 
     private attachAutocomplete() {
@@ -108,7 +131,7 @@ export class SearchWidget {
 
     private async getResults(term: string) {
         const items = await this.getSearchIndex();
-        return fuzzy.filter(
+        const results = fuzzy.filter(
             term,
             items,
             {
@@ -118,7 +141,27 @@ export class SearchWidget {
                     return input.heading;
                 },
             },
-        ).slice(0, this.MAX_RESULTS);
+        );
+
+        if (this.MAX_RESULTS < results.length) {
+            // limit the maximum number of results from a particular
+            // section to prevent other possibly relevant results getting
+            // buried.
+            const guides = results.filter(r => r.original.section === 'guides');
+            const gql = results.filter(r => r.original.section === 'gql');
+            const config = results.filter(r => r.original.section === 'config');
+            let pool = [guides, gql, config].filter(p => p.length);
+            const balancedResults = [];
+            for (let i = 0; i < this.MAX_RESULTS; i ++) {
+                const next = pool[i % pool.length].shift();
+                if (next) {
+                    balancedResults.push(next);
+                }
+                pool = [guides, gql, config].filter(p => p.length);
+            }
+            return balancedResults;
+        }
+        return results;
     }
 
     private getSearchIndex(): Promise<DenormalizedItem[]> {
@@ -129,8 +172,9 @@ export class SearchWidget {
                 .then(res => eval(res))
                 .then((items: IndexItem[]) => {
                     const denormalized: DenormalizedItem[] = [];
-                    for (const { title, headings, url } of items) {
+                    for (const { section, title, headings, url } of items) {
                         denormalized.push({
+                            section,
                             title,
                             heading: title,
                             url,
@@ -138,6 +182,7 @@ export class SearchWidget {
                         if (headings.length) {
                             for (const heading of headings) {
                                 denormalized.push({
+                                    section,
                                     title,
                                     heading,
                                     url,

+ 11 - 2
docs/assets/styles/_search-widget.scss

@@ -28,7 +28,8 @@
 
 .autocomplete {
     position: absolute;
-    background-color: $gray-100;
+    background-color: white;
+    box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.52);
     border-radius: 4px;
     top: 35px;
     right: 0;
@@ -49,8 +50,16 @@
         padding: 12px;
         background-color: transparent;
         transition: background-color 0.2s;
+        &:hover {
+            background-color: $gray-200;
+        }
+        &.section {
+            background-color: $gray-100;
+            color: $brand-color;
+            font-size: 12px;
+        }
         &.selected {
-            background-color: $gray-300;
+            background-color: $gray-200;
         }
         a {
             display: flex;

+ 4 - 3
docs/layouts/searchindex/searchindex.html

@@ -1,11 +1,12 @@
 {
 result :[
-    {{ range .Site.Pages -}}
+    {{- range .Site.Pages -}}
         {{ $titles := findRE "<h(2|3).*?>(.|\n)*?</h(2|3)>" .Content -}}
         {{ $plain := apply $titles "plainify" "." -}}
         {{ $cleaned := $plain | complement (slice "constructor" "Signature" "Members") -}}
-        {{ $quoted := apply $cleaned "replaceRE" ".*" "'$0'" "." }}
-        { title: '{{ .Title }}', headings: [{{ delimit $quoted ", " }}], url: '{{ .RelPermalink }}' },
+        {{ $quoted := apply $cleaned "replaceRE" ".*" "'$0'" "." -}}
+        {{ $section := cond (in .Dir "configuration") "config" (cond (in .Dir "graphql-api") "gql" "guides") }}
+        { section: '{{ $section }}', title: '{{ .Title }}', headings: [{{ delimit $quoted ", " }}], url: '{{ .RelPermalink }}' },
     {{- end }}
 ]
 }

+ 1 - 1
docs/webpack.config.ts

@@ -37,7 +37,7 @@ const config: webpack.Configuration = {
             chunkFilename: '[id].css',
         }),
     ],
-    devtool: 'inline-source-map',
+    devtool: 'source-map',
 };
 
 export default config;