/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import styled from '@emotion/styled'; import React, { useEffect, memo, useState, useRef, createContext, useContext, } from 'react'; import {debounce} from 'lodash'; const Highlighted = styled.span({ backgroundColor: '#fcd872', }); export interface HighlightManager { setFilter(text: string | undefined): void; render(text: string): React.ReactNode; } function createHighlightManager(initialText: string = ''): HighlightManager { const callbacks = new Set<(prev: string, next: string) => void>(); let matches = 0; let currentFilter = initialText; const Highlight: React.FC<{text: string}> = memo(({text}) => { const [, setUpdate] = useState(0); const elem = useRef(null); useEffect(() => { function onChange(prevHighlight: string, newHighlight: string) { const prevIndex = text.toLowerCase().indexOf(prevHighlight); const newIndex = text.toLowerCase().indexOf(newHighlight); if (prevIndex !== newIndex || newIndex !== -1) { // either we had a result, and we have no longer, // or we still have a result, but the highlightable text changed if (newIndex !== -1) { if (++matches === 1) { elem.current?.parentElement?.parentElement?.scrollIntoView?.(); } } setUpdate((s) => s + 1); } } callbacks.add(onChange); return () => { callbacks.delete(onChange); }; }, [text]); const index = text.toLowerCase().indexOf(currentFilter); return ( {index === -1 ? ( text ) : ( <> {text.substr(0, index)} {text.substr(index, currentFilter.length)} {text.substr(index + currentFilter.length)} )} ); }); return { setFilter: debounce((text: string = '') => { if (currentFilter !== text) { matches = 0; const base = currentFilter; currentFilter = text.toLowerCase(); callbacks.forEach((cb) => cb(base, currentFilter)); } }, 100), render(text: string) { return ; }, }; } export const HighlightContext = createContext({ setFilter(_text: string) { throw new Error('Cannot set the filter of a stub highlight manager'); }, render(text: string) { // stub implementation in case we render a component without a Highlight context return text; }, }); export function HighlightProvider({ text, children, }: { text: string | undefined; children: React.ReactElement; }) { const [highlightManager] = useState(() => createHighlightManager(text)); useEffect(() => { highlightManager.setFilter(text); }, [text, highlightManager]); return ( {children} ); } export function useHighlighter(): HighlightManager { return useContext(HighlightContext); }