1+ import * as Blockly from "blockly" ;
2+ import { FieldDropdown } from "./field_dropdown" ;
3+ import { PointerCoords , UserInputAction } from "./field_utils" ;
4+
5+
6+ export abstract class FieldDropdownGrid extends FieldDropdown {
7+ public isFieldCustom_ = true ;
8+ // Width in pixels
9+ protected width_ : number ;
10+ // Columns in grid
11+ protected columns_ : number ;
12+ // Number of rows to display (if there are extra rows, the grid will be scrollable)
13+ protected maxRows_ : number ;
14+ protected backgroundColour_ : string ;
15+ protected borderColour_ : string ;
16+
17+ // Properties for grid keyboard controls
18+ protected activeDescendantIndex : number | undefined ;
19+ protected gridItems : HTMLDivElement [ ] = [ ] ;
20+ protected openingPointerCoords : PointerCoords | undefined ;
21+ protected lastUserInputAction : UserInputAction | undefined ;
22+ protected keyDownBinding : Blockly . browserEvents . Data | null = null ;
23+ protected pointerMoveBinding : Blockly . browserEvents . Data | null = null ;
24+
25+ /**
26+ * Callback for when a grid item is clicked inside the dropdown or widget div.
27+ * Should be bound to the FieldIconMenu.
28+ * @param {string | null } value the value to set for the field
29+ * @protected
30+ */
31+ protected abstract buttonClickAndClose_ ( value : string | null ) : void ;
32+
33+ /**
34+ * Callback for when a grid item is highlighted using keyboard navigation.
35+ * Use this method to classNames for grid items and scroll to the highlighted item if required.
36+ * @param {HTMLElement } gridItemContainer the HTMLElement containing the grid items
37+ * @protected
38+ */
39+ protected abstract setFocusedItem_ ( gridItemContainer : HTMLElement ) : void ;
40+
41+ private setFocusedItem ( gridItemContainer : HTMLElement , e : KeyboardEvent ) {
42+ this . lastUserInputAction = 'keymove' ;
43+ this . setFocusedItem_ ( gridItemContainer ) ;
44+ gridItemContainer . setAttribute ( 'aria-activedescendant' , ":" + this . activeDescendantIndex ) ;
45+ e . preventDefault ( ) ;
46+ e . stopPropagation ( ) ;
47+ }
48+
49+ /**
50+ * Set openingPointerCoords if the Event is a PointerEvent.
51+ * @param {Event } e the event that triggered showEditor_
52+ * @protected
53+ */
54+ protected setOpeningPointerCoords ( e : Event ) {
55+ if ( ! e ) {
56+ return ;
57+ }
58+ const { pageX, pageY} = e as PointerEvent ;
59+ if ( pageX !== undefined && pageY !== undefined ) {
60+ this . openingPointerCoords = {
61+ x : pageX ,
62+ y : pageY
63+ }
64+ }
65+ }
66+
67+ protected addKeyDownHandler ( gridItemContainer : HTMLElement ) {
68+ this . keyDownBinding = Blockly . browserEvents . bind ( gridItemContainer , 'keydown' , this , ( e : KeyboardEvent ) => {
69+ if ( this . activeDescendantIndex === undefined ) {
70+ if ( e . code === 'ArrowDown' || e . code === 'ArrowRight' || e . code === 'Home' ) {
71+ this . activeDescendantIndex = 0 ;
72+ return this . setFocusedItem ( gridItemContainer , e ) ;
73+ } else if ( e . code === 'ArrowUp' || e . code === 'ArrowLeft' || e . code === 'End' ) {
74+ this . activeDescendantIndex = this . gridItems . length - 1 ;
75+ return this . setFocusedItem ( gridItemContainer , e ) ;
76+ }
77+ }
78+
79+ const ctrlCmd = pxt . BrowserUtils . isMac ( ) ? e . metaKey : e . ctrlKey ;
80+ switch ( e . code ) {
81+ case 'ArrowUp' :
82+ if ( this . activeDescendantIndex - this . columns_ >= 0 ) {
83+ this . activeDescendantIndex -= this . columns_ ;
84+ }
85+ break ;
86+ case 'ArrowDown' :
87+ if ( this . activeDescendantIndex + this . columns_ < this . gridItems . length ) {
88+ this . activeDescendantIndex += this . columns_ ;
89+ }
90+ break ;
91+ case 'ArrowRight' :
92+ if ( this . activeDescendantIndex < this . gridItems . length - 1 ) {
93+ this . activeDescendantIndex ++ ;
94+ }
95+ break ;
96+ case 'ArrowLeft' :
97+ if ( this . activeDescendantIndex !== 0 ) {
98+ this . activeDescendantIndex -- ;
99+ }
100+ break ;
101+ case "Home" : {
102+ if ( ctrlCmd ) {
103+ this . activeDescendantIndex = 0 ;
104+ } else {
105+ while ( this . activeDescendantIndex % this . columns_ !== 0 ) {
106+ this . activeDescendantIndex -- ;
107+ }
108+ }
109+ break ;
110+ }
111+ case "End" : {
112+ if ( ctrlCmd ) {
113+ this . activeDescendantIndex = this . gridItems . length - 1 ;
114+ } else {
115+ while (
116+ this . activeDescendantIndex % this . columns_ !== this . columns_ - 1 &&
117+ this . activeDescendantIndex < this . gridItems . length - 1
118+ ) {
119+ this . activeDescendantIndex ++ ;
120+ }
121+ }
122+ break ;
123+ }
124+ case "Enter" :
125+ case "Space" : {
126+ this . buttonClickAndClose_ ( this . gridItems [ this . activeDescendantIndex ] . getAttribute ( 'data-value' ) ) ;
127+ e . preventDefault ( ) ;
128+ e . stopPropagation ( ) ;
129+ return ;
130+ }
131+ default : {
132+ return ;
133+ }
134+ }
135+ this . setFocusedItem ( gridItemContainer , e ) ;
136+ } ) ;
137+ }
138+
139+ protected addPointerListener ( parentDiv : HTMLElement ) {
140+ this . pointerMoveBinding = Blockly . browserEvents . bind ( parentDiv , 'pointermove' , this , ( ) => {
141+ this . lastUserInputAction = 'pointermove' ;
142+ } ) ;
143+ }
144+
145+ protected pointerMoveTriggeredByUser ( ) {
146+ return this . openingPointerCoords && ! this . lastUserInputAction || this . lastUserInputAction === 'pointermove' ;
147+ }
148+
149+ protected pointerOutTriggeredByUser ( ) {
150+ return this . lastUserInputAction === 'pointermove' ;
151+ }
152+
153+ protected disposeGrid ( ) : void {
154+ if ( this . keyDownBinding ) {
155+ Blockly . browserEvents . unbind ( this . keyDownBinding ) ;
156+ }
157+ if ( this . pointerMoveBinding ) {
158+ Blockly . browserEvents . unbind ( this . pointerMoveBinding ) ;
159+ }
160+ this . keyDownBinding = null ;
161+ this . pointerMoveBinding = null ;
162+ this . openingPointerCoords = undefined ;
163+ this . lastUserInputAction = undefined ;
164+ this . activeDescendantIndex = undefined ;
165+ this . gridItems = [ ] ;
166+ }
167+ }
0 commit comments