1
1
import { EuiText , useEuiTheme } from '@elastic/eui' ;
2
2
import { SerializedStyles , css } from '@emotion/react' ;
3
- import { Ref , createRef , useEffect , useMemo , useRef , useState } from 'react' ;
3
+ import {
4
+ ReactNode ,
5
+ Ref ,
6
+ createRef ,
7
+ useCallback ,
8
+ useEffect ,
9
+ useMemo ,
10
+ useRef ,
11
+ useState ,
12
+ } from 'react' ;
4
13
import GridLayout , { Layout } from 'react-grid-layout' ;
5
14
import { useWindowDimensions } from '../../hooks/window-dimensions' ;
15
+ import { LocalStorage } from '../../lib/local-storage' ;
6
16
import { GridItem } from '../grid-item' ;
7
17
8
- const Grid : React . FC = ( ) : JSX . Element => {
18
+ interface GridItemProps {
19
+ itemId : string ;
20
+ title : string ;
21
+ content : ReactNode ;
22
+ }
23
+
24
+ interface GridProps {
25
+ items : Array < GridItemProps > ;
26
+ }
27
+
28
+ const Grid : React . FC < GridProps > = ( props : GridProps ) : ReactNode => {
29
+ const { items } = props ;
30
+
9
31
const { euiTheme } = useEuiTheme ( ) ;
10
32
11
33
const windowDimensions = useWindowDimensions ( ) ;
@@ -44,65 +66,6 @@ const Grid: React.FC = (): JSX.Element => {
44
66
paddingRight : euiTheme . size . s ,
45
67
} ) ;
46
68
47
- /**
48
- * Define the initial layout state.
49
- *
50
- * The min width and height are used to prevent the grid item from being
51
- * resized so small that it's unusable and hides its title bar.
52
- *
53
- * TODO move the definition of the layout to a separate file
54
- * and pass this in as a grid prop
55
- * TODO load the layout from storage
56
- * TODO create an item per game window that is open (e.g. Room, Spells, etc)
57
- * and one of the properties should be the game window's title
58
- * and one of the properties should be the game window's text
59
- * Probably make the property another component to encapsulate use of rxjs
60
- * and then exposes a property that is the text so that when that changes
61
- * then the grid item will rerender.
62
- */
63
- type MyLayout = Array < Layout & { [ key : string ] : any } > ;
64
- const defaultLayout : MyLayout = [
65
- {
66
- i : 'a' ,
67
- x : 0 ,
68
- y : 0 ,
69
- w : 5 ,
70
- minW : 5 ,
71
- h : 10 ,
72
- minH : 2 ,
73
- // TODO the title and content should come from another variable
74
- // the coordinates should be part of a layout that gets saved/loaded
75
- // and the cross-ref between the two should be the key (`i` prop)
76
- // This is because the react nodes are not serializable.
77
- title : 'Room' ,
78
- content : < EuiText css = { gridItemTextStyles } > room room room</ EuiText > ,
79
- } ,
80
- {
81
- i : 'b' ,
82
- x : 5 ,
83
- y : 5 ,
84
- w : 5 ,
85
- minW : 5 ,
86
- h : 10 ,
87
- minH : 2 ,
88
- title : 'Spells' ,
89
- content : < EuiText css = { gridItemTextStyles } > spells spells spells</ EuiText > ,
90
- } ,
91
- {
92
- i : 'c' ,
93
- x : 10 ,
94
- y : 10 ,
95
- w : 5 ,
96
- minW : 5 ,
97
- h : 10 ,
98
- minH : 2 ,
99
- title : 'Combat' ,
100
- content : < EuiText css = { gridItemTextStyles } > combat combat combat</ EuiText > ,
101
- } ,
102
- ] ;
103
-
104
- const [ layout , setLayout ] = useState < MyLayout > ( defaultLayout ) ;
105
-
106
69
/**
107
70
* When grid items are resized the increment is based on the the layout size.
108
71
* Horizontal resize increments are based on the number of columns.
@@ -147,6 +110,75 @@ const Grid: React.FC = (): JSX.Element => {
147
110
}
148
111
} , [ windowDimensions ] ) ;
149
112
113
+ /**
114
+ * Load the layout from storage or build a default layout.
115
+ */
116
+ const buildDefaultLayout = ( ) : Array < Layout > => {
117
+ let layout = LocalStorage . get < Array < Layout > > ( 'layout' ) ;
118
+
119
+ if ( layout ) {
120
+ layout = layout . filter ( ( layoutItem ) => {
121
+ return items . find ( ( item ) => item . itemId === layoutItem . i ) ;
122
+ } ) ;
123
+ return layout ;
124
+ }
125
+
126
+ // We'll tile the items three per row.
127
+ const maxItemsPerRow = 3 ;
128
+
129
+ // The min width and height are used to prevent the grid item from being
130
+ //resized so small that it's unusable and hides its title bar.
131
+ const minWidth = 5 ;
132
+ const minHeight = 2 ;
133
+
134
+ // The number of columns and rows the item will span.
135
+ const defaultWidth = Math . floor ( gridMaxColumns / maxItemsPerRow ) ;
136
+ const defaultHeight = gridRowHeight ;
137
+
138
+ let rowOffset = 0 ;
139
+ let colOffset = 0 ;
140
+
141
+ layout = items . map ( ( item , index ) : Layout => {
142
+ // If time to move to next row then adjust the offsets.
143
+ if ( index > 0 && index % maxItemsPerRow === 0 ) {
144
+ rowOffset += gridRowHeight ;
145
+ colOffset = 0 ;
146
+ }
147
+
148
+ const newItem = {
149
+ i : item . itemId ,
150
+ x : defaultWidth * colOffset ,
151
+ y : rowOffset ,
152
+ w : defaultWidth ,
153
+ h : defaultHeight ,
154
+ minW : minWidth ,
155
+ minH : minHeight ,
156
+ } ;
157
+
158
+ colOffset += 1 ;
159
+
160
+ return newItem ;
161
+ } ) ;
162
+
163
+ return layout ;
164
+ } ;
165
+
166
+ const [ layout , setLayout ] = useState < Array < Layout > > ( buildDefaultLayout ) ;
167
+
168
+ // Save the layout when it changes in the grid.
169
+ const onLayoutChange = useCallback ( ( layout : Array < Layout > ) => {
170
+ setLayout ( layout ) ;
171
+ LocalStorage . set ( 'layout' , layout ) ;
172
+ } , [ ] ) ;
173
+
174
+ // Remove the item from the layout.
175
+ const onGridItemClose = useCallback ( ( itemId : string ) => {
176
+ const newLayout = layout . filter ( ( layoutItem ) => {
177
+ return layoutItem . i !== itemId ;
178
+ } ) ;
179
+ onLayoutChange ( newLayout ) ;
180
+ } , [ ] ) ;
181
+
150
182
/**
151
183
* Originally I called `useRef` in the children's `useMemo` hook below but
152
184
* that caused "Error: Rendered fewer hooks than expected" to be thrown.
@@ -168,14 +200,17 @@ const Grid: React.FC = (): JSX.Element => {
168
200
* https://github.com/react-grid-layout/react-grid-layout?tab=readme-ov-file#performance
169
201
*/
170
202
const children = useMemo ( ( ) => {
171
- return layout . map ( ( item , i ) => {
203
+ return layout . map ( ( layoutItem , i ) => {
204
+ const item = items . find ( ( item ) => item . itemId === layoutItem . i ) ;
172
205
return (
173
206
< GridItem
174
- key = { item . i }
207
+ key = { item ! . itemId }
175
208
ref = { childRefs . current [ i ] }
176
- titleBarText = { item . title }
209
+ itemId = { item ! . itemId }
210
+ titleBarText = { item ! . title }
211
+ onClose = { onGridItemClose }
177
212
>
178
- { item . content }
213
+ < EuiText css = { gridItemTextStyles } > { item ! . content } </ EuiText >
179
214
</ GridItem >
180
215
) ;
181
216
} ) ;
@@ -195,10 +230,8 @@ const Grid: React.FC = (): JSX.Element => {
195
230
// Provide nominal spacing between grid items.
196
231
// If this value changes then review the grid row height variables.
197
232
margin = { [ 1 , 1 ] }
198
- onLayoutChange = { ( layout ) => {
199
- // TODO save the layout to storage
200
- setLayout ( layout ) ;
201
- } }
233
+ // Handle each time the layout changes (e.g. an item is moved or resized)
234
+ onLayoutChange = { onLayoutChange }
202
235
// Allow items to be placed anywhere in the grid.
203
236
compactType = { null }
204
237
// Prevent items from overlapping or being pushed.
0 commit comments