UNPKG

4.86 kBTypeScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import { InputGroup } from './inputgroup';
5import { ReactWidget } from './vdom';
6import { StringExt } from '@lumino/algorithm';
7import React, { useEffect, useState } from 'react';
8
9/**
10 * The class name added to the filebrowser crumbs node.
11 */
12export interface IFilterBoxProps {
13 /**
14 * Whether to use case-sensitive search
15 */
16 caseSensitive?: boolean;
17
18 /**
19 * Whether the search box is disabled or not.
20 */
21 disabled?: boolean;
22
23 /**
24 * Whether to force a refresh.
25 */
26 forceRefresh?: boolean;
27
28 /**
29 * An optional initial search value.
30 */
31 initialQuery?: string;
32
33 /**
34 * Pass a ref to the input element
35 */
36 inputRef?: React.RefObject<HTMLInputElement>;
37
38 /**
39 * Optional placeholder for the search box.
40 */
41 placeholder?: string;
42
43 /**
44 * A function to callback when filter is updated.
45 */
46 updateFilter: (
47 filterFn: (item: string) => Partial<IScore> | null,
48 query?: string
49 ) => void;
50
51 /**
52 * Whether to use the fuzzy filter.
53 */
54 useFuzzyFilter: boolean;
55}
56
57/**
58 * A text match score with associated content item.
59 */
60export interface IScore {
61 /**
62 * The numerical score for the text match.
63 */
64 score: number;
65
66 /**
67 * The indices of the text matches.
68 */
69 indices: number[] | null;
70}
71
72/**
73 * Perform a fuzzy search on a single item.
74 */
75export function fuzzySearch(source: string, query: string): IScore | null {
76 // Set up the match score and indices array.
77 let score = Infinity;
78 let indices: number[] | null = null;
79
80 // The regex for search word boundaries
81 const rgx = /\b\w/g;
82
83 let continueSearch = true;
84
85 // Search the source by word boundary.
86 while (continueSearch) {
87 // Find the next word boundary in the source.
88 let rgxMatch = rgx.exec(source);
89
90 // Break if there is no more source context.
91 if (!rgxMatch) {
92 break;
93 }
94
95 // Run the string match on the relevant substring.
96 let match = StringExt.matchSumOfDeltas(source, query, rgxMatch.index);
97
98 // Break if there is no match.
99 if (!match) {
100 break;
101 }
102
103 // Update the match if the score is better.
104 if (match && match.score <= score) {
105 score = match.score;
106 indices = match.indices;
107 }
108 }
109
110 // Bail if there was no match.
111 if (!indices || score === Infinity) {
112 return null;
113 }
114
115 // Handle a split match.
116 return {
117 score,
118 indices
119 };
120}
121
122export const updateFilterFunction = (
123 value: string,
124 useFuzzyFilter: boolean,
125 caseSensitive?: boolean
126) => {
127 return (item: string): Partial<IScore> | null => {
128 if (useFuzzyFilter) {
129 // Run the fuzzy search for the item and query.
130 const query = value.toLowerCase();
131 // Ignore the item if it is not a match.
132 return fuzzySearch(item, query);
133 }
134 if (!caseSensitive) {
135 item = item.toLocaleLowerCase();
136 value = value.toLocaleLowerCase();
137 }
138 const i = item.indexOf(value);
139 if (i === -1) {
140 return null;
141 }
142 return {
143 indices: [...Array(item.length).keys()].map(x => x + 1)
144 };
145 };
146};
147
148export const FilterBox = (props: IFilterBoxProps): JSX.Element => {
149 const [filter, setFilter] = useState(props.initialQuery ?? '');
150
151 if (props.forceRefresh) {
152 useEffect(() => {
153 props.updateFilter((item: string) => {
154 return {};
155 });
156 }, []);
157 }
158
159 useEffect(() => {
160 // If there is an initial search value, pass the parent the initial filter function for that value.
161 if (props.initialQuery !== undefined) {
162 props.updateFilter(
163 updateFilterFunction(
164 props.initialQuery,
165 props.useFuzzyFilter,
166 props.caseSensitive
167 ),
168 props.initialQuery
169 );
170 }
171 }, []);
172
173 /**
174 * Handler for search input changes.
175 */
176 const handleChange = (e: React.FormEvent<HTMLElement>) => {
177 const target = e.target as HTMLInputElement;
178 setFilter(target.value);
179 props.updateFilter(
180 updateFilterFunction(
181 target.value,
182 props.useFuzzyFilter,
183 props.caseSensitive
184 ),
185 target.value
186 );
187 };
188
189 return (
190 <InputGroup
191 className="jp-FilterBox"
192 inputRef={props.inputRef}
193 type="text"
194 disabled={props.disabled}
195 rightIcon="ui-components:search"
196 placeholder={props.placeholder}
197 onChange={handleChange}
198 value={filter}
199 />
200 );
201};
202
203/**
204 * A widget which hosts a input textbox to filter on file names.
205 */
206export const FilenameSearcher = (props: IFilterBoxProps): ReactWidget => {
207 return ReactWidget.create(
208 <FilterBox
209 updateFilter={props.updateFilter}
210 useFuzzyFilter={props.useFuzzyFilter}
211 placeholder={props.placeholder}
212 forceRefresh={props.forceRefresh}
213 caseSensitive={props.caseSensitive}
214 />
215 );
216};