import { chunk } from 'lodash'; import React from 'react'; import { areEqual, ListChildComponentProps, VariableSizeList as List, } from 'react-window'; import styled from 'styled-components'; import useResizeObserver from 'use-resize-observer'; import { CategoryRow } from './CategoryRow'; import { ICON_SPACE, ICON_WIDTH } from '../lib/constants'; import { IconListEmpty } from './IconListEmpty'; import { IconsRow } from './IconsRow'; import { ReactWindowScroller } from './ReactWindowScroller'; export interface IconListFilters { search?: string; } export interface IconListCustomizations { size: number; strokeWidth: number; hexColor: string; } export interface Icon { filename: string; category: string; tags: string[]; iconComponentName: string; } export const DEFAULT_CUSTOMIZATIONS: IconListCustomizations = { size: 24, strokeWidth: 1.5, hexColor: '#000000', }; function normalizeString(s: string) { return s.toLowerCase().replace(/[!@#$%^&*(),.\][-]/g, ''); } function filterIcons(allIcons: Icon[], filters: IconListFilters): Icon[] { if (filters.search) { const normalSearch = normalizeString(filters.search!); let result = allIcons; for (const term of normalSearch.split(' ')) { result = result.filter((icon) => { return ( normalizeString(icon.filename).includes(term) || normalizeString(icon.category).includes(term) || icon.tags.some((tag) => normalizeString(tag).includes(term)) ); }); } return result; } else return allIcons; } interface IconCategoryRow { category: string; numIcons: number; } interface IconIconsRow { icons: Icon[]; } type IconRow = IconCategoryRow | IconIconsRow; function isCategoryRow(iconRow: IconRow): iconRow is IconCategoryRow { return !!(iconRow as IconCategoryRow).category; } function getRowsFromIcons( filteredIcons: Icon[], iconsPerRow: number, ): IconRow[] { const categoryGroups: Record = {}; for (const icon of filteredIcons) { if (!categoryGroups[icon.category]) categoryGroups[icon.category] = []; categoryGroups[icon.category].push(icon); } const result: IconRow[] = []; const sortedCategories = Object.keys(categoryGroups).sort(); for (const sortedCategory of sortedCategories) { result.push({ category: sortedCategory, numIcons: categoryGroups[sortedCategory].length, }); const iconRows = chunk(categoryGroups[sortedCategory], iconsPerRow); for (const iconRow of iconRows) { result.push({ icons: iconRow }); } } return result; } const ICON_BOTTOM_PADDING = 65; const HEADER_HEIGHT = 150; const HEADER_INNER_HEIGHT = 15 + 40; const HEADER_TOP_PADDING = HEADER_HEIGHT - HEADER_INNER_HEIGHT; function getItemSize(row: IconRow, iconWidth: number): number { if (isCategoryRow(row)) { return HEADER_HEIGHT; } else { return iconWidth + ICON_BOTTOM_PADDING; } } interface IconListContextValue { iconWidth: number; iconsPerRow: number; } export const IconListContext = React.createContext< IconListContextValue | undefined >(undefined); export interface IconListProps { filters: IconListFilters; allIcons: Icon[]; } export function IconList({ filters, allIcons }: IconListProps) { const filteredIcons = filterIcons(allIcons, filters); const { ref, width = 400 } = useResizeObserver(); const iconsPerRow = width ? Math.floor((width + ICON_SPACE) / (ICON_WIDTH + ICON_SPACE)) : null; let children = null; const listRef = React.useRef | null>(); const [height, setHeight] = React.useState(400); const iconWidth = iconsPerRow ? Math.floor((width + ICON_SPACE) / iconsPerRow) - ICON_SPACE : null; React.useEffect(() => { setHeight(window.innerHeight); }, []); React.useEffect(() => { if (listRef.current) { listRef.current.resetAfterIndex(0, true); } }, [iconWidth, height]); if (filteredIcons.length && iconsPerRow && width && iconWidth) { const iconRows = getRowsFromIcons(filteredIcons, iconsPerRow); children = ( {({ ref, outerRef, style, onScroll }: any) => ( ref={(c) => { if (typeof ref === 'function') ref(c); else ref.current = c; listRef.current = c; }} itemData={iconRows} width={width} outerRef={outerRef} style={style} height={height} itemCount={iconRows.length} onScroll={onScroll} itemSize={(index) => getItemSize(iconRows[index], iconWidth)} > {Row} )} ); } else if (width && filters.search) { return ; } return {children}; } const Container = styled.div` width: 100%; margin-top: -${HEADER_TOP_PADDING}px; > :first-child { overflow: visible !important; > :first-child { -webkit-overflow-scrolling: touch; } } `; const Row = React.memo( ({ data, index, style }: ListChildComponentProps) => { const { iconWidth } = React.useContext(IconListContext)!; const row = data[index]; if (isCategoryRow(row)) { return ( ); } else { return ; } }, areEqual, ); Row.displayName = 'Row';