diff --git a/demo/src/components/ColumnsControllers.jsx b/demo/src/components/ColumnsControllers.jsx index 80d8774..972f24e 100644 --- a/demo/src/components/ColumnsControllers.jsx +++ b/demo/src/components/ColumnsControllers.jsx @@ -40,6 +40,11 @@ const ColumnsControllers = ({ controllers }) => { setColumns(columns); }; + const setSearchText = (column, searchText) => { + column.searchText = searchText; + setColumns(columns); + }; + const setSortable = (column) => { column.sortable = !column.sortable; setColumns(columns); @@ -73,6 +78,19 @@ const ColumnsControllers = ({ controllers }) => { /> ) : null} + {column.searchable && + column.id !== "checkbox" && + column.id !== "buttons" ? ( + + + setSearchText(column, e.target.value) + } + /> + + ) : null} { setSearchable(column)} + onChange={() => { + if (column.searchable) { + setSearchText(column, ""); + } + setSearchable(column); + }} /> ) : null} diff --git a/demo/src/components/TableControllers.jsx b/demo/src/components/TableControllers.jsx index 9a89f47..d9868ef 100644 --- a/demo/src/components/TableControllers.jsx +++ b/demo/src/components/TableControllers.jsx @@ -80,6 +80,27 @@ const TableControllers = ({ controllers }) => { } /> + + { + const shouldSearchByColumn = + !controllers.searchByColumn[0]; + + if (shouldSearchByColumn) { + const cols = controllers.columns[0].map( + (column) => ({ + ...column, + searchText: "", + }) + ); + controllers.columns[1](cols); + } + controllers.searchByColumn[1](shouldSearchByColumn); + }} + /> + { pinned: true, sortable: false, resizable: false, + searchable: false, cellRenderer: ButtonsCell, editorCellRenderer: ButtonsEditorCell, }, diff --git a/demo/src/views/sync.js b/demo/src/views/sync.js index f8c9039..c2088cc 100644 --- a/demo/src/views/sync.js +++ b/demo/src/views/sync.js @@ -31,6 +31,7 @@ const MyAwesomeTable = () => { let [columns, setColumns] = useState(getColumns({ setRowsData })); let [isSettingsOpen, setIsSettingsOpen] = useState(false); let [selectAllMode, setSelectAllMode] = useState("page"); + let [searchByColumn, setSearchByColumn] = useState(true); const controllers = { columns: [columns, setColumns], @@ -55,6 +56,7 @@ const MyAwesomeTable = () => { minSearchChars: [minSearchChars, setMinSearchChars], minColumnResizeWidth: [minColumnResizeWidth, setMinColumnWidth], selectAllMode: [selectAllMode, setSelectAllMode], + searchByColumn: [searchByColumn, setSearchByColumn], }; useEffect(() => { @@ -114,6 +116,7 @@ const MyAwesomeTable = () => { minSearchChars={minSearchChars} minColumnResizeWidth={minColumnResizeWidth} selectAllMode={selectAllMode} + searchByColumn={searchByColumn} /> diff --git a/src/components/CellContainer.jsx b/src/components/CellContainer.jsx index 9da6f85..1f4c11e 100644 --- a/src/components/CellContainer.jsx +++ b/src/components/CellContainer.jsx @@ -17,6 +17,7 @@ const CellContainer = ({ id, config: { highlightSearch, + searchByColumn, tableHasSelection, additionalProps: { cellContainer: additionalProps = {} }, }, @@ -67,6 +68,9 @@ const CellContainer = ({ const getValue = () => { let value; + const searchToHighlight = searchByColumn + ? column.searchText + : searchText; switch (column.id) { case "checkbox": @@ -89,7 +93,7 @@ const CellContainer = ({ highlightSearch && valuePassesSearch(value, column) ) - return getHighlightedText(value, searchText); + return getHighlightedText(value, searchToHighlight); } return value; diff --git a/src/components/HeaderCell.jsx b/src/components/HeaderCell.jsx index 23ee6d8..cf3626a 100644 --- a/src/components/HeaderCell.jsx +++ b/src/components/HeaderCell.jsx @@ -4,7 +4,10 @@ const HeaderCell = ({ column, tableManager }) => { const { config: { additionalProps: { headerCell: additionalProps = {} }, + components: { SearchColumn }, + searchByColumn, }, + searchApi: { setColumnSearchText }, } = tableManager; let classNames = ( @@ -12,13 +15,25 @@ const HeaderCell = ({ column, tableManager }) => { ).trim(); return ( - - {column.label} - +
+ + {column.label} + + {searchByColumn && column.searchable && ( + + setColumnSearchText(column.field, value) + } + /> + )} +
); }; diff --git a/src/components/HeaderCellContainer.jsx b/src/components/HeaderCellContainer.jsx index b51d92b..032b279 100644 --- a/src/components/HeaderCellContainer.jsx +++ b/src/components/HeaderCellContainer.jsx @@ -21,11 +21,13 @@ const HeaderCellContainer = ({ index, column, tableManager }) => { let { config: { isHeaderSticky, + searchByColumn, components: { DragHandle }, additionalProps: { headerCellContainer: additionalProps = {} }, icons: { sortAscending: sortAscendingIcon, sortDescending: sortDescendingIcon, + sortUnsorted: sortUnsortedIcon, }, }, sortApi: { sort, toggleSort }, @@ -79,7 +81,9 @@ const HeaderCellContainer = ({ index, column, tableManager }) => { isPinnedRight ? " rgt-cell-header-pinned rgt-cell-header-pinned-right" : "" - } ${column.className}`.trim(); + }${searchByColumn ? " rgt-cell-header-with-search" : ""} ${ + column.className + }`.trim(); } return ( @@ -144,7 +148,11 @@ const HeaderCellContainer = ({ index, column, tableManager }) => { ...selectionProps, }) : column.headerCellRenderer(headerCellProps)} - {sort.colId !== column.id ? null : sort.isAsc ? ( + {!column.sortable ? null : sort.colId !== column.id ? ( + + {sortUnsortedIcon} + + ) : sort.isAsc ? ( {sortAscendingIcon} diff --git a/src/components/SearchColumn.jsx b/src/components/SearchColumn.jsx new file mode 100644 index 0000000..a1bbbd2 --- /dev/null +++ b/src/components/SearchColumn.jsx @@ -0,0 +1,23 @@ +import React from "react"; + +const SearchColumn = ({ tableManager, name, value, onChange }) => { + const { + config: { + texts: { search: searchText }, + }, + } = tableManager; + + return ( + onChange(event.target.value)} + onClick={(event) => event.stopPropagation()} + placeholder={searchText} + className="rgt-search-column-input" + /> + ); +}; + +export default SearchColumn; diff --git a/src/components/index.js b/src/components/index.js index a5f6254..7abbe47 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -14,6 +14,7 @@ import NoResults from "./NoResults"; import PopoverButton from "./PopoverButton"; import Row from "./Row"; import Search from "./Search"; +import SearchColumn from "./SearchColumn"; import Information from "./Information"; import PageSize from "./PageSize"; import Pagination from "./Pagination"; @@ -35,6 +36,7 @@ export { PopoverButton, Row, Search, + SearchColumn, Information, PageSize, Pagination, diff --git a/src/defaults/icons.js b/src/defaults/icons.js index c893025..e7c93b9 100644 --- a/src/defaults/icons.js +++ b/src/defaults/icons.js @@ -68,6 +68,7 @@ const MENU_ICON = ( const SORT_ASCENDING_ICON = ; const SORT_DESCENDING_ICON = ; +const SORT_UNSORTED_ICON = ; const SEARCH_ICON = ; @@ -77,5 +78,6 @@ export default { columnVisibility: MENU_ICON, sortAscending: SORT_ASCENDING_ICON, sortDescending: SORT_DESCENDING_ICON, + sortUnsorted: SORT_UNSORTED_ICON, search: SEARCH_ICON, }; diff --git a/src/hooks/useColumns.jsx b/src/hooks/useColumns.jsx index d05743c..afd7a2d 100644 --- a/src/hooks/useColumns.jsx +++ b/src/hooks/useColumns.jsx @@ -58,6 +58,7 @@ const useColumns = (props, tableManager) => { editable: true, sortable: true, resizable: true, + searchText: "", search: ({ value, searchText }) => value .toString() diff --git a/src/hooks/useSearch.jsx b/src/hooks/useSearch.jsx index 82ad372..0931e1f 100644 --- a/src/hooks/useSearch.jsx +++ b/src/hooks/useSearch.jsx @@ -2,8 +2,8 @@ import { useState, useCallback, useRef } from "react"; const useSearch = (props, tableManager) => { const { - config: { minSearchChars }, - columnsApi: { columns }, + config: { minSearchChars, searchByColumn }, + columnsApi: { columns, setColumns }, } = tableManager; const searchApi = useRef({}).current; @@ -20,35 +20,65 @@ const useSearch = (props, tableManager) => { props.onSearchTextChange?.(searchText, tableManager); }; + searchApi.setColumnSearchText = (columnField, searchText) => { + const columnsClone = [...columns]; + const columnIndex = columnsClone.findIndex( + (col) => col.field === columnField + ); + + if (columnIndex !== -1) { + columnsClone[columnIndex].searchText = searchText; + setColumns(columnsClone); + + props.onColumnSearchTextChange?.( + searchText, + columnField, + tableManager + ); + } + }; + searchApi.valuePassesSearch = (value, column) => { if (!value) return false; if (!column?.searchable) return false; - if (searchApi.searchText.length < minSearchChars) return false; + if (!searchByColumn && searchApi.searchText.length < minSearchChars) { + return false; + } else if ( + searchByColumn && + column.searchText.length < minSearchChars + ) { + return false; + } return column.search({ value: value.toString(), - searchText: searchApi.searchText, + searchText: searchByColumn + ? column.searchText + : searchApi.searchText, }); }; searchApi.searchRows = useCallback( (rows) => { - var cols = columns.reduce((cols, coldef) => { - cols[coldef.field] = coldef; - return cols; - }, {}); + const searchValue = ({ searchText, cellValue, column }) => { + const value = column.getValue({ + value: cellValue, + column, + }); + return column.search({ + value: value?.toString() || "", + searchText, + }); + }; if (searchApi.searchText.length >= minSearchChars) { rows = rows.filter((item) => - Object.keys(item).some((key) => { - if (cols[key] && cols[key].searchable) { - const value = cols[key].getValue({ - value: item[key], - column: cols[key], - }); - return cols[key].search({ - value: value?.toString() || "", + columns.some((column) => { + if (column.searchable) { + return searchValue({ searchText: searchApi.searchText, + cellValue: item[column.field], + column, }); } return false; @@ -56,9 +86,26 @@ const useSearch = (props, tableManager) => { ); } + if (searchByColumn) { + columns.forEach((column) => { + if ( + column.searchable && + column.searchText.length >= minSearchChars + ) { + rows = rows.filter((item) => { + return searchValue({ + searchText: column.searchText, + cellValue: item[column.field], + column, + }); + }); + } + }); + } + return rows; }, - [searchApi.searchText, columns, minSearchChars] + [searchApi.searchText, searchByColumn, columns, minSearchChars] ); return searchApi; diff --git a/src/hooks/useTableManager.jsx b/src/hooks/useTableManager.jsx index abba823..5c583bd 100644 --- a/src/hooks/useTableManager.jsx +++ b/src/hooks/useTableManager.jsx @@ -51,6 +51,7 @@ const useTableManager = (props) => { isPaginated: props.isPaginated, enableColumnsReorder: props.enableColumnsReorder, highlightSearch: props.highlightSearch, + searchByColumn: props.searchByColumn, showSearch: props.showSearch, showRowsInformation: props.showRowsInformation, showColumnVisibilityManager: props.showColumnVisibilityManager, diff --git a/src/index.css b/src/index.css index 0d5bb73..5afa14f 100644 --- a/src/index.css +++ b/src/index.css @@ -103,6 +103,23 @@ border-bottom: var(--rgt-border); } +.rgt-cell-header-with-search { + display: flex; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 100%; + z-index: 1; + min-height: 68px; + max-height: 68px; + border-bottom: var(--rgt-border); +} + +.rgt-cell-header-with-search .rgt-cell-header-content-container { + gap: 5px; +} + .rgt-cell-header-virtual-col { border-bottom: var(--rgt-border); background: var(--rgt-background-color); @@ -129,6 +146,13 @@ justify-content: center; } +.rgt-cell-header-content-container { + display: inline-flex; + flex-direction: column; + flex: 1; + overflow: hidden; +} + .rgt-placeholder-cell { position: relative; border-radius: 2px; @@ -269,6 +293,13 @@ font-size: 16px; margin-left: 5px; display: inline-flex; + justify-content: center; + width: 12px; + line-height: 18px; +} + +.rgt-sort-icon-unsorted { + font-size: 15px; } .rgt-container-overlay { @@ -431,6 +462,12 @@ padding: 0; } +.rgt-search-column-input { + width: 100%; + outline: none; + border: 1px solid; +} + .rgt-cell-editor-inner { position: relative; height: 30px; diff --git a/src/index.js b/src/index.js index 54737a3..ebced9a 100644 --- a/src/index.js +++ b/src/index.js @@ -128,6 +128,7 @@ GridTable.defaultProps = { pageSizes: [20, 50, 100], isHeaderSticky: true, highlightSearch: true, + searchByColumn: false, minSearchChars: 2, isPaginated: true, isVirtualScroll: true, @@ -162,6 +163,7 @@ GridTable.propTypes = { minColumnResizeWidth: PropTypes.number, highlightSearch: PropTypes.bool, showSearch: PropTypes.bool, + searchByColumn: PropTypes.bool, showRowsInformation: PropTypes.bool, showColumnVisibilityManager: PropTypes.bool, minSearchChars: PropTypes.number, @@ -177,6 +179,7 @@ GridTable.propTypes = { selectAllMode: PropTypes.string, // events onColumnsChange: PropTypes.func, + onColumnSearchTextChange: PropTypes.func, onSearchTextChange: PropTypes.func, onSelectedRowsChange: PropTypes.func, onSortChange: PropTypes.func,