import { observer } from "mobx-react"; import Mustache from "mustache"; import { FC, MouseEventHandler, useCallback, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useVirtual } from "react-virtual"; import styled from "styled-components"; import SearchableItemMixin from "../../../ModelMixins/SearchableItemMixin"; import { ItemSearchResult } from "../../../Models/ItemSearchProviders/ItemSearchProvider"; import Box from "../../../Styled/Box"; import Button from "../../../Styled/Button"; import parseCustomMarkdownToReact from "../../Custom/parseCustomMarkdownToReact"; import MapEffects, { MapEffect } from "./MapEffects"; export interface SearchResultsProps { item: SearchableItemMixin.Instance; results: ItemSearchResult[]; template?: string; } type ResultClickHandler = (result: ItemSearchResult) => void; const SearchResults: FC = (props) => { const { item, results } = props; const [currentMapEffect, setCurrentMapEffect] = useState({ is: "highlightAll" }); const selectedResult = currentMapEffect.is === "highlightSingleResult" ? currentMapEffect.result : undefined; const parentRef = useRef(null); const list = useVirtual({ size: results.length, parentRef, estimateSize: useCallback(() => 50, []) }); const [t] = useTranslation(); const toggleSelection = (newSelection: MapEffect) => { if ( currentMapEffect.is === newSelection.is && (currentMapEffect as any).result === (newSelection as any).result ) { setCurrentMapEffect({ is: "none" }); } else { setCurrentMapEffect(newSelection); } }; return ( toggleSelection({ is: "highlightAll" })} > {t("itemSearchTool.actions.highlightAll")} toggleSelection({ is: "showMatchingOnly" })} > {t("itemSearchTool.actions.showMatchingOnly")} {list.virtualItems.map(({ index, ...row }) => ( toggleSelection({ is: "highlightSingleResult", result: results[index] }) } template={props.template} style={{ position: "absolute", top: 0, left: 0, width: "100%", height: `${row.size}px`, transform: `translateY(${row.start}px)` }} /> ))} ); }; type ResultProps = { result: ItemSearchResult; isSelected: boolean; isEven: boolean; onClick: ResultClickHandler; template?: string; style: any; }; export const Result: FC = observer((props) => { const { result, template, isEven, isSelected, style } = props; const content = template ? parseCustomMarkdownToReact(Mustache.render(template, result.properties)) : result.id; const onClick: MouseEventHandler = (e) => { try { props.onClick(result); } finally { e.preventDefault(); } }; return ( {content} ); }); const ClickableItem = styled.a<{ isEven: boolean; isSelected: boolean }>` display: block; box-sizing: border-box; padding: 5px 10px; cursor: pointer; ${(p) => `background-color: ${ p.isSelected ? p.theme.toolPrimaryColor : p.isEven ? p.theme.dark : p.theme.darkLighter };`} `; const List = styled.div<{ height: string }>` ${(p) => `height: ${p.height}`}; width: 100%; overflow: auto; `; const ListInner = styled.div<{ height: string }>` ${(p) => `height: ${p.height}`}; width: 100%; position: relative; `; const Wrapper = styled(Box).attrs({ column: true, flex: 1 })` > :only-child { flex: 1; align-self: center; align-items: center; } `; export const ResultsCount: FC<{ count: number }> = ({ count }) => { const [t] = useTranslation(); return ( {t(`itemSearchTool.resultsCount`, { count })} ); }; const ActionButton = styled(Button).attrs((props: { selected: boolean }) => ({ primary: props.selected, secondary: !props.selected, textProps: { medium: true } }))<{ selected: boolean }>` min-height: 20px; padding: 1em; padding-top: 2px; padding-bottom: 2px; border: 0px; border-radius: 5px; `; const ActionMenu = styled.div` display: flex; justify-content: flex-end; padding: 0.5em; background-color: ${(p) => p.theme.charcoalGrey}; border-top-left-radius: 5px; border-top-right-radius: 5px; > ${ActionButton}:first-child { margin-right: 1em; } `; export default SearchResults;