55 */
66
77import { BlockSvg } from '../block_svg.js' ;
8+ import { ConnectionType } from '../connection_type.js' ;
89import type { Field } from '../field.js' ;
10+ import type { Icon } from '../icons/icon.js' ;
911import type { IFocusableNode } from '../interfaces/i_focusable_node.js' ;
1012import type { INavigationPolicy } from '../interfaces/i_navigation_policy.js' ;
13+ import { RenderedConnection } from '../rendered_connection.js' ;
1114import { WorkspaceSvg } from '../workspace_svg.js' ;
1215
1316/**
@@ -21,21 +24,8 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
2124 * @returns The first field or input of the given block, if any.
2225 */
2326 getFirstChild ( current : BlockSvg ) : IFocusableNode | null {
24- const icons = current . getIcons ( ) ;
25- if ( icons . length ) return icons [ 0 ] ;
26-
27- for ( const input of current . inputList ) {
28- if ( ! input . isVisible ( ) ) {
29- continue ;
30- }
31- for ( const field of input . fieldRow ) {
32- return field ;
33- }
34- if ( input . connection ?. targetBlock ( ) )
35- return input . connection . targetBlock ( ) as BlockSvg ;
36- }
37-
38- return null ;
27+ const candidates = getBlockNavigationCandidates ( current ) ;
28+ return candidates [ 0 ] ;
3929 }
4030
4131 /**
@@ -66,36 +56,10 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
6656 getNextSibling ( current : BlockSvg ) : IFocusableNode | null {
6757 if ( current . nextConnection ?. targetBlock ( ) ) {
6858 return current . nextConnection ?. targetBlock ( ) ;
69- }
70-
71- const parent = this . getParent ( current ) ;
72- let navigatingCrossStacks = false ;
73- let siblings : ( BlockSvg | Field ) [ ] = [ ] ;
74- if ( parent instanceof BlockSvg ) {
75- for ( let i = 0 , input ; ( input = parent . inputList [ i ] ) ; i ++ ) {
76- if ( ! input . isVisible ( ) ) {
77- continue ;
78- }
79- siblings . push ( ...input . fieldRow ) ;
80- const child = input . connection ?. targetBlock ( ) ;
81- if ( child ) {
82- siblings . push ( child as BlockSvg ) ;
83- }
84- }
85- } else if ( parent instanceof WorkspaceSvg ) {
86- siblings = parent . getTopBlocks ( true ) ;
87- navigatingCrossStacks = true ;
88- } else {
89- return null ;
90- }
91-
92- const currentIndex = siblings . indexOf (
93- navigatingCrossStacks ? current . getRootBlock ( ) : current ,
94- ) ;
95- if ( currentIndex >= 0 && currentIndex < siblings . length - 1 ) {
96- return siblings [ currentIndex + 1 ] ;
97- } else if ( currentIndex === siblings . length - 1 && navigatingCrossStacks ) {
98- return siblings [ 0 ] ;
59+ } else if ( current . outputConnection ?. targetBlock ( ) ) {
60+ return navigateBlock ( current , 1 ) ;
61+ } else if ( this . getParent ( current ) instanceof WorkspaceSvg ) {
62+ return navigateStacks ( current , 1 ) ;
9963 }
10064
10165 return null ;
@@ -111,44 +75,13 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
11175 getPreviousSibling ( current : BlockSvg ) : IFocusableNode | null {
11276 if ( current . previousConnection ?. targetBlock ( ) ) {
11377 return current . previousConnection ?. targetBlock ( ) ;
78+ } else if ( current . outputConnection ?. targetBlock ( ) ) {
79+ return navigateBlock ( current , - 1 ) ;
80+ } else if ( this . getParent ( current ) instanceof WorkspaceSvg ) {
81+ return navigateStacks ( current , - 1 ) ;
11482 }
11583
116- const parent = this . getParent ( current ) ;
117- let navigatingCrossStacks = false ;
118- let siblings : ( BlockSvg | Field ) [ ] = [ ] ;
119- if ( parent instanceof BlockSvg ) {
120- for ( let i = 0 , input ; ( input = parent . inputList [ i ] ) ; i ++ ) {
121- if ( ! input . isVisible ( ) ) {
122- continue ;
123- }
124- siblings . push ( ...input . fieldRow ) ;
125- const child = input . connection ?. targetBlock ( ) ;
126- if ( child ) {
127- siblings . push ( child as BlockSvg ) ;
128- }
129- }
130- } else if ( parent instanceof WorkspaceSvg ) {
131- siblings = parent . getTopBlocks ( true ) ;
132- navigatingCrossStacks = true ;
133- } else {
134- return null ;
135- }
136-
137- const currentIndex = siblings . indexOf ( current ) ;
138- let result : IFocusableNode | null = null ;
139- if ( currentIndex >= 1 ) {
140- result = siblings [ currentIndex - 1 ] ;
141- } else if ( currentIndex === 0 && navigatingCrossStacks ) {
142- result = siblings [ siblings . length - 1 ] ;
143- }
144-
145- // If navigating to a previous stack, our previous sibling is the last
146- // block in it.
147- if ( navigatingCrossStacks && result instanceof BlockSvg ) {
148- return result . lastConnectionInStack ( false ) ?. getSourceBlock ( ) ?? result ;
149- }
150-
151- return result ;
84+ return null ;
15285 }
15386
15487 /**
@@ -171,3 +104,88 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
171104 return current instanceof BlockSvg ;
172105 }
173106}
107+
108+ /**
109+ * Returns a list of the navigable children of the given block.
110+ *
111+ * @param block The block to retrieve the navigable children of.
112+ * @returns A list of navigable/focusable children of the given block.
113+ */
114+ function getBlockNavigationCandidates ( block : BlockSvg ) : IFocusableNode [ ] {
115+ const candidates : IFocusableNode [ ] = block . getIcons ( ) ;
116+
117+ for ( const input of block . inputList ) {
118+ if ( ! input . isVisible ( ) ) continue ;
119+ candidates . push ( ...input . fieldRow ) ;
120+ if ( input . connection ?. targetBlock ( ) ) {
121+ candidates . push ( input . connection . targetBlock ( ) as BlockSvg ) ;
122+ } else if ( input . connection ?. type === ConnectionType . INPUT_VALUE ) {
123+ candidates . push ( input . connection as RenderedConnection ) ;
124+ }
125+ }
126+
127+ return candidates ;
128+ }
129+
130+ /**
131+ * Returns the next/previous stack relative to the given block's stack.
132+ *
133+ * @param current The block whose stack will be navigated relative to.
134+ * @param delta The difference in index to navigate; positive values navigate
135+ * to the nth next stack, while negative values navigate to the nth previous
136+ * stack.
137+ * @returns The first block in the stack offset by `delta` relative to the
138+ * current block's stack, or the last block in the stack offset by `delta`
139+ * relative to the current block's stack when navigating backwards.
140+ */
141+ export function navigateStacks ( current : BlockSvg , delta : number ) {
142+ const stacks = current . workspace . getTopBlocks ( true ) ;
143+ const currentIndex = stacks . indexOf ( current . getRootBlock ( ) ) ;
144+ const targetIndex = currentIndex + delta ;
145+ let result : BlockSvg | null = null ;
146+ if ( targetIndex >= 0 && targetIndex < stacks . length ) {
147+ result = stacks [ targetIndex ] ;
148+ } else if ( targetIndex < 0 ) {
149+ result = stacks [ stacks . length - 1 ] ;
150+ } else if ( targetIndex >= stacks . length ) {
151+ result = stacks [ 0 ] ;
152+ }
153+
154+ // When navigating to a previous stack, our previous sibling is the last
155+ // block in it.
156+ if ( delta < 0 && result ) {
157+ return result . lastConnectionInStack ( false ) ?. getSourceBlock ( ) ?? result ;
158+ }
159+
160+ return result ;
161+ }
162+
163+ /**
164+ * Returns the next navigable item relative to the provided block child.
165+ *
166+ * @param current The navigable block child item to navigate relative to.
167+ * @param delta The difference in index to navigate; positive values navigate
168+ * forward by n, while negative values navigate backwards by n.
169+ * @returns The navigable block child offset by `delta` relative to `current`.
170+ */
171+ export function navigateBlock (
172+ current : Icon | Field | RenderedConnection | BlockSvg ,
173+ delta : number ,
174+ ) : IFocusableNode | null {
175+ const block =
176+ current instanceof BlockSvg
177+ ? current . outputConnection . targetBlock ( )
178+ : current . getSourceBlock ( ) ;
179+ if ( ! ( block instanceof BlockSvg ) ) return null ;
180+
181+ const candidates = getBlockNavigationCandidates ( block ) ;
182+ const currentIndex = candidates . indexOf ( current ) ;
183+ if ( currentIndex === - 1 ) return null ;
184+
185+ const targetIndex = currentIndex + delta ;
186+ if ( targetIndex >= 0 && targetIndex < candidates . length ) {
187+ return candidates [ targetIndex ] ;
188+ }
189+
190+ return null ;
191+ }
0 commit comments