Skip to content

Commit

Permalink
add fuzzy search
Browse files Browse the repository at this point in the history
  • Loading branch information
greggman committed Dec 12, 2023
1 parent 3aa5732 commit 8e116b1
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 24 deletions.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"base64-arraybuffer": "^1.0.2",
"codemirror": "^5.65.1",
"mime-types": "^2.1.34",
"minisearch": "^6.2.0",
"react": "^17.0.2",
"react-codemirror2": "^7.2.1",
"react-dom": "^17.0.2",
Expand Down
80 changes: 56 additions & 24 deletions src/components/LoadGist.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import MiniSearch from 'minisearch'
import EditLine from './EditLine.js';
import {classNames} from '../libs/css-utils.js';
import * as gists from '../libs/gists.js';
Expand All @@ -16,20 +17,29 @@ type Gist = {

interface GistIdMap {
[key: string]: Gist;
}
}

interface ScoredGist extends Gist {
score: number,
id: string,
};

type LoadGistState = {
loading: boolean,
gists: GistIdMap,
checks: Set<string>,
filter: string, // TODO: move up
newFilter: boolean,
sortKey: string, // TODO: move up
sortDir: string, // TODO: move up
shift: boolean,
index: any,
};

function getSortFn(sortKey: string, checks: Set<string>): (a: Gist, b: Gist) => number {
function getSortFn(sortKey: string, checks: Set<string>): (a: ScoredGist, b: ScoredGist) => number {
switch (sortKey) {
case 'score':
return (a: ScoredGist, b: ScoredGist) => Math.sign(a.score - b.score);
case 'name':
return (a: Gist, b: Gist) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : 0);
case 'date':
Expand All @@ -51,24 +61,32 @@ function getSortFn(sortKey: string, checks: Set<string>): (a: Gist, b: Gist) =>
}
}

function gistsToSortedArray(gists: GistIdMap, checks: Set<string>, sortKey: string, sortDir: string) {
function scoredGistsToSortedArray(gists: ScoredGist[], checks: Set<string>, sortKey: string, sortDir: string) {
const compareDirMult = sortDir === 'down' ? 1 : -1;
const compFn = getSortFn(sortKey, checks);
return Object.entries(gists).map(([id, {name, date, public: _public}]) => {
return {id, name, date, public: _public};
}).sort((b, a) => compFn(a, b) * compareDirMult);
return gists.slice().sort((b, a) => compFn(a, b) * compareDirMult);
}

function matchFilter(filter: string) {
filter = filter.trim().toLowerCase();
return function(gist: Gist) {
const {name, date} = gist;
return filter === '' ||
name.toLowerCase().includes(filter) ||
date.substring(0, 10).includes(filter);
}
function createIndex(gists: GistIdMap) {
const miniSearch = new MiniSearch({
fields: ['name'], // fields to index for full-text search
});
miniSearch.addAll(Object.entries(gists).map(([id, {name}]) => ({id, name})));
return miniSearch;
}

function matchingGists(index: MiniSearch, filter: string, gists: GistIdMap): ScoredGist[] {
filter = filter.trim();
if (filter === '') {
return Object.entries(gists)
.map(([id, gist]) => ({...gist, id, score: 0}))
}
const results = new Map(index.search(filter, { prefix: true, fuzzy: 0.2 }).map(r => [r.id, r.score]));
return Object.entries(gists)
.filter(([id]) => results.has(id))
.map(([id, gist]) => ({...gist, id, score: results.get(id)! }))

}

type SortKeyInfo = {
sortDir: string,
Expand Down Expand Up @@ -106,14 +124,17 @@ export default class LoadGist extends React.Component<{}, LoadGistState> {
gists: _gists,
checks: new Set(),
filter: '',
newFilter: false,
sortKey: 'date',
sortDir: 'down',
shift: false,
index: createIndex(_gists),
};
}
handleNewGists = (gists: GistIdMap) => {
this.setState({
gists,
index: createIndex(gists),
});
}
toggleCheck = (id: string) => {
Expand All @@ -133,8 +154,7 @@ export default class LoadGist extends React.Component<{}, LoadGistState> {
}
}
updateSort = (sortKey: string, sortDir: string) => {
console.log('update:', sortKey, sortDir);
this.setState({sortDir, sortKey});
this.setState({sortDir, sortKey, newFilter: false});
}
componentDidMount() {
const {userManager} = this.context;
Expand Down Expand Up @@ -219,10 +239,17 @@ export default class LoadGist extends React.Component<{}, LoadGistState> {
}
renderLoad() {
const {userManager} = this.context;
const {gists, checks, loading, filter, sortKey, sortDir, shift} = this.state;
const {gists, checks, loading, index, filter, sortKey, sortDir, shift, newFilter} = this.state;
const userData = userManager.getUserData();
const canLoad = !!userData && !loading;
const gistArray = gistsToSortedArray(gists, checks, sortKey, sortDir);
const effectiveSortKey = newFilter ? 'score' : sortKey;
const effectiveSortDir = newFilter ? 'down' : sortDir;
const gistArray = scoredGistsToSortedArray(
matchingGists(index, filter, gists),
checks,
effectiveSortKey,
effectiveSortDir);

return (
<div>
<p>
Expand All @@ -235,21 +262,26 @@ export default class LoadGist extends React.Component<{}, LoadGistState> {
gistArray.length >= 0 &&
<React.Fragment>
<p>
<EditLine className="foobar" placeholder="search:" value={filter} onChange={(filter:string) => {this.setState({filter})}} />
<EditLine className="foobar" placeholder="search:" value={filter} onChange={
(filter:string) => {
this.setState({filter, newFilter: filter.trim() !== ''});
}
}
/>
</p>
<div className="gists">
<table>
<thead>
<tr>
<th><SortBy selected={sortKey === 'check'} sortDir={sortDir} update={(dir: string) => this.updateSort('check', dir)}/></th>
<th><SortBy selected={sortKey === 'name'} sortDir={sortDir} update={(dir: string) => this.updateSort('name', dir)}/></th>
<th><SortBy selected={sortKey === 'date'} sortDir={sortDir} update={(dir: string) => this.updateSort('date', dir)}/></th>
<th><SortBy selected={sortKey === 'public'} sortDir={sortDir} update={(dir: string) => this.updateSort('public', dir)}/></th>
<th><SortBy selected={effectiveSortKey === 'check'} sortDir={sortDir} update={(dir: string) => this.updateSort('check', dir)}/></th>
<th><SortBy selected={effectiveSortKey === 'name'} sortDir={sortDir} update={(dir: string) => this.updateSort('name', dir)}/></th>
<th><SortBy selected={effectiveSortKey === 'date'} sortDir={sortDir} update={(dir: string) => this.updateSort('date', dir)}/></th>
<th><SortBy selected={effectiveSortKey === 'public'} sortDir={sortDir} update={(dir: string) => this.updateSort('public', dir)}/></th>
</tr>
</thead>
<tbody>
{
gistArray.filter(matchFilter(filter)).map((gist, ndx) => {
gistArray.map((gist, ndx) => {
return (
<tr key={`g${ndx}`}>
<td><input type="checkbox" id={`gc${ndx}`} checked={checks.has(gist.id)} onChange={() => this.toggleCheck(gist.id)}/><label htmlFor={`gc${ndx}`}/></td>
Expand Down

0 comments on commit 8e116b1

Please sign in to comment.