@@ -15,6 +15,7 @@ import {
1515 removeNodeAtPath ,
1616 insertNode ,
1717 getDescendantCount ,
18+ find ,
1819} from './utils/tree-data-utils' ;
1920import {
2021 swapRows ,
@@ -23,6 +24,7 @@ import {
2324 defaultGetNodeKey ,
2425 defaultToggleChildrenVisibility ,
2526 defaultMoveNode ,
27+ defaultSearchMethod ,
2628} from './utils/default-handlers' ;
2729import {
2830 dndWrapRoot ,
@@ -63,6 +65,8 @@ class ReactSortableTree extends Component {
6365 swapLength : null ,
6466 swapDepth : null ,
6567 rows : this . getRows ( props . treeData ) ,
68+ searchMatches : [ ] ,
69+ searchFocusTreeIndex : null ,
6670 } ;
6771
6872 this . startDrag = this . startDrag . bind ( this ) ;
@@ -72,12 +76,21 @@ class ReactSortableTree extends Component {
7276
7377 componentWillMount ( ) {
7478 this . loadLazyChildren ( ) ;
79+ this . search ( this . props , false , false ) ;
80+ this . ignoreOneTreeUpdate = false ;
7581 }
7682
7783 componentWillReceiveProps ( nextProps ) {
84+ this . setState ( { searchFocusTreeIndex : null } ) ;
7885 if ( this . props . treeData !== nextProps . treeData ) {
79- // Load any children defined by a function
80- this . loadLazyChildren ( nextProps ) ;
86+ // Ignore updates caused by search, in order to avoid infinite looping
87+ if ( this . ignoreOneTreeUpdate ) {
88+ this . ignoreOneTreeUpdate = false ;
89+ } else {
90+ this . loadLazyChildren ( nextProps ) ;
91+ // Load any children defined by a function
92+ this . search ( nextProps , false , false ) ;
93+ }
8194
8295 // Calculate the rows to be shown from the new tree data
8396 this . setState ( {
@@ -87,6 +100,12 @@ class ReactSortableTree extends Component {
87100 swapDepth : null ,
88101 rows : this . getRows ( nextProps . treeData ) ,
89102 } ) ;
103+ } else if ( this . props . searchQuery !== nextProps . searchQuery ||
104+ this . props . searchMethod !== nextProps . searchMethod
105+ ) {
106+ this . search ( nextProps ) ;
107+ } else if ( this . props . searchFocusOffset !== nextProps . searchFocusOffset ) {
108+ this . search ( nextProps , true , true , true ) ;
90109 }
91110 }
92111
@@ -98,6 +117,68 @@ class ReactSortableTree extends Component {
98117 } ) ;
99118 }
100119
120+ search ( props = this . props , seekIndex = true , expand = true , singleSearch = false ) {
121+ const {
122+ treeData,
123+ updateTreeData,
124+ searchFinishCallback,
125+ searchQuery,
126+ searchMethod,
127+ searchFocusOffset,
128+ } = props ;
129+
130+ // Skip search if no conditions are specified
131+ if ( ( searchQuery === null || typeof searchQuery === 'undefined' || String ( searchQuery ) === '' ) &&
132+ ! searchMethod
133+ ) {
134+ this . setState ( {
135+ searchMatches : [ ] ,
136+ } ) ;
137+
138+ if ( searchFinishCallback ) {
139+ searchFinishCallback ( [ ] ) ;
140+ }
141+
142+ return ;
143+ }
144+
145+ const {
146+ treeData : expandedTreeData ,
147+ matches : searchMatches ,
148+ } = find ( {
149+ getNodeKey : this . getNodeKey ,
150+ treeData,
151+ searchQuery,
152+ searchMethod : searchMethod || defaultSearchMethod ,
153+ searchFocusOffset,
154+ expandAllMatchPaths : expand && ! singleSearch ,
155+ expandFocusMatchPaths : expand && true ,
156+ } ) ;
157+
158+ // Update the tree with data leaving all paths leading to matching nodes open
159+ if ( expand ) {
160+ this . ignoreOneTreeUpdate = true ; // Prevents infinite loop
161+ updateTreeData ( expandedTreeData ) ;
162+ }
163+
164+ if ( searchFinishCallback ) {
165+ searchFinishCallback ( searchMatches ) ;
166+ }
167+
168+ let searchFocusTreeIndex = null ;
169+ if ( seekIndex &&
170+ searchFocusOffset !== null &&
171+ searchFocusOffset < searchMatches . length
172+ ) {
173+ searchFocusTreeIndex = searchMatches [ searchFocusOffset ] . treeIndex ;
174+ }
175+
176+ this . setState ( {
177+ searchMatches,
178+ searchFocusTreeIndex,
179+ } ) ;
180+ }
181+
101182 startDrag ( { path } ) {
102183 const draggingTreeData = removeNodeAtPath ( {
103184 treeData : this . props . treeData ,
@@ -198,7 +279,18 @@ class ReactSortableTree extends Component {
198279 innerStyle,
199280 rowHeight,
200281 } = this . props ;
201- const { rows } = this . state ;
282+ const {
283+ rows,
284+ searchMatches,
285+ searchFocusTreeIndex,
286+ } = this . state ;
287+
288+ // Get indices for rows that match the search conditions
289+ const matchIndices = { } ;
290+ searchMatches . forEach ( ( { treeIndex : tIndex } , i ) => { matchIndices [ tIndex ] = i ; } ) ;
291+
292+ // Seek to the focused search result if there is one specified
293+ const scrollToInfo = searchFocusTreeIndex !== null ? { scrollToIndex : searchFocusTreeIndex } : { } ;
202294
203295 return (
204296 < div
@@ -208,6 +300,8 @@ class ReactSortableTree extends Component {
208300 < AutoSizer >
209301 { ( { height, width} ) => (
210302 < List
303+ { ...scrollToInfo }
304+ scrollToAlignment = "start"
211305 className = { styles . virtualScrollOverride }
212306 width = { width }
213307 height = { height }
@@ -220,7 +314,8 @@ class ReactSortableTree extends Component {
220314 index ,
221315 key ,
222316 rowStyle ,
223- ( ) => ( rows [ index - 1 ] || null )
317+ ( ) => ( rows [ index - 1 ] || null ) ,
318+ matchIndices
224319 ) }
225320 />
226321 ) }
@@ -229,13 +324,19 @@ class ReactSortableTree extends Component {
229324 ) ;
230325 }
231326
232- renderRow ( { node, path, lowerSiblingCounts, treeIndex } , listIndex , key , style , getPrevRow ) {
327+ renderRow ( { node, path, lowerSiblingCounts, treeIndex } , listIndex , key , style , getPrevRow , matchIndices ) {
233328 const NodeContentRenderer = this . nodeContentRenderer ;
329+ const isSearchMatch = treeIndex in matchIndices ;
330+ const isSearchFocus = isSearchMatch &&
331+ matchIndices [ treeIndex ] === this . props . searchFocusOffset ;
332+
234333 const nodeProps = ! this . props . generateNodeProps ? { } : this . props . generateNodeProps ( {
235334 node,
236335 path,
237336 lowerSiblingCounts,
238337 treeIndex,
338+ isSearchMatch,
339+ isSearchFocus,
239340 } ) ;
240341
241342 return (
@@ -258,6 +359,8 @@ class ReactSortableTree extends Component {
258359 < NodeContentRenderer
259360 node = { node }
260361 path = { path }
362+ isSearchMatch = { isSearchMatch }
363+ isSearchFocus = { isSearchFocus }
261364 treeIndex = { treeIndex }
262365 startDrag = { this . startDrag }
263366 endDrag = { this . endDrag }
@@ -291,6 +394,12 @@ ReactSortableTree.propTypes = {
291394
292395 maxDepth : PropTypes . number ,
293396
397+ // Search stuff
398+ searchQuery : PropTypes . oneOfType ( [ PropTypes . number , PropTypes . string ] ) ,
399+ searchFocusOffset : PropTypes . number ,
400+ searchMethod : PropTypes . func ,
401+ searchFinishCallback : PropTypes . func , // eslint-disable-line react/no-unused-prop-types
402+
294403 nodeContentRenderer : PropTypes . any ,
295404 generateNodeProps : PropTypes . func ,
296405
@@ -305,6 +414,7 @@ ReactSortableTree.defaultProps = {
305414 innerStyle : { } ,
306415 scaffoldBlockPxWidth : 44 ,
307416 loadCollapsedLazyChildren : false ,
417+ searchQuery : null ,
308418} ;
309419
310420export default dndWrapRoot ( ReactSortableTree ) ;
0 commit comments