1
+ <!DOCTYPE html>
2
+ < html lang ="en ">
3
+
4
+ < head >
5
+ < meta charset ="UTF-8 ">
6
+ < meta name ="viewport " content ="width=device-width, initial-scale=1.0 ">
7
+ < title > Excel.js</ title >
8
+ < style >
9
+ body {
10
+ font-family : system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI' , Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans' , 'Helvetica Neue' , sans-serif;
11
+ }
12
+
13
+ * ,
14
+ * ::before ,
15
+ * ::after {
16
+ box-sizing : border-box;
17
+ }
18
+
19
+ img {
20
+ max-width : 20px ;
21
+ height : auto;
22
+ }
23
+
24
+ table {
25
+ border-collapse : collapse;
26
+ }
27
+
28
+ thead ,
29
+ tr td : first-child {
30
+ background : # eee ;
31
+ }
32
+
33
+ th ,
34
+ td {
35
+ border : 1px solid # ccc ;
36
+ font-weight : normal;
37
+ font-size : 12px ;
38
+ text-align : center;
39
+ width : 64px ;
40
+ height : 20px ;
41
+ vertical-align : middle;
42
+ position : relative
43
+ }
44
+
45
+ /* td:active {
46
+ border-radius: 2px;
47
+ outline: 2px solid #09f;
48
+ } */
49
+
50
+ span ,
51
+ input {
52
+ position : absolute;
53
+ inset : 0 ;
54
+ vertical-align : middle;
55
+ display : inline-flex;
56
+ justify-content : center;
57
+ align-items : center;
58
+ }
59
+
60
+ input {
61
+ border : 0 ;
62
+ opacity : 0 ;
63
+ pointer-events : none;
64
+ width : 100% ;
65
+ border-radius : 2px ;
66
+
67
+ & : focus {
68
+ opacity : 1 ;
69
+ outline : 2px solid # 09f ;
70
+ }
71
+ }
72
+
73
+ .selected {
74
+ background : rgb (174 , 223 , 255 );
75
+ }
76
+
77
+ th .selected {
78
+ background : rgb (146 , 211 , 255 );
79
+ }
80
+ </ style >
81
+ < script type ="module ">
82
+ const $ = el => document . querySelector ( el )
83
+ const $$ = el => document . querySelectorAll ( el )
84
+
85
+ const $table = $ ( 'table' )
86
+ const $head = $ ( 'thead' )
87
+ const $body = $ ( 'tbody' )
88
+
89
+ const ROWS = 10
90
+ const COLUMNS = 5
91
+ const FIRST_CHAR_CODE = 65
92
+
93
+ const times = length => Array . from ( { length } , ( _ , i ) => i )
94
+ const getColumn = i => String . fromCharCode ( FIRST_CHAR_CODE + i )
95
+
96
+ let selectedColumn = null
97
+
98
+ let STATE = times ( COLUMNS )
99
+ . map ( i => times ( ROWS ) . map ( j => ( { computedValue : 0 , value : 0 } ) ) )
100
+
101
+ console . log ( STATE )
102
+
103
+ function updateCell ( { x, y, value } ) {
104
+ const newState = structuredClone ( STATE )
105
+ const constants = generateCellsConstants ( newState )
106
+
107
+ const cell = newState [ x ] [ y ]
108
+
109
+ cell . computedValue = computeValue ( value , constants ) // -> span
110
+ cell . value = value // -> input
111
+
112
+ newState [ x ] [ y ] = cell
113
+
114
+ computeAllCells ( newState , generateCellsConstants ( newState ) )
115
+
116
+ STATE = newState
117
+
118
+ renderSpreadSheet ( )
119
+ }
120
+
121
+ function generateCellsConstants ( cells ) {
122
+ return cells . map ( ( rows , x ) => {
123
+ return rows . map ( ( cell , y ) => {
124
+ const letter = getColumn ( x ) // -> A
125
+ const cellId = `${ letter } ${ y + 1 } ` // -> A1
126
+ return `const ${ cellId } = ${ cell . computedValue } ;`
127
+ } ) . join ( '\n' )
128
+ } ) . join ( '\n' )
129
+ }
130
+
131
+ function computeAllCells ( cells , constants ) {
132
+ console . log ( 'computeAllCells' )
133
+ cells . forEach ( ( rows , x ) => {
134
+ rows . forEach ( ( cell , y ) => {
135
+ const computedValue = computeValue ( cell . value , constants )
136
+ cell . computedValue = computedValue
137
+ } )
138
+ } )
139
+ }
140
+
141
+ function computeValue ( value , constants ) {
142
+ if ( typeof value === 'number' ) return value
143
+ if ( ! value . startsWith ( '=' ) ) return value
144
+
145
+ const formula = value . slice ( 1 )
146
+
147
+ let computedValue
148
+ try {
149
+ computedValue = eval ( `(() => {
150
+ ${ constants }
151
+ return ${ formula } ;
152
+ })()` )
153
+ } catch ( e ) {
154
+ computedValue = `!ERROR: ${ e . message } `
155
+ }
156
+
157
+ console . log ( { value, computedValue } )
158
+
159
+ return computedValue
160
+ }
161
+
162
+ const renderSpreadSheet = ( ) => {
163
+ const headerHTML = `<tr>
164
+ <th></th>
165
+ ${ times ( COLUMNS ) . map ( i => `<th>${ getColumn ( i ) } </th>` ) . join ( '' ) }
166
+ </tr>`
167
+
168
+ $head . innerHTML = headerHTML
169
+
170
+ const bodyHTML = times ( ROWS ) . map ( row => {
171
+ return `<tr>
172
+ <td>${ row + 1 } </td>
173
+ ${ times ( COLUMNS ) . map ( column => `
174
+ <td data-x="${ column } " data-y="${ row } ">
175
+ <span>${ STATE [ column ] [ row ] . computedValue } </span>
176
+ <input type="text" value="${ STATE [ column ] [ row ] . value } " />
177
+ </td>
178
+ ` ) . join ( '' ) }
179
+ </tr>`
180
+ } ) . join ( '' )
181
+
182
+ $body . innerHTML = bodyHTML
183
+ }
184
+
185
+ $body . addEventListener ( 'click' , event => {
186
+ const td = event . target . closest ( 'td' )
187
+ if ( ! td ) return
188
+
189
+ const { x, y } = td . dataset
190
+ const input = td . querySelector ( 'input' )
191
+ const span = td . querySelector ( 'span' )
192
+
193
+ const end = input . value . length
194
+ input . setSelectionRange ( end , end )
195
+ input . focus ( )
196
+
197
+ $$ ( '.selected' ) . forEach ( el => el . classList . remove ( 'selected' ) )
198
+ selectedColumn = null
199
+
200
+ input . addEventListener ( 'keydown' , ( event ) => {
201
+ if ( event . key === 'Enter' ) input . blur ( )
202
+ } )
203
+
204
+ input . addEventListener ( 'blur' , ( ) => {
205
+ console . log ( { value : input . value , state : STATE [ x ] [ y ] . value } )
206
+
207
+ if ( input . value === STATE [ x ] [ y ] . value ) return
208
+
209
+ updateCell ( { x, y, value : input . value } )
210
+ } , { once : true } )
211
+ } )
212
+
213
+ $head . addEventListener ( 'click' , event => {
214
+ const th = event . target . closest ( 'th' )
215
+ if ( ! th ) return
216
+
217
+ const x = [ ...th . parentNode . children ] . indexOf ( th )
218
+ if ( x <= 0 ) return
219
+
220
+ selectedColumn = x - 1
221
+
222
+ $$ ( '.selected' ) . forEach ( el => el . classList . remove ( 'selected' ) )
223
+ th . classList . add ( 'selected' )
224
+ $$ ( `tr td:nth-child(${ x + 1 } )` ) . forEach ( el => el . classList . add ( 'selected' ) )
225
+ } )
226
+
227
+ document . addEventListener ( 'keydown' , event => {
228
+ if ( event . key === 'Backspace' && selectedColumn !== null ) {
229
+ times ( ROWS ) . forEach ( row => {
230
+ updateCell ( { x : selectedColumn , y : row , value : '' } )
231
+ } )
232
+ renderSpreadSheet ( )
233
+ }
234
+ } )
235
+
236
+ document . addEventListener ( 'copy' , event => {
237
+ if ( selectedColumn !== null ) {
238
+ const columnValues = times ( ROWS ) . map ( row => {
239
+ return STATE [ selectedColumn ] [ row ] . computedValue
240
+ } )
241
+
242
+ event . clipboardData . setData ( 'text/plain' , columnValues . join ( '\n' ) )
243
+ event . preventDefault ( )
244
+ }
245
+ } )
246
+
247
+ document . addEventListener ( 'click' , event => {
248
+ const { target } = event
249
+
250
+ const isThClicked = target . closest ( 'th' )
251
+ const isTdClicked = target . closest ( 'td' )
252
+
253
+ if ( ! isThClicked && ! isTdClicked ) {
254
+ $$ ( '.selected' ) . forEach ( el => el . classList . remove ( 'selected' ) )
255
+ selectedColumn = null
256
+ }
257
+ } )
258
+
259
+ renderSpreadSheet ( )
260
+ </ script >
261
+ </ head >
262
+
263
+ < body >
264
+ < img
265
+ src ="https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Microsoft_Excel_2013-2019_logo.svg/1200px-Microsoft_Excel_2013-2019_logo.svg.png " />
266
+
267
+ < table >
268
+ < thead > </ thead >
269
+ < tbody > </ tbody >
270
+ </ table >
271
+ </ body >
272
+
273
+ </ html >
274
+
275
+
276
+ <!--
277
+
278
+ - Añadir la funconalidad de filas
279
+ - Haz la suma por rangos=A1:A20
280
+ - Seleccionar por celas
281
+
282
+
283
+ -->
0 commit comments