@@ -39,6 +39,7 @@ interface Props {
3939 toolbarStickyTop ?: number ;
4040 renderToolbar ?: ( props : ToolbarProps ) => React . ReactNode ;
4141 collapseIconType ?: CollapseIconType ;
42+ searchInCollapsed ?: boolean ;
4243}
4344
4445interface State {
@@ -50,6 +51,7 @@ interface State {
5051 filter : string ;
5152 matchIndex : number ;
5253 matchedRows : Array < number > ;
54+ allMatchPaths : Array < string > ;
5355 fullValue ?: {
5456 value : UnipikaFlattenTreeItem [ 'value' ] ;
5557 searchInfo ?: SearchInfo ;
@@ -61,27 +63,49 @@ function calculateState(
6163 collapsedState : CollapsedState ,
6264 filter : string ,
6365 settings : UnipikaSettings ,
66+ searchInCollapsed ?: boolean ,
6467) {
6568 const flattenResult = flattenUnipika ( value , {
6669 isJson : settings . format !== 'yson' ,
6770 collapsedState : collapsedState ,
6871 filter,
6972 settings : settings ,
73+ searchInCollapsed,
7074 } ) ;
7175
76+ const allMatchPaths = flattenResult . allMatchPaths || [ ] ;
77+ // Calculate hiddenMatches for collapsed nodes if searchInCollapsed is enabled
78+ if ( searchInCollapsed && allMatchPaths . length > 0 ) {
79+ // Count matches that are inside or at collapsed nodes
80+ flattenResult . data . forEach ( ( item ) => {
81+ if ( item . collapsed && item . path ) {
82+ const prefix = item . path + '/' ;
83+ const count = allMatchPaths . filter ( ( matchPath ) => {
84+ // Match if path is exactly the item path (match at the node itself)
85+ // or starts with prefix (match inside the node)
86+ return matchPath === item . path || matchPath . startsWith ( prefix ) ;
87+ } ) . length ;
88+ if ( count > 0 ) {
89+ item . hiddenMatches = count ;
90+ }
91+ }
92+ } ) ;
93+ }
94+
7295 return Object . assign (
7396 { } ,
7497 {
7598 flattenResult,
7699 matchedRows : Object . keys ( flattenResult . searchIndex ) . map ( Number ) ,
100+ allMatchPaths,
77101 } ,
78102 ) ;
79103}
80104
81105export class StructuredYson extends React . PureComponent < Props , State > {
82106 static getDerivedStateFromProps ( props : Props , state : State ) {
83107 const { value : prevValue , settings : prevSettings , yson : prevYson } = state ;
84- const { value, settings} = props ;
108+ const { value, settings, searchInCollapsed } = props ;
85109 const res : Partial < State > = { } ;
86110 const yson = settings . format === 'yson' ;
87111 if ( prevSettings !== settings || yson !== prevYson ) {
@@ -93,7 +117,13 @@ export class StructuredYson extends React.PureComponent<Props, State> {
93117 if ( prevValue !== value || ! isEmpty_ ( res ) ) {
94118 Object . assign < Partial < State > , Partial < State > > ( res , {
95119 value,
96- ...calculateState ( value , state . collapsedState , state . filter , settings ) ,
120+ ...calculateState (
121+ value ,
122+ state . collapsedState ,
123+ state . filter ,
124+ settings ,
125+ searchInCollapsed ,
126+ ) ,
97127 } ) ;
98128 }
99129 return isEmpty_ ( res ) ? null : res ;
@@ -109,6 +139,7 @@ export class StructuredYson extends React.PureComponent<Props, State> {
109139 filter : '' ,
110140 matchIndex : - 1 ,
111141 matchedRows : [ ] ,
142+ allMatchPaths : [ ] ,
112143 } ;
113144
114145 tableRef : TableProps [ 'scrollToRef' ] = React . createRef ( ) ;
@@ -131,6 +162,7 @@ export class StructuredYson extends React.PureComponent<Props, State> {
131162 cb ?: ( ) => void ,
132163 ) {
133164 const { value, settings} = this . state ;
165+ const { searchInCollapsed} = this . props ;
134166 const {
135167 collapsedState = this . state . collapsedState ,
136168 matchIndex = this . state . matchIndex ,
@@ -142,7 +174,7 @@ export class StructuredYson extends React.PureComponent<Props, State> {
142174 collapsedState,
143175 filter,
144176 matchIndex,
145- ...calculateState ( value , collapsedState , filter , settings ) ,
177+ ...calculateState ( value , collapsedState , filter , settings , searchInCollapsed ) ,
146178 } ,
147179 cb ,
148180 ) ;
@@ -199,23 +231,62 @@ export class StructuredYson extends React.PureComponent<Props, State> {
199231 } ) ;
200232 } ;
201233
234+ findCollapsedParent = ( matchPath : string , collapsedState : CollapsedState ) : string | null => {
235+ const pathParts = matchPath . split ( '/' ) ;
236+ for ( let i = 1 ; i <= pathParts . length ; i ++ ) {
237+ const checkPath = pathParts . slice ( 0 , i ) . join ( '/' ) ;
238+ if ( collapsedState [ checkPath ] ) {
239+ return checkPath ;
240+ }
241+ }
242+ return null ;
243+ } ;
244+
202245 onNextMatch = ( _event : unknown , diff = 1 ) => {
203- const { matchIndex, matchedRows} = this . state ;
204- if ( isEmpty_ ( matchedRows ) ) {
246+ const { matchIndex, matchedRows, allMatchPaths, collapsedState} = this . state ;
247+
248+ const totalMatches = allMatchPaths ?. length || 0 ;
249+
250+ if ( totalMatches === 0 ) {
205251 return ;
206252 }
207253
208- let index = ( matchIndex + diff ) % matchedRows . length ;
209- if ( index < 0 ) {
210- index = matchedRows . length + index ;
211- }
254+ // Calculate next index in total matches
255+ let nextTotalIndex = matchIndex + diff ;
256+ nextTotalIndex = ( ( nextTotalIndex % totalMatches ) + totalMatches ) % totalMatches ;
257+
258+ const targetMatchPath = allMatchPaths [ nextTotalIndex ] ;
259+
260+ const collapsedParent = this . findCollapsedParent ( targetMatchPath , collapsedState ) ;
261+
262+ // If target is not hidden, it's visible - navigate to it
263+ if ( collapsedParent === null ) {
264+ // Count how many visible matches are before our target
265+ let visibleMatchCount = 0 ;
266+ for ( let i = 0 ; i < nextTotalIndex ; i ++ ) {
267+ if ( this . findCollapsedParent ( allMatchPaths [ i ] , collapsedState ) === null ) {
268+ visibleMatchCount ++ ;
269+ }
270+ }
212271
213- if ( index !== matchIndex ) {
214- this . setState ( { matchIndex : index } ) ;
272+ // Navigate to the visible match
273+ if ( visibleMatchCount < matchedRows . length ) {
274+ this . setState ( { matchIndex : nextTotalIndex } ) ;
275+ this . tableRef . current ?. scrollToIndex ( matchedRows [ visibleMatchCount ] ) ;
276+ this . searchRef . current ?. focus ( ) ;
277+ }
278+ return ;
215279 }
216280
217- this . tableRef . current ?. scrollToIndex ( matchedRows [ index ] ) ;
218- this . searchRef . current ?. focus ( ) ;
281+ // Target is hidden - expand the collapsed parent
282+ const newCollapsedState = { ...collapsedState } ;
283+ delete newCollapsedState [ collapsedParent ] ;
284+
285+ // Recalculate state with new collapsed state
286+ this . updateState ( { collapsedState : newCollapsedState , matchIndex : nextTotalIndex } , ( ) => {
287+ // Retry navigation to the same target
288+ this . onNextMatch ( null , 0 ) ;
289+ } ) ;
219290 } ;
220291
221292 onPrevMatch = ( ) => {
@@ -233,7 +304,7 @@ export class StructuredYson extends React.PureComponent<Props, State> {
233304 } ;
234305
235306 renderToolbar ( className ?: string ) {
236- const { matchIndex, matchedRows, filter, collapsedState} = this . state ;
307+ const { matchIndex, matchedRows, filter, collapsedState, allMatchPaths } = this . state ;
237308 const { extraTools, renderToolbar} = this . props ;
238309
239310 // Calculate if there are any collapsed nodes
@@ -254,6 +325,7 @@ export class StructuredYson extends React.PureComponent<Props, State> {
254325 filter = { filter }
255326 matchIndex = { matchIndex }
256327 matchedRows = { matchedRows }
328+ allMatchPaths = { allMatchPaths }
257329 extraTools = { extraTools }
258330 onExpandAll = { this . onExpandAll }
259331 onCollapseAll = { this . onCollapseAll }
0 commit comments