11import React from 'react' ;
2+ import MiniSearch from 'minisearch'
23import EditLine from './EditLine.js' ;
34import { classNames } from '../libs/css-utils.js' ;
45import * as gists from '../libs/gists.js' ;
@@ -16,20 +17,29 @@ type Gist = {
1617
1718interface GistIdMap {
1819 [ key : string ] : Gist ;
19- }
20+ }
21+
22+ interface ScoredGist extends Gist {
23+ score : number ,
24+ id : string ,
25+ } ;
2026
2127type LoadGistState = {
2228 loading : boolean ,
2329 gists : GistIdMap ,
2430 checks : Set < string > ,
2531 filter : string , // TODO: move up
32+ newFilter : boolean ,
2633 sortKey : string , // TODO: move up
2734 sortDir : string , // TODO: move up
2835 shift : boolean ,
36+ index : any ,
2937} ;
3038
31- function getSortFn ( sortKey : string , checks : Set < string > ) : ( a : Gist , b : Gist ) => number {
39+ function getSortFn ( sortKey : string , checks : Set < string > ) : ( a : ScoredGist , b : ScoredGist ) => number {
3240 switch ( sortKey ) {
41+ case 'score' :
42+ return ( a : ScoredGist , b : ScoredGist ) => Math . sign ( a . score - b . score ) ;
3343 case 'name' :
3444 return ( a : Gist , b : Gist ) => a . name . toLowerCase ( ) < b . name . toLowerCase ( ) ? - 1 : ( a . name . toLowerCase ( ) > b . name . toLowerCase ( ) ? 1 : 0 ) ;
3545 case 'date' :
@@ -51,24 +61,32 @@ function getSortFn(sortKey: string, checks: Set<string>): (a: Gist, b: Gist) =>
5161 }
5262}
5363
54- function gistsToSortedArray ( gists : GistIdMap , checks : Set < string > , sortKey : string , sortDir : string ) {
64+ function scoredGistsToSortedArray ( gists : ScoredGist [ ] , checks : Set < string > , sortKey : string , sortDir : string ) {
5565 const compareDirMult = sortDir === 'down' ? 1 : - 1 ;
5666 const compFn = getSortFn ( sortKey , checks ) ;
57- return Object . entries ( gists ) . map ( ( [ id , { name, date, public : _public } ] ) => {
58- return { id, name, date, public : _public } ;
59- } ) . sort ( ( b , a ) => compFn ( a , b ) * compareDirMult ) ;
67+ return gists . slice ( ) . sort ( ( b , a ) => compFn ( a , b ) * compareDirMult ) ;
6068}
6169
62- function matchFilter ( filter : string ) {
63- filter = filter . trim ( ) . toLowerCase ( ) ;
64- return function ( gist : Gist ) {
65- const { name, date} = gist ;
66- return filter === '' ||
67- name . toLowerCase ( ) . includes ( filter ) ||
68- date . substring ( 0 , 10 ) . includes ( filter ) ;
69- }
70+ function createIndex ( gists : GistIdMap ) {
71+ const miniSearch = new MiniSearch ( {
72+ fields : [ 'name' ] , // fields to index for full-text search
73+ } ) ;
74+ miniSearch . addAll ( Object . entries ( gists ) . map ( ( [ id , { name} ] ) => ( { id, name} ) ) ) ;
75+ return miniSearch ;
7076}
7177
78+ function matchingGists ( index : MiniSearch , filter : string , gists : GistIdMap ) : ScoredGist [ ] {
79+ filter = filter . trim ( ) ;
80+ if ( filter === '' ) {
81+ return Object . entries ( gists )
82+ . map ( ( [ id , gist ] ) => ( { ...gist , id, score : 0 } ) )
83+ }
84+ const results = new Map ( index . search ( filter , { prefix : true , fuzzy : 0.2 } ) . map ( r => [ r . id , r . score ] ) ) ;
85+ return Object . entries ( gists )
86+ . filter ( ( [ id ] ) => results . has ( id ) )
87+ . map ( ( [ id , gist ] ) => ( { ...gist , id, score : results . get ( id ) ! } ) )
88+
89+ }
7290
7391type SortKeyInfo = {
7492 sortDir : string ,
@@ -106,14 +124,17 @@ export default class LoadGist extends React.Component<{}, LoadGistState> {
106124 gists : _gists ,
107125 checks : new Set ( ) ,
108126 filter : '' ,
127+ newFilter : false ,
109128 sortKey : 'date' ,
110129 sortDir : 'down' ,
111130 shift : false ,
131+ index : createIndex ( _gists ) ,
112132 } ;
113133 }
114134 handleNewGists = ( gists : GistIdMap ) => {
115135 this . setState ( {
116136 gists,
137+ index : createIndex ( gists ) ,
117138 } ) ;
118139 }
119140 toggleCheck = ( id : string ) => {
@@ -133,8 +154,7 @@ export default class LoadGist extends React.Component<{}, LoadGistState> {
133154 }
134155 }
135156 updateSort = ( sortKey : string , sortDir : string ) => {
136- console . log ( 'update:' , sortKey , sortDir ) ;
137- this . setState ( { sortDir, sortKey} ) ;
157+ this . setState ( { sortDir, sortKey, newFilter : false } ) ;
138158 }
139159 componentDidMount ( ) {
140160 const { userManager} = this . context ;
@@ -219,10 +239,17 @@ export default class LoadGist extends React.Component<{}, LoadGistState> {
219239 }
220240 renderLoad ( ) {
221241 const { userManager} = this . context ;
222- const { gists, checks, loading, filter, sortKey, sortDir, shift} = this . state ;
242+ const { gists, checks, loading, index , filter, sortKey, sortDir, shift, newFilter } = this . state ;
223243 const userData = userManager . getUserData ( ) ;
224244 const canLoad = ! ! userData && ! loading ;
225- const gistArray = gistsToSortedArray ( gists , checks , sortKey , sortDir ) ;
245+ const effectiveSortKey = newFilter ? 'score' : sortKey ;
246+ const effectiveSortDir = newFilter ? 'down' : sortDir ;
247+ const gistArray = scoredGistsToSortedArray (
248+ matchingGists ( index , filter , gists ) ,
249+ checks ,
250+ effectiveSortKey ,
251+ effectiveSortDir ) ;
252+
226253 return (
227254 < div >
228255 < p >
@@ -235,21 +262,26 @@ export default class LoadGist extends React.Component<{}, LoadGistState> {
235262 gistArray . length >= 0 &&
236263 < React . Fragment >
237264 < p >
238- < EditLine className = "foobar" placeholder = "search:" value = { filter } onChange = { ( filter :string ) => { this . setState ( { filter} ) } } />
265+ < EditLine className = "foobar" placeholder = "search:" value = { filter } onChange = {
266+ ( filter :string ) => {
267+ this . setState ( { filter, newFilter : filter . trim ( ) !== '' } ) ;
268+ }
269+ }
270+ />
239271 </ p >
240272 < div className = "gists" >
241273 < table >
242274 < thead >
243275 < tr >
244- < th > < SortBy selected = { sortKey === 'check' } sortDir = { sortDir } update = { ( dir : string ) => this . updateSort ( 'check' , dir ) } /> </ th >
245- < th > < SortBy selected = { sortKey === 'name' } sortDir = { sortDir } update = { ( dir : string ) => this . updateSort ( 'name' , dir ) } /> </ th >
246- < th > < SortBy selected = { sortKey === 'date' } sortDir = { sortDir } update = { ( dir : string ) => this . updateSort ( 'date' , dir ) } /> </ th >
247- < th > < SortBy selected = { sortKey === 'public' } sortDir = { sortDir } update = { ( dir : string ) => this . updateSort ( 'public' , dir ) } /> </ th >
276+ < th > < SortBy selected = { effectiveSortKey === 'check' } sortDir = { sortDir } update = { ( dir : string ) => this . updateSort ( 'check' , dir ) } /> </ th >
277+ < th > < SortBy selected = { effectiveSortKey === 'name' } sortDir = { sortDir } update = { ( dir : string ) => this . updateSort ( 'name' , dir ) } /> </ th >
278+ < th > < SortBy selected = { effectiveSortKey === 'date' } sortDir = { sortDir } update = { ( dir : string ) => this . updateSort ( 'date' , dir ) } /> </ th >
279+ < th > < SortBy selected = { effectiveSortKey === 'public' } sortDir = { sortDir } update = { ( dir : string ) => this . updateSort ( 'public' , dir ) } /> </ th >
248280 </ tr >
249281 </ thead >
250282 < tbody >
251283 {
252- gistArray . filter ( matchFilter ( filter ) ) . map ( ( gist , ndx ) => {
284+ gistArray . map ( ( gist , ndx ) => {
253285 return (
254286 < tr key = { `g${ ndx } ` } >
255287 < td > < input type = "checkbox" id = { `gc${ ndx } ` } checked = { checks . has ( gist . id ) } onChange = { ( ) => this . toggleCheck ( gist . id ) } /> < label htmlFor = { `gc${ ndx } ` } /> </ td >
0 commit comments