UNPKG

@jupyterlab/ui-components

Version:

JupyterLab - UI components written in React

138 lines 5.37 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { ReactWidget } from './vdom'; import { StringExt } from '@lumino/algorithm'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Search } from '@jupyter/react-components'; import { searchIcon } from '../icon'; /** * Perform a fuzzy search on a single item. */ export function fuzzySearch(source, query) { // Set up the match score and indices array. let score = Infinity; let indices = null; // Look for letters (including in Asian scripts), numbers, and diacritical marks. const rgx = /[\p{L}\p{N}\p{M}]+/gu; let continueSearch = true; // Search the source by word boundary. while (continueSearch) { // Find the next word boundary in the source. let rgxMatch = rgx.exec(source); // Break if there is no more source context. if (!rgxMatch) { break; } // Run the string match on the relevant substring. let match = StringExt.matchSumOfDeltas(source, query, rgxMatch.index); // Break if there is no match. if (!match) { break; } // Update the match if the score is better. if (match && match.score <= score) { score = match.score; indices = match.indices; } } // Bail if there was no match. if (!indices || score === Infinity) { return null; } // Handle a split match. return { score, indices }; } export const updateFilterFunction = (value, useFuzzyFilter, caseSensitive) => { return (item) => { if (useFuzzyFilter) { // Run the fuzzy search for the item and query. const query = value.toLowerCase(); // Ignore the item if it is not a match. return fuzzySearch(item, query); } if (!caseSensitive) { item = item.toLocaleLowerCase(); value = value.toLocaleLowerCase(); } const i = item.indexOf(value); if (i === -1) { return null; } return { indices: [...Array(value.length).keys()].map(x => x + i) }; }; }; export const FilterBox = (props) => { var _a, _b, _c; const [filter, setFilter] = useState((_a = props.initialQuery) !== null && _a !== void 0 ? _a : ''); if (props.forceRefresh) { useEffect(() => { props.updateFilter((item) => { return {}; }); }, []); } const firstRender = useRef(true); const inputRef = (_b = props.inputRef) !== null && _b !== void 0 ? _b : useRef(); useEffect(() => { // If there is an initial search value, pass the parent the initial filter function for that value. if (firstRender.current) { firstRender.current = false; if (props.initialQuery !== undefined) { props.updateFilter(updateFilterFunction(props.initialQuery, props.useFuzzyFilter, props.caseSensitive), props.initialQuery); } } else { if (inputRef.current) { // If filter settings changed, update the filter props.updateFilter(updateFilterFunction(inputRef.current.value, props.useFuzzyFilter, props.caseSensitive), inputRef.current.value); } } }, [props.updateFilter, props.useFuzzyFilter, props.caseSensitive]); /** * Handler for search input changes. */ const handleChange = useCallback((e) => { const target = e.target; setFilter(target.value); props.updateFilter(updateFilterFunction(target.value, props.useFuzzyFilter, props.caseSensitive), target.value); }, [props.updateFilter, props.useFuzzyFilter, props.caseSensitive]); // Show the icon by default, or if the caller specifically requests it const showSearchIcon = (_c = props.showIcon) !== null && _c !== void 0 ? _c : true; // Cast typing is required because HTMLInputElement and JupyterSearch element types don't exactly match return (React.createElement(Search, { role: "search", className: "jp-FilterBox", ref: props.inputRef, value: filter, onChange: handleChange, onInput: handleChange, placeholder: props.placeholder, disabled: props.disabled }, showSearchIcon && React.createElement(searchIcon.react, { slot: "end", tag: null }))); }; /** * A widget which hosts a input textbox to filter on file names. */ class FilenameSearcherWidget extends ReactWidget { constructor(props) { var _a; super(); this._filterBoxProps = { ...props }; (_a = props === null || props === void 0 ? void 0 : props.filterSettingsChanged) === null || _a === void 0 ? void 0 : _a.connect((_, args) => { this._updateProps(args); }, this); } render() { return React.createElement(FilterBox, { ...this._filterBoxProps }); } /** * Update a specific prop. */ _updateProps(props) { Object.assign(this._filterBoxProps, props); this.update(); } } /** * Function which returns a widget that hosts an input textbox to filter on file names. */ export const FilenameSearcher = (props) => { return new FilenameSearcherWidget(props); }; //# sourceMappingURL=search.js.map