diff --git a/frontend/src/components/cur-dir-path/dir-tool.js b/frontend/src/components/cur-dir-path/dir-tool.js index 9c32a916772..df7240f0d55 100644 --- a/frontend/src/components/cur-dir-path/dir-tool.js +++ b/frontend/src/components/cur-dir-path/dir-tool.js @@ -9,6 +9,7 @@ import ListTagPopover from '../popover/list-tag-popover'; import ViewModes from '../../components/view-modes'; import SortMenu from '../../components/sort-menu'; import MetadataViewToolBar from '../../metadata/components/view-toolbar'; +import TagsTableSearcher from '../../tag/views/all-tags/tags-table/tags-searcher'; import { PRIVATE_FILE_TYPE } from '../../constants'; const propTypes = { @@ -116,6 +117,7 @@ class DirTool extends React.Component { if (isTagView) { return (
+
); } diff --git a/frontend/src/components/sf-table/index.css b/frontend/src/components/sf-table/index.css index 2e442725e7b..83981c91b14 100644 --- a/frontend/src/components/sf-table/index.css +++ b/frontend/src/components/sf-table/index.css @@ -1,3 +1,97 @@ +.sf-table-searcher-container { + margin-left: 15px; +} + +.sf-table-searcher-btn { + display: inline-flex; + align-items: center; + border-radius: 3px; + height: 22px; + padding: 0 .5rem; + transition: all .1s ease-in; +} + +.sf-table-searcher-btn:hover { + background-color: #efefef; + cursor: pointer; +} + +.sf-table-searcher-btn .sf3-font-search { + display: inline-block; + color: #666; +} + +.sf-table-searcher-input-wrapper { + position: relative; +} + +.sf-table-searcher-input { + padding-left: 30px; + padding-right: 60px; + height: 30px; + width: 260px; +} + +.sf-table-searcher-input-wrapper .btn-close-searcher-wrapper { + pointer-events: all; + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + min-width: 20px; + height: 20px; + right: 4px; + top: 50%; + transform: translateY(-50%); +} + +.sf-table-searcher-input-wrapper .btn-close-searcher { + font-size: 14px; +} + +.sf-table-searcher-input-wrapper .btn-close-searcher-wrapper:hover { + background-color: #efefef; + cursor: pointer; +} + +.sf-table-searcher-input-wrapper .input-icon-addon.search-poll-button { + display: flex; + font-size: 12px; + height: 30px; + left: auto; + line-height: 30px; + min-width: 35px; + pointer-events: all; + right: 28px; + text-align: center; +} + +.sf-table-searcher-input-wrapper .search-upward, +.sf-table-searcher-input-wrapper .search-backward { + color: #666; + font-size: 12px; + background-color: #efefef; + display: inline-block; + height: 20px; + line-height: 20px; + width: 20px; +} + +.sf-table-searcher-input-wrapper .search-upward:hover, +.sf-table-searcher-input-wrapper .search-backward:hover { + cursor: pointer; + background-color: #DBDBDB; +} + +.sf-table-searcher-input-wrapper .search-upward { + margin-left: 8px; + border-radius: 2px 0 0 2px; +} + +.sf-table-searcher-input-wrapper .search-backward { + border-radius: 0 2px 2px 0; +} + .sf-table-wrapper { height: 100%; width: 100%; diff --git a/frontend/src/components/sf-table/searcher/index.js b/frontend/src/components/sf-table/searcher/index.js new file mode 100644 index 00000000000..0b3443e0757 --- /dev/null +++ b/frontend/src/components/sf-table/searcher/index.js @@ -0,0 +1,100 @@ +import React, { useMemo, useState } from 'react'; +import { gettext } from '../../../utils/constants'; +import { KeyCodes } from '../../../constants'; +import { isModG, isModShiftG } from '../../../metadata/utils/hotkey'; +import SFTableSearcherInput from './searcher-input'; +import { checkHasSearchResult } from '../utils/search'; + +const SFTableSearcher = ({ recordsCount, columnsCount, searchResult, searchCells, closeSearcher, focusNextMatchedCell, focusPreviousMatchedCell }) => { + const [isSearchActive, setIsSearchActive] = useState(false); + const [hasSearchValue, setHasSearchValue] = useState(false); + + const hasSearchResult = useMemo(() => { + return checkHasSearchResult(searchResult); + }, [searchResult]); + + const onToggleSearch = () => { + setIsSearchActive(!isSearchActive); + }; + + const handleCloseSearcher = () => { + setIsSearchActive(false); + closeSearcher && closeSearcher(); + }; + + const onKeyDown = (e) => { + const isEmptySearchResult = !hasSearchResult; + if (e.keyCode === KeyCodes.Escape) { + e.preventDefault(); + handleCloseSearcher(); + } else if (isModG(e)) { + e.preventDefault(); + if (isEmptySearchResult) return; + focusNextMatchedCell && focusNextMatchedCell(); + } else if (isModShiftG(e)) { + e.preventDefault(); + if (isEmptySearchResult) return; + focusPreviousMatchedCell && focusPreviousMatchedCell(); + } + }; + + const renderSearchPollButton = () => { + return ( + + {hasSearchValue && + + {hasSearchResult ? + (searchResult.currentSelectIndex + 1 + ' of ' + searchResult.matchedCells.length) : '0 of 0' + } + + } + {hasSearchResult && + <> + {}}> + + {}}> + + + } + + ); + }; + + return ( +
+ {!isSearchActive && ( + + + + )} + {isSearchActive && ( +
+ + + {renderSearchPollButton()} + + + +
+ )} +
+ ); +}; + +export default SFTableSearcher; diff --git a/frontend/src/components/sf-table/searcher/searcher-input.js b/frontend/src/components/sf-table/searcher/searcher-input.js new file mode 100644 index 00000000000..f48bcf2e1a3 --- /dev/null +++ b/frontend/src/components/sf-table/searcher/searcher-input.js @@ -0,0 +1,59 @@ +import React, { useRef, useState } from 'react'; +import { gettext } from '../../../utils/constants'; + +const SFTableSearcherInput = ({ recordsCount, columnsCount, setHasSearchValue, searchCells, onKeyDown }) => { + const [searchValue, setSearchValue] = useState(''); + + const isInputtingChinese = useRef(false); + const inputTimer = useRef(null); + + const getSearchDelayTime = () => { + const viewCellsCount = (recordsCount || 0) * (columnsCount || 0); + let delayTime = viewCellsCount * 0.1; + delayTime = delayTime > 500 ? 500 : Math.floor(delayTime); + if (delayTime < 100) { + delayTime = 100; + } + return delayTime; + }; + + const onChangeSearchValue = (e) => { + inputTimer.current && clearTimeout(inputTimer.current); + const text = e.target.value; + const wait = getSearchDelayTime(); + const currSearchValue = text || ''; + const trimmedSearchValue = currSearchValue.trim(); + setSearchValue(currSearchValue); + setHasSearchValue(!!trimmedSearchValue); + if (!isInputtingChinese.current) { + inputTimer.current = setTimeout(() => { + searchCells && searchCells(trimmedSearchValue); + }, wait); + } + }; + + const onCompositionStart = () => { + isInputtingChinese.current = true; + }; + + const onCompositionEnd = (e) => { + isInputtingChinese.current = false; + onChangeSearchValue(e); + }; + + return ( + + ); +}; + +export default SFTableSearcherInput; diff --git a/frontend/src/components/sf-table/table-main/records/record/index.js b/frontend/src/components/sf-table/table-main/records/record/index.js index 9cc8eea26a8..626aa9c3bf4 100644 --- a/frontend/src/components/sf-table/table-main/records/record/index.js +++ b/frontend/src/components/sf-table/table-main/records/record/index.js @@ -82,15 +82,15 @@ class Record extends React.Component { getFrozenCells = () => { const { columns, sequenceColumnWidth, lastFrozenColumnKey, groupRecordIndex, index: recordIndex, record, - cellMetaData, isGroupView, height, columnColor + cellMetaData, isGroupView, height, columnColor, treeNodeKey, } = this.props; const frozenColumns = getFrozenColumns(columns); if (frozenColumns.length === 0) return null; const recordId = record._id; return frozenColumns.map((column, index) => { const { key } = column; - const isCellHighlight = this.checkIsCellHighlight(key, recordId); - const isCurrentCellHighlight = this.checkIsCurrentCellHighlight(key, recordId); + const isCellHighlight = this.checkIsCellHighlight(key, recordId, treeNodeKey); + const isCurrentCellHighlight = this.checkIsCurrentCellHighlight(key, recordId, treeNodeKey); const highlightClassName = isCurrentCellHighlight ? 'cell-current-highlight' : isCellHighlight ? 'cell-highlight' : null; const isCellSelected = this.checkIsCellSelected(index); const isLastCell = this.checkIsLastCell(columns, key); @@ -126,10 +126,10 @@ class Record extends React.Component { }); }; - checkIsCellHighlight = (columnKey, rowId) => { + checkIsCellHighlight = (columnKey, rowId, treeNodeKey) => { const { searchResult } = this.props; if (searchResult) { - const matchedColumns = searchResult.matchedRows[rowId]; + const matchedColumns = this.props.showRecordAsTree ? searchResult.matchedRows[treeNodeKey] : searchResult.matchedRows[rowId]; if (matchedColumns && matchedColumns.includes(columnKey)) { return true; } @@ -137,14 +137,15 @@ class Record extends React.Component { return false; }; - checkIsCurrentCellHighlight = (columnKey, rowId) => { + checkIsCurrentCellHighlight = (columnKey, rowId, treeNodeKey) => { const { searchResult } = this.props; if (searchResult) { const { currentSelectIndex } = searchResult; if (typeof(currentSelectIndex) !== 'number') return false; const currentSelectCell = searchResult.matchedCells[currentSelectIndex]; if (!currentSelectCell) return false; - if (currentSelectCell.row === rowId && currentSelectCell.column === columnKey) return true; + const isCurrentRow = this.props.showRecordAsTree ? currentSelectCell.nodeKey === treeNodeKey : currentSelectCell.row === rowId; + return isCurrentRow && currentSelectCell.column === columnKey; } return false; }; @@ -152,7 +153,7 @@ class Record extends React.Component { getColumnCells = () => { const { columns, sequenceColumnWidth, colOverScanStartIdx, colOverScanEndIdx, groupRecordIndex, index: recordIndex, - record, cellMetaData, isGroupView, height, columnColor, + record, cellMetaData, isGroupView, height, columnColor, treeNodeKey, } = this.props; const recordId = record._id; const rendererColumns = columns.slice(colOverScanStartIdx, colOverScanEndIdx); @@ -160,8 +161,8 @@ class Record extends React.Component { const { key, frozen } = column; const needBindEvents = !frozen; const isCellSelected = this.checkIsCellSelected(columns.findIndex(col => col.key === column.key)); - const isCellHighlight = this.checkIsCellHighlight(key, recordId); - const isCurrentCellHighlight = this.checkIsCurrentCellHighlight(key, recordId); + const isCellHighlight = this.checkIsCellHighlight(key, recordId, treeNodeKey); + const isCurrentCellHighlight = this.checkIsCurrentCellHighlight(key, recordId, treeNodeKey); const highlightClassName = isCurrentCellHighlight ? 'cell-current-highlight' : isCellHighlight ? 'cell-highlight' : null; const isLastCell = this.checkIsLastCell(columns, key); const bgColor = columnColor && columnColor[key]; diff --git a/frontend/src/components/sf-table/table-main/records/tree-body.js b/frontend/src/components/sf-table/table-main/records/tree-body.js index 19e0e7be749..f6d809e238c 100644 --- a/frontend/src/components/sf-table/table-main/records/tree-body.js +++ b/frontend/src/components/sf-table/table-main/records/tree-body.js @@ -13,6 +13,7 @@ import { checkEditableViaClickCell, checkIsColumnSupportDirectEdit, getColumnByI import { checkIsCellSupportOpenEditor } from '../../utils/selected-cell-utils'; import { LOCAL_KEY_TREE_NODE_FOLDED } from '../../constants/tree'; import { TreeMetrics } from '../../utils/tree-metrics'; +import { checkHasSearchResult } from '../../utils/search'; const ROW_HEIGHT = 33; const RENDER_MORE_NUMBER = 10; @@ -36,6 +37,7 @@ class TreeBody extends Component { startRenderIndex: 0, endRenderIndex: this.getInitEndIndex(nodes), keyNodeFoldedMap: validKeyTreeNodeFoldedMap, + keyNodeFoldedMapForSearch: {}, selectedPosition: null, isScrollingRightScrollbar: false, }; @@ -58,12 +60,19 @@ class TreeBody extends Component { } UNSAFE_componentWillReceiveProps(nextProps) { - const { recordsCount, recordIds, treeNodesCount, recordsTree } = nextProps; + const { recordsCount, recordIds, treeNodesCount, recordsTree, searchResult } = nextProps; + const searchResultChanged = searchResult !== this.props.searchResult; if ( recordsCount !== this.props.recordsCount || recordIds !== this.props.recordIds || - treeNodesCount !== this.props.treeNodesCount || recordsTree !== this.props.recordsTree + treeNodesCount !== this.props.treeNodesCount || recordsTree !== this.props.recordsTree || + searchResultChanged ) { - this.recalculateRenderIndex(recordsTree); + const hasSearchResult = checkHasSearchResult(searchResult); + const keyNodeFoldedMap = hasSearchResult ? {} : this.state.keyNodeFoldedMap; + this.recalculateRenderIndex(recordsTree, keyNodeFoldedMap); + if (searchResultChanged) { + this.setState({ keyNodeFoldedMapForSearch: {} }); + } } } @@ -119,8 +128,8 @@ class TreeBody extends Component { return Math.min(Math.ceil((contentScrollTop + height) / ROW_HEIGHT) + RENDER_MORE_NUMBER, nodes.length); }; - recalculateRenderIndex = (recordsTree) => { - const { startRenderIndex, endRenderIndex, keyNodeFoldedMap } = this.state; + recalculateRenderIndex = (recordsTree, keyNodeFoldedMap) => { + const { startRenderIndex, endRenderIndex } = this.state; const nodes = this.getShownNodes(recordsTree, keyNodeFoldedMap); const contentScrollTop = this.resultContentRef.scrollTop; const start = Math.max(0, Math.floor(contentScrollTop / ROW_HEIGHT) - RENDER_MORE_NUMBER); @@ -339,6 +348,14 @@ class TreeBody extends Component { this.columnVisibleEnd = columnVisibleEnd; }; + jumpToRow = (scrollToRowIndex) => { + const { treeNodesCount } = this.props; + const rowHeight = this.getRowHeight(); + const height = this.resultContentRef.offsetHeight; + const scrollTop = Math.min(scrollToRowIndex * rowHeight, treeNodesCount * rowHeight - height); + this.setScrollTop(scrollTop); + }; + scrollToColumn = (idx) => { const { columns, getTableContentRect } = this.props; const { width: tableContentWidth } = getTableContentRect(); @@ -496,13 +513,25 @@ class TreeBody extends Component { }; toggleExpandNode = (nodeKey) => { - const { recordsTree } = this.props; - const { keyNodeFoldedMap, endRenderIndex } = this.state; + const { recordsTree, searchResult } = this.props; + const { keyNodeFoldedMap, keyNodeFoldedMapForSearch, endRenderIndex } = this.state; + const hasSearchResult = checkHasSearchResult(searchResult); let updatedKeyNodeFoldedMap = { ...keyNodeFoldedMap }; - if (updatedKeyNodeFoldedMap[nodeKey]) { - delete updatedKeyNodeFoldedMap[nodeKey]; + let updatedKeyNodeFoldedMapForSearch = { ...keyNodeFoldedMapForSearch }; + if (hasSearchResult) { + if (updatedKeyNodeFoldedMapForSearch[nodeKey]) { + delete updatedKeyNodeFoldedMapForSearch[nodeKey]; + delete updatedKeyNodeFoldedMap[nodeKey]; + } else { + updatedKeyNodeFoldedMapForSearch[nodeKey] = true; + updatedKeyNodeFoldedMap[nodeKey] = true; + } } else { - updatedKeyNodeFoldedMap[nodeKey] = true; + if (updatedKeyNodeFoldedMap[nodeKey]) { + delete updatedKeyNodeFoldedMap[nodeKey]; + } else { + updatedKeyNodeFoldedMap[nodeKey] = true; + } } if (this.props.storeFoldedTreeNodes) { @@ -510,8 +539,15 @@ class TreeBody extends Component { this.props.storeFoldedTreeNodes(LOCAL_KEY_TREE_NODE_FOLDED, updatedKeyNodeFoldedMap); } - const updatedNodes = this.getShownNodes(recordsTree, updatedKeyNodeFoldedMap); - let updates = { nodes: updatedNodes, keyNodeFoldedMap: updatedKeyNodeFoldedMap }; + const updatedNodes = this.getShownNodes(recordsTree, hasSearchResult ? updatedKeyNodeFoldedMapForSearch : updatedKeyNodeFoldedMap); + let updates = { + nodes: updatedNodes, + keyNodeFoldedMap: updatedKeyNodeFoldedMap, + }; + if (hasSearchResult) { + updates.keyNodeFoldedMapForSearch = updatedKeyNodeFoldedMapForSearch; + } + const end = this.recalculateRenderEndIndex(updatedNodes); if (end !== endRenderIndex) { updates.endRenderIndex = end; @@ -525,8 +561,8 @@ class TreeBody extends Component { }; renderRecords = () => { - const { treeMetrics, showCellColoring, columnColors } = this.props; - const { nodes, keyNodeFoldedMap, startRenderIndex, endRenderIndex, selectedPosition } = this.state; + const { treeMetrics, showCellColoring, columnColors, searchResult } = this.props; + const { nodes, keyNodeFoldedMap, keyNodeFoldedMapForSearch, startRenderIndex, endRenderIndex, selectedPosition } = this.state; this.initFrozenNodesRef(); const visibleNodes = this.getVisibleNodesInRange(); const nodesCount = nodes.length; @@ -534,6 +570,7 @@ class TreeBody extends Component { const scrollLeft = this.props.getScrollLeft(); const rowHeight = this.getRowHeight(); const cellMetaData = this.getCellMetaData(); + const hasSearchResult = checkHasSearchResult(searchResult); let shownNodes = visibleNodes.map((node, index) => { const { _id: recordId, node_key, node_depth, node_index } = node; const hasChildNodes = checkTreeNodeHasChildNodes(node); @@ -543,7 +580,7 @@ class TreeBody extends Component { const isLastRecord = lastRecordIndex === recordIndex; const hasSelectedCell = this.props.hasSelectedCell({ recordIndex }, selectedPosition); const columnColor = showCellColoring ? columnColors[recordId] : {}; - const isFoldedNode = !!keyNodeFoldedMap[node_key]; + const isFoldedNode = hasSearchResult ? !!keyNodeFoldedMapForSearch[node_key] : !!keyNodeFoldedMap[node_key]; return ( { + if (typeof value !== 'string') return ''; + return value.replace(/[.\\[\]{}()|^$?*+]/g, '\\$&'); +}; + +export const getSearchRule = (value) => { + if (typeof value !== 'string') { + return false; + } + let searchRule = value; + searchRule = searchRule.trim(); + if (searchRule.length === 0) { + return false; + } + // i: search value uppercase and lowercase are not sensitive + return new RegExp(escapeRegExp(searchRule), 'i'); +}; + +export const checkHasSearchResult = (searchResult) => { + const { matchedCells } = searchResult || {}; + return Array.isArray(matchedCells) ? matchedCells.length > 0 : false; +}; diff --git a/frontend/src/metadata/constants/event-bus-type.js b/frontend/src/metadata/constants/event-bus-type.js index 43d479dea46..3916e7fa678 100644 --- a/frontend/src/metadata/constants/event-bus-type.js +++ b/frontend/src/metadata/constants/event-bus-type.js @@ -41,6 +41,7 @@ export const EVENT_BUS_TYPE = { // metadata RELOAD_DATA: 'reload_data', + UPDATE_SEARCH_RESULT: 'update_search_result', // view MODIFY_FILTERS: 'modify_filters', diff --git a/frontend/src/tag/constants/column/private.js b/frontend/src/tag/constants/column/private.js index 2b1190739a7..ad5245557e2 100644 --- a/frontend/src/tag/constants/column/private.js +++ b/frontend/src/tag/constants/column/private.js @@ -31,3 +31,7 @@ export const EDITABLE_PRIVATE_COLUMN_KEYS = [ PRIVATE_COLUMN_KEY.TAG_COLOR, PRIVATE_COLUMN_KEY.TAG_FILE_LINKS, ]; + +export const VISIBLE_COLUMNS_KEYS = [ + PRIVATE_COLUMN_KEY.TAG_NAME, PRIVATE_COLUMN_KEY.PARENT_LINKS, PRIVATE_COLUMN_KEY.SUB_LINKS, PRIVATE_COLUMN_KEY.TAG_FILE_LINKS, +]; diff --git a/frontend/src/tag/views/all-tags/tags-table/index.css b/frontend/src/tag/views/all-tags/tags-table/index.css index 5af683cd09f..e1647ae5a84 100644 --- a/frontend/src/tag/views/all-tags/tags-table/index.css +++ b/frontend/src/tag/views/all-tags/tags-table/index.css @@ -30,6 +30,11 @@ } +.sf-metadata-all-tags-wrapper .sf-table-row .name-cell.cell-selected { + cursor: default; +} + .sf-table-cell.cell-selected .sf-table-tag-name-formatter .sf-table-tag-name:hover { text-decoration: underline; + cursor: pointer; } diff --git a/frontend/src/tag/views/all-tags/tags-table/index.js b/frontend/src/tag/views/all-tags/tags-table/index.js index 0ff5116a0c6..455ac836220 100644 --- a/frontend/src/tag/views/all-tags/tags-table/index.js +++ b/frontend/src/tag/views/all-tags/tags-table/index.js @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import SFTable from '../../../../components/sf-table'; import EditTagDialog from '../../../components/dialog/edit-tag-dialog'; @@ -6,11 +6,13 @@ import MergeTagsSelector from '../../../components/merge-tags-selector'; import { createTableColumns } from './columns-factory'; import { createContextMenuOptions } from './context-menu-options'; import { gettext } from '../../../../utils/constants'; -import { PRIVATE_COLUMN_KEY } from '../../../constants'; +import { PRIVATE_COLUMN_KEY, VISIBLE_COLUMNS_KEYS } from '../../../constants'; import { useTags } from '../../../hooks'; import EventBus from '../../../../components/common/event-bus'; -import { EVENT_BUS_TYPE } from '../../../../components/sf-table/constants/event-bus-type'; +import { EVENT_BUS_TYPE } from '../../../../metadata/constants'; +import { EVENT_BUS_TYPE as TABLE_EVENT_BUS_TYPE } from '../../../../components/sf-table/constants/event-bus-type'; import { LOCAL_KEY_TREE_NODE_FOLDED } from '../../../../components/sf-table/constants/tree'; +import { isNumber } from '../../../../utils/number'; import './index.css'; @@ -24,10 +26,6 @@ const DEFAULT_TABLE_DATA = { const KEY_STORE_SCROLL = 'table_scroll'; -const VISIBLE_COLUMNS_KEYS = [ - PRIVATE_COLUMN_KEY.TAG_NAME, PRIVATE_COLUMN_KEY.PARENT_LINKS, PRIVATE_COLUMN_KEY.SUB_LINKS, PRIVATE_COLUMN_KEY.TAG_FILE_LINKS, -]; - const TagsTable = ({ context, isLoadingMoreRecords, @@ -40,6 +38,7 @@ const TagsTable = ({ const [isShowNewSubTagDialog, setIsShowNewSubTagDialog] = useState(false); const [isShowMergeTagsSelector, setIsShowMergeTagsSelector] = useState(false); + const [searchResult, setSearchResult] = useState(null); const parentTagIdRef = useRef(null); const mergeTagsSelectorProps = useRef({}); @@ -108,7 +107,7 @@ const TagsTable = ({ deleteTags(tagsIds); const eventBus = EventBus.getInstance(); - eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE); + eventBus.dispatch(TABLE_EVENT_BUS_TYPE.SELECT_NONE); }, [deleteTags]); const onNewSubTag = useCallback((parentTagId) => { @@ -179,6 +178,44 @@ const TagsTable = ({ return false; }, []); + const scrollToCurrentSelectedCell = useCallback((searchResult, currentSelectIndex) => { + if (!window.sfTableBody) { + return; + } + const cell = searchResult.matchedCells[currentSelectIndex]; + if (!cell) { + return; + } + const { column: cellColumn, rowIndex: focusRowIndex } = cell; + const { rowVisibleStart, rowVisibleEnd, columnVisibleStart, columnVisibleEnd } = window.sfTableBody; + if (focusRowIndex < rowVisibleStart) { + window.sfTableBody.jumpToRow(focusRowIndex - 1); + } else if (focusRowIndex >= rowVisibleEnd) { + window.sfTableBody.jumpToRow(focusRowIndex); + } + + const focusColumnIndex = visibleColumns.findIndex((column) => column.key === cellColumn); + if (columnVisibleStart >= focusColumnIndex || focusColumnIndex > columnVisibleEnd) { + window.sfTableBody.scrollToColumn(focusColumnIndex); + } + }, [visibleColumns]); + + const updateSearchResult = useCallback((searchResult) => { + setSearchResult(searchResult); + const { currentSelectIndex } = searchResult || {}; + if (searchResult && isNumber(currentSelectIndex)) { + scrollToCurrentSelectedCell(searchResult, currentSelectIndex); + } + }, [scrollToCurrentSelectedCell]); + + useEffect(() => { + const eventBus = EventBus.getInstance(); + const unsubscribeUpdateSearchResult = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SEARCH_RESULT, updateSearchResult); + return () => { + unsubscribeUpdateSearchResult(); + }; + }, [updateSearchResult]); + return ( <> { + const { tagsData } = useTags(); + const [searchResult, setSearchResult] = useState(null); + + const recordsTree = useMemo(() => { + return tagsData.rows_tree || []; + }, [tagsData]); + + const recordsCount = useMemo(() => { + return recordsTree.length; + }, [recordsTree]); + + const getTagStrContentByNode = useCallback((tagNode) => { + const tagId = getTreeNodeId(tagNode); + const tag = getRowById(tagsData, tagId); + if (!tag) return null; + let strContent = {}; + VISIBLE_COLUMNS_KEYS.forEach((columnKey) => { + switch (columnKey) { + case PRIVATE_COLUMN_KEY.TAG_NAME: { + strContent[columnKey] = tag[columnKey] || ''; + break; + } + default: { + break; + } + } + }); + return strContent; + }, [tagsData]); + + const checkIsCellValueMatchedRegVal = useCallback((tagContent, columnKey, regVal) => { + const cellValueDisplayString = (tagContent && tagContent[columnKey]) || ''; + const isMatched = regVal.test(cellValueDisplayString); + return isMatched; + }, []); + + const handleUpdateSearchResult = useCallback((searchResult) => { + setSearchResult(searchResult); + + const eventBus = EventBus.getInstance(); + eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SEARCH_RESULT, searchResult); + }, []); + + const searchCells = useCallback((searchRegRule) => { + let searchResult = {}; + let matchedRows = {}; + searchResult.matchedCells = []; + searchResult.matchedRows = matchedRows; + recordsTree.forEach((tagNode, nodeIndex) => { + const nodeKey = getTreeNodeKey(tagNode); + const tagStrContent = getTagStrContentByNode(tagNode); + SUPPORT_SEARCH_COLUMNS_KEYS.forEach((columnKey) => { + const isMatched = checkIsCellValueMatchedRegVal(tagStrContent, columnKey, searchRegRule); + if (isMatched) { + if (matchedRows[nodeKey]) { + matchedRows[nodeKey].push(columnKey); + } else { + matchedRows[nodeKey] = [columnKey]; + } + searchResult.matchedCells.push({ nodeKey, tagStrContent, column: columnKey, rowIndex: nodeIndex }); + } + }); + }); + searchResult.currentSelectIndex = 0; + return searchResult; + }, [recordsTree, getTagStrContentByNode, checkIsCellValueMatchedRegVal]); + + + const handleSearchTags = useCallback((searchVal) => { + if (searchVal.length === 0) { + handleUpdateSearchResult(null); + } else { + const searchRegRule = getSearchRule(searchVal); + const searchResult = searchRegRule ? searchCells(searchRegRule) : null; + handleUpdateSearchResult(searchResult); + } + }, [searchCells, handleUpdateSearchResult]); + + const focusNextMatchedCell = useCallback(() => { + if (!searchResult) return; + const matchedCellsLength = searchResult.matchedCells.length; + let currentSelectIndex = searchResult.currentSelectIndex; + currentSelectIndex++; + if (currentSelectIndex === matchedCellsLength) { + currentSelectIndex = 0; + } + const nextSearchResult = Object.assign({}, searchResult, { currentSelectIndex: currentSelectIndex }); + handleUpdateSearchResult(nextSearchResult); + }, [searchResult, handleUpdateSearchResult]); + + const focusPreviousMatchedCell = useCallback(() => { + if (!searchResult) return; + const matchedCellsLength = searchResult.matchedCells.length; + let currentSelectIndex = searchResult.currentSelectIndex; + currentSelectIndex--; + if (currentSelectIndex === -1) { + currentSelectIndex = matchedCellsLength - 1; + } + const nextSearchResult = Object.assign({}, searchResult, { currentSelectIndex: currentSelectIndex }); + handleUpdateSearchResult(nextSearchResult); + }, [searchResult, handleUpdateSearchResult]); + + const closeSearcher = useCallback(() => { + handleUpdateSearchResult(null); + }, [handleUpdateSearchResult]); + + return ( + + ); +}; + +export default TagsTableSearcher;