@@ -29,7 +29,7 @@ function getNodeDataAtTreeIndexOrNextIndex({
29
29
return { nextIndex : currentIndex + 1 } ;
30
30
}
31
31
32
- // Iterate over each child and their ancestors and return the
32
+ // Iterate over each child and their descendants and return the
33
33
// target node if childIndex reaches the targetIndex
34
34
let childIndex = currentIndex + 1 ;
35
35
const childCount = node . children . length ;
@@ -824,3 +824,132 @@ export function getDepth(node, depth = 0) {
824
824
depth
825
825
) ;
826
826
}
827
+
828
+ /**
829
+ * Find nodes matching a search query in the tree,
830
+ *
831
+ * @param {!function } getNodeKey - Function to get the key from the nodeData and tree index
832
+ * @param {!Object[] } treeData - Tree data
833
+ * @param {?string|number } searchQuery - Function returning a boolean to indicate whether the node is a match or not
834
+ * @param {!function } searchMethod - Function returning a boolean to indicate whether the node is a match or not
835
+ * @param {?number } searchFocusOffset - The offset of the match to focus on
836
+ * (e.g., 0 focuses on the first match, 1 on the second)
837
+ * @param {boolean= } expandAllMatchPaths - If true, expands the paths to any matched node
838
+ * @param {boolean= } expandFocusMatchPaths - If true, expands the path to the focused node
839
+ *
840
+ * @return {Object[] } matches - An array of objects containing the matching `node`s, their `path`s and `treeIndex`s
841
+ * @return {Object[] } treeData - The original tree data with all relevant nodes expanded.
842
+ * If expandAllMatchPaths and expandFocusMatchPaths are both false,
843
+ * it will be the same as the original tree data.
844
+ */
845
+ export function find ( {
846
+ getNodeKey,
847
+ treeData,
848
+ searchQuery,
849
+ searchMethod,
850
+ searchFocusOffset,
851
+ expandAllMatchPaths = false ,
852
+ expandFocusMatchPaths = true ,
853
+ } ) {
854
+ const matches = [ ] ;
855
+
856
+ const trav = ( {
857
+ isPseudoRoot = false ,
858
+ node,
859
+ currentIndex,
860
+ path = [ ] ,
861
+ } ) => {
862
+ let hasMatch = false ;
863
+ let hasFocusMatch = false ;
864
+ // The pseudo-root is not considered in the path
865
+ const selfPath = isPseudoRoot ? [ ] : [
866
+ ...path ,
867
+ getNodeKey ( { node, treeIndex : currentIndex } ) ,
868
+ ] ;
869
+ const extraInfo = isPseudoRoot ? null : {
870
+ path : selfPath ,
871
+ treeIndex : currentIndex ,
872
+ } ;
873
+
874
+ // Nodes with with children that aren't lazy
875
+ const hasChildren = node . children &&
876
+ typeof node . children !== 'function' &&
877
+ node . children . length > 0 ;
878
+
879
+ let childIndex = currentIndex ;
880
+ const newNode = { ...node } ;
881
+
882
+ // Examine the current node to see if it is a match
883
+ if ( ! isPseudoRoot && searchMethod ( { ...extraInfo , node : newNode , searchQuery } ) ) {
884
+ hasMatch = true ;
885
+ if ( matches . length === searchFocusOffset ) {
886
+ hasFocusMatch = true ;
887
+ if ( ( expandAllMatchPaths || expandFocusMatchPaths ) && hasChildren ) {
888
+ newNode . expanded = true ;
889
+ }
890
+ }
891
+
892
+ matches . push ( { ...extraInfo , node : newNode } ) ;
893
+ }
894
+
895
+ if ( hasChildren ) {
896
+ // Get all descendants
897
+ newNode . children = newNode . children . map ( ( child ) => {
898
+ const mapResult = trav ( {
899
+ node : child ,
900
+ currentIndex : childIndex + 1 ,
901
+ path : selfPath ,
902
+ } ) ;
903
+
904
+ // Ignore hidden nodes by only advancing the index counter to the returned treeIndex
905
+ // if the child is expanded.
906
+ //
907
+ // The child could have been expanded from the start,
908
+ // or expanded due to a matching node being found in its descendants
909
+ if ( mapResult . node . expanded ) {
910
+ childIndex = mapResult . treeIndex ;
911
+ } else {
912
+ childIndex += 1 ;
913
+ }
914
+
915
+ if ( mapResult . hasMatch || mapResult . hasFocusMatch ) {
916
+ hasMatch = true ;
917
+
918
+ if ( mapResult . hasFocusMatch ) {
919
+ hasFocusMatch = true ;
920
+ }
921
+
922
+ // Expand the current node if it has descendants matching the search
923
+ // and the settings are set to do so.
924
+ if ( ( expandAllMatchPaths && mapResult . hasMatch ) ||
925
+ ( ( expandAllMatchPaths || expandFocusMatchPaths ) && mapResult . hasFocusMatch )
926
+ ) {
927
+ newNode . expanded = true ;
928
+ }
929
+ }
930
+
931
+ return mapResult . node ;
932
+ } ) ;
933
+ } else {
934
+ childIndex += 1 ;
935
+ }
936
+
937
+ return {
938
+ node : hasMatch ? newNode : node ,
939
+ hasMatch,
940
+ hasFocusMatch,
941
+ treeIndex : childIndex ,
942
+ } ;
943
+ } ;
944
+
945
+ const result = trav ( {
946
+ node : { children : treeData } ,
947
+ isPseudoRoot : true ,
948
+ currentIndex : - 1 ,
949
+ } ) ;
950
+
951
+ return {
952
+ matches,
953
+ treeData : result . node . children ,
954
+ } ;
955
+ }
0 commit comments