diff --git a/example/index.tsx b/example/index.tsx index d138f40..84ba8bb 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -39,6 +39,8 @@ root.render( autoFocus leftIcon={<>🎨} iconBoxSize="48px" + includeMatches={true} + highlightStyle={{ fontWeight: "bolder", backgroundColor: "yellow" }} /> ); diff --git a/src/dropdown/index.tsx b/src/dropdown/index.tsx index f5fef2f..2081773 100644 --- a/src/dropdown/index.tsx +++ b/src/dropdown/index.tsx @@ -1,6 +1,14 @@ -import { FC } from "react"; +import { CSSProperties, FC } from "react"; import { StyledDropdown } from "./styles"; +// the prototype of fuse.js's search result's match +// only work when includeMatches == true +interface Match { + indices: number[][]; + key: string; + value: string; +} + interface IProps { onClick: any; matchedRecords: [ @@ -9,17 +17,99 @@ interface IProps { key: string; value: string; }; + matches?: Match[]; } ]; dropdownHoverColor: string; dropdownBorderColor: string; + highlightStyle?: CSSProperties; } +interface ItemProps { + /* + * the content + */ + value: string; + /* + * the matched places, to be highlighted + */ + matches?: Match[]; + /* + * the highlighted span's style + * if matches is undefined or empty, this won't work + */ + highlightStyle?: CSSProperties; +} + +const DropDownItem: FC = ({ value, matches, highlightStyle }) => { + if (matches === undefined) { + return
value
; + } else { + const parts: JSX.Element[] = []; + let lastIndex = 0; + + let rawIndexes = matches.map((item) => item.indices).flat(); + let indexes = mergeIntervals(rawIndexes); + indexes.forEach((arr, index) => { + let start = arr[0]; + let end = arr[1]; + // Add non-highlighted text before the current highlighted text + if (start > lastIndex) { + parts.push( + + {value.substring(lastIndex, start)} + + ); + } + // Add highlighted text + parts.push( + + {value.substring(start, end + 1)} + + ); + lastIndex = end + 1; + }); + + // Add any remaining non-highlighted text after the last highlighted section + if (lastIndex < value.length) { + parts.push({value.substring(lastIndex)}); + } + + return
{parts}
; + } +}; + +// merge intervals of spans +const mergeIntervals = (intervals: number[][]): number[][] => { + if (!intervals.length) return []; + + // Sort intervals by their start values + intervals.sort((a, b) => a[0] - b[0]); + + const merged: number[][] = [intervals[0]]; + + for (let i = 1; i < intervals.length; i++) { + const prev = merged[merged.length - 1]; + const current = intervals[i]; + + if (current[0] <= prev[1] + 1) { + // Check if current interval overlaps or is consecutive + // Merge intervals by updating the end value of the previous interval + prev[1] = Math.max(prev[1], current[1]); + } else { + merged.push(current); + } + } + + return merged; +}; + const Dropdown: FC = ({ onClick, matchedRecords = [], dropdownBorderColor, dropdownHoverColor, + highlightStyle, }) => { return ( = ({ className="react-search-box-dropdown-list-item" onClick={() => onClick(record)} > - {record.item.value} + ); })} diff --git a/src/index.tsx b/src/index.tsx index 4362952..57c5424 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,6 @@ import Fuse from "fuse.js"; import React, { + CSSProperties, ChangeEvent, FC, KeyboardEvent, @@ -102,6 +103,17 @@ interface IProps { * The type of the input. */ type?: string; + + /* + * If return the matched ranges + */ + includeMatches?: boolean; + + /* + * the highlighted span's style + * only works if the includeMatches is true + */ + highlightStyle?: CSSProperties; } const ReactSearchBox: FC = ({ @@ -124,6 +136,8 @@ const ReactSearchBox: FC = ({ leftIcon, iconBoxSize = "24px", type = "text", + includeMatches = false, + highlightStyle, }) => { const [matchedRecords, setMatchedRecords] = useState([]); const [value, setValue] = useState(""); @@ -137,6 +151,7 @@ const ReactSearchBox: FC = ({ * for more details. */ const defaultFuseConfigs = { + includeMatches: includeMatches, /** * At what point does the match algorithm give up. A threshold of 0.0 * requires a perfect match (of both letters and location), a threshold @@ -302,6 +317,7 @@ const ReactSearchBox: FC = ({ onClick={handleDropdownItemClick} dropdownHoverColor={dropdownHoverColor} dropdownBorderColor={dropdownBorderColor} + highlightStyle={highlightStyle} /> ); };