OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011 The Native Client Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 /** | |
6 * @file | |
7 * The stamp editor object. This manages a table whose cells represent cells | |
8 * of the stamp. Clicking on a table cell changes the state of the | |
9 * corresponding stamp cell. The table can be resized. | |
10 */ | |
11 | |
12 | |
13 goog.provide('stamp'); | |
14 goog.provide('stamp.Editor'); | |
15 | |
16 goog.require('goog.Disposable'); | |
17 goog.require('goog.dom'); | |
18 goog.require('goog.editor.Table'); | |
19 goog.require('goog.events'); | |
20 goog.require('goog.style'); | |
21 | |
22 /** | |
23 * Manages the data and interface for the stamp editor. | |
24 * @param {!Element} noteContainer The element under which DOM nodes for | |
25 * the stamp editor should be added. | |
26 * @constructor | |
27 * @extends {goog.Disposable} | |
28 */ | |
29 stamp.Editor = function() { | |
30 goog.Disposable.call(this); | |
31 }; | |
32 goog.inherits(stamp.Editor, goog.events.EventTarget); | |
33 | |
34 /** | |
35 * The table that represents the stamp. | |
36 * @type {goog.editor.Table} | |
37 * @private | |
38 */ | |
39 stamp.Editor.prototype.stampEditorTable_ = null; | |
40 | |
41 /** | |
42 * The minimum number of rows and columns in the stamp editor table. | |
43 */ | |
44 stamp.Editor.prototype.MIN_ROW_COUNT = 3; | |
45 stamp.Editor.prototype.MIN_COLUMN_COUNT = 3; | |
46 | |
47 /** | |
48 * Attributes added to cells to cache certain parameters like aliveState. | |
49 * @enum {string} | |
50 * @private | |
51 */ | |
52 stamp.Editor.CellAttributes_ = { | |
53 IS_ALIVE: 'isalive' // Whether the cell is alive or dead. | |
54 }; | |
55 | |
56 /** | |
57 * Characters used to encode the stamp as a string. | |
58 * @enum {string} | |
59 * @private | |
60 */ | |
61 stamp.Editor.StringEncoding_ = { | |
62 DEAD_CELL: '.', | |
63 END_OF_ROW: '\n', | |
64 LIVE_CELL: '*' | |
65 }; | |
66 | |
67 /** | |
68 * Override of disposeInternal() to dispose of retained objects and unhook all | |
69 * events. | |
70 * @override | |
71 */ | |
72 stamp.Editor.prototype.disposeInternal = function() { | |
73 var tableCells = | |
74 this.stampEditorTable_.element.getElementsByTagName(goog.dom.TagName.TD); | |
75 for (var i = 0; i < tableCells.length; ++i) { | |
76 goog.events.removeAll(tableCells[i]); | |
77 } | |
78 this.stampEditorTable_ = null; | |
79 stamp.Editor.superClass_.disposeInternal.call(this); | |
80 } | |
81 | |
82 /** | |
83 * Fills out the TABLE structure for the stamp editor. The stamp editor | |
84 * can be resized, and handles clicks in its cells by toggling their state. | |
85 * The resulting TABLE element will have the minumum number of rows and | |
86 * columns, and be filled in with a default stamp that creates a glider. | |
87 * @param {!Element<TABLE>} stampEditorTableElement The TABLE element that gets | |
88 * filled out with the editable cells. | |
89 * @private | |
90 */ | |
91 stamp.Editor.prototype.makeStampEditorDom = function(stampEditorTableElement) { | |
92 var domTable = goog.editor.Table.createDomTable( | |
93 document, | |
94 this.MIN_COLUMN_COUNT, | |
95 this.MIN_ROW_COUNT, | |
96 { 'borderWidth': 1, 'borderColor': 'white' }); | |
97 var tableStyle = { | |
98 'borderCollpase': 'collapse', | |
99 'borderSpacing': '0px', | |
100 'borderStyle': 'solid' | |
101 }; | |
102 | |
103 goog.style.setStyle(domTable, tableStyle); | |
104 var tableCells = | |
105 domTable.getElementsByTagName(goog.dom.TagName.TD); | |
106 this.initCells_(tableCells); | |
107 goog.dom.appendChild(stampEditorTableElement, domTable); | |
108 this.stampEditorTable_ = new goog.editor.Table(domTable); | |
109 } | |
110 | |
111 /** | |
112 * Initialize a list of cells to the "alive" state: sets the is-alive | |
113 * attribute and the enclosed image element. Fix up the various attributes | |
114 * that goog.editor.Table sets on cells. | |
115 * @param {!Array<Element>} cells The array of cells to initialize. | |
116 * @private | |
117 */ | |
118 stamp.Editor.prototype.initCells_ = function(cells) { | |
119 var cellStyle = { | |
120 'padding': '0px' | |
121 }; | |
122 for (var i = 0; i < cells.length; ++i) { | |
123 var cell = cells[i]; | |
124 // The goog.editor.Table functions set the cell widths to 60px. | |
125 cell.style.removeProperty('width'); | |
126 goog.style.setStyle(cell, cellStyle); | |
127 this.setCellIsAlive(cell, false); | |
128 goog.events.listen(cell, goog.events.EventType.CLICK, | |
129 this.toggleCellState_, false, this); | |
130 } | |
131 } | |
132 | |
133 /** | |
134 * Inspect the encoded stamp string and make sure it's valid. Add things like | |
135 * newline characters when necessary. |stampStringIn| is assumed to have | |
136 * length > 0. | |
137 * @param {!string} stampStringIn The input stamp string. Must have length > 0. | |
138 * @return {!string} The santized version of the input string. | |
139 * @private | |
140 */ | |
141 stamp.Editor.prototype.sanitizeStampString_ = function(stampStringIn) { | |
142 var stampString = stampStringIn; | |
143 if (stampString[stampString.length - 1] != | |
144 stamp.Editor.StringEncoding_.END_OF_ROW) { | |
145 stampString += stamp.Editor.StringEncoding_.END_OF_ROW; | |
146 } | |
147 return stampString; | |
148 } | |
149 | |
150 /** | |
151 * Compute a stamp size from an encoded stamp string. Stamps are assumed to be | |
152 * rectangular. The width is the length in characters of the first line in | |
153 * the stamp string. The height is the number of lines. | |
154 * @param {!string} stampString The encoded stamp string. Must have length > 0. | |
155 * @return {!Object} An object containing width and height. | |
156 * @private | |
157 */ | |
158 stamp.Editor.prototype.getSizeFromStampString_ = function(stampString) { | |
159 var size = {width: 0, height: 0}; | |
160 var eorPos = stampString.indexOf(stamp.Editor.StringEncoding_.END_OF_ROW); | |
161 if (eorPos == -1) { | |
162 // The stamp string is a single row. | |
163 size.width = stampString.length; | |
164 size.height = 1; | |
165 } else { | |
166 size.width = eorPos; | |
167 // Count up the number of rows in the encoded string. | |
168 var rowCount = 0; | |
169 do { | |
170 ++rowCount; | |
171 eorPos = stampString.indexOf(stamp.Editor.StringEncoding_.END_OF_ROW, | |
172 eorPos + 1); | |
173 } while (eorPos != -1); | |
174 size.height = rowCount; | |
175 } | |
176 return size; | |
177 } | |
178 | |
179 /** | |
180 * Return the current stamp expressed as a string. The format loosely follows | |
181 * the .LIF 1.05 "spec", where rows are delineated by a \n, a live cell is | |
182 * represented by a '*' and a dead one by a '.'. | |
183 */ | |
184 stamp.Editor.prototype.getStampAsString = function() { | |
185 var stampString = ''; | |
186 var rowCount = this.rowCount(); | |
187 for (var rowIndex = 0; rowIndex < rowCount; ++rowIndex) { | |
188 var row = this.stampEditorTable_.rows[rowIndex]; | |
189 for (var colIndex = 0; colIndex < row.columns.length; ++colIndex) { | |
190 var cell = row.columns[colIndex]; | |
191 if (this.cellIsAlive(cell.element)) { | |
192 stampString += stamp.Editor.StringEncoding_.LIVE_CELL; | |
193 } else { | |
194 stampString += stamp.Editor.StringEncoding_.DEAD_CELL; | |
195 } | |
196 } | |
197 stampString += stamp.Editor.StringEncoding_.END_OF_ROW; | |
198 } | |
199 return stampString; | |
200 } | |
201 | |
202 /** | |
203 * Sets the current stamp baes on the stamp encoding in |stampString|. The | |
204 * format loosely follows the .LIF 1.05 "spec", where rows are delineated by a | |
205 * \n, a live cell is represented by a '*' and a dead one by a '.'. | |
206 * @param {!string} stampString The encoded stamp string. | |
207 */ | |
208 stamp.Editor.prototype.setStampFromString = function(stampStringIn) { | |
209 if (stampStringIn.length == 0) | |
210 return; // Error? | |
211 var stampString = this.sanitizeStampString_(stampStringIn); | |
212 var newSize = this.getSizeFromStampString_(stampString); | |
213 this.resize(newSize.width, newSize.height); | |
214 | |
215 // Set all the cells to "dead". | |
216 var tableCells = | |
217 this.stampEditorTable_.element.getElementsByTagName(goog.dom.TagName.TD); | |
218 this.initCells_(tableCells); | |
219 | |
220 // Parse the stamp string and set cell states. | |
221 var rowIndex = 0; | |
222 var columnIndex = 0; | |
223 for (var i = 0; i < stampString.length; ++i) { | |
224 var cell = this.domCellAt(rowIndex, columnIndex); | |
225 switch (stampString.charAt(i)) { | |
226 case stamp.Editor.StringEncoding_.DEAD_CELL: | |
227 this.setCellIsAlive(cell, false); | |
228 ++columnIndex; | |
229 break; | |
230 case stamp.Editor.StringEncoding_.LIVE_CELL: | |
231 this.setCellIsAlive(cell, true); | |
232 ++columnIndex; | |
233 break; | |
234 case stamp.Editor.StringEncoding_.END_OF_ROW: | |
235 ++rowIndex; | |
236 columnIndex = 0; | |
237 break; | |
238 default: | |
239 // Invalid character, set the cell to "dead". | |
240 this.setCellIsAlive(cell, false); | |
241 ++columnIndex; | |
242 break; | |
243 } | |
244 } | |
245 } | |
246 | |
247 /** | |
248 * Return the first TABLE cell element that contains |target|. Return null | |
249 * if there is no such enclosing element. | |
250 * @return {?Element} The DOM element (a TD) that contains |target|. | |
251 */ | |
252 stamp.Editor.prototype.enclosingTargetForElement = function(target) { | |
253 // The cell is the enclosing TD element. | |
254 var domCell = goog.dom.getAncestor(target, function(node) { | |
255 return node.tagName && node.tagName.toUpperCase() == goog.dom.TagName.TD; | |
256 }); | |
257 return domCell; | |
258 } | |
259 | |
260 /** | |
261 * Respond to a CLICK event in a table cell by toggling its state. | |
262 * @param {!goog.events.Event} clickEvent The CLICK event that triggered this | |
263 * handler. | |
264 * @private | |
265 */ | |
266 stamp.Editor.prototype.toggleCellState_ = function(clickEvent) { | |
267 var cell = this.enclosingTargetForElement(clickEvent.target); | |
268 if (!cell) | |
269 return; | |
270 // TODO(dspringer): throw an error or assert if no enclosing TD element is | |
271 // found. | |
272 this.setCellIsAlive(cell, !this.cellIsAlive(cell)); | |
273 } | |
274 | |
275 /** | |
276 * Return the DOM element for the cell at location (|row|, |column|) (this is | |
277 * usually a TD element). | |
278 * @param {number} rowIndex The row index. This is 0-based. | |
279 * @param {number} columnIndex The column index. This is 0-based. | |
280 * @return {?Element} The TD element representing the cell. If no cell exists | |
281 * then return null. | |
282 */ | |
283 stamp.Editor.prototype.domCellAt = function(rowIndex, columnIndex) { | |
284 if (rowIndex < 0 || rowIndex >= this.stampEditorTable_.rows.length) | |
285 return null; | |
286 var row = this.stampEditorTable_.rows[rowIndex]; | |
287 if (columnIndex < 0 || columnIndex >= row.columns.length) | |
288 return null; | |
289 return row.columns[columnIndex].element; | |
290 } | |
291 | |
292 /** | |
293 * Resize the table of cells to contain |width| columns and |height| rows. A | |
294 * 0 value for either dimension leaves that dimension unchanged. Both | |
295 * dimensions are clamped to the minumum and maximum row/column counts. | |
296 * @param {!number} width The new width, must be >= 0. | |
297 * @param {!number} height The new height, must be >= 0. | |
298 */ | |
299 stamp.Editor.prototype.resize = function(width, height) { | |
300 if (width < this.MIN_COLUMN_COUNT) { | |
301 width = this.MIN_COLUMN_COUNT; | |
302 } | |
303 if (height < this.MIN_ROW_COUNT) { | |
304 height = this.MIN_ROW_COUNT; | |
305 } | |
306 var currentWidth = this.columnCount(); | |
307 if (width > 0 && width != currentWidth) { | |
308 if (currentWidth < width) { | |
309 for (var col = 0; col < width - currentWidth; ++col) { | |
310 this.appendColumn(); | |
311 } | |
312 } else { | |
313 for (var col = 0; col < currentWidth - width; ++col) { | |
314 this.removeLastColumn(); | |
315 } | |
316 } | |
317 } | |
318 var currentHeight = this.rowCount(); | |
319 if (height > 0 && height != currentHeight) { | |
320 if (currentHeight < height) { | |
321 for (var row = 0; row < height - currentHeight; ++row) { | |
322 this.appendRow(); | |
323 } | |
324 } else { | |
325 for (var row = 0; row < currentHeight - height; ++row) { | |
326 this.removeLastRow(); | |
327 } | |
328 } | |
329 } | |
330 } | |
331 | |
332 /** | |
333 * Add a column at the right-end of the editor table. | |
334 */ | |
335 stamp.Editor.prototype.appendColumn = function() { | |
336 var newCells = this.stampEditorTable_.insertColumn(); | |
337 this.initCells_(newCells); | |
338 } | |
339 | |
340 /** | |
341 * Remove the last column of editor table cells. If the number of columns is | |
342 * already at the minumum, do nothing. | |
343 */ | |
344 stamp.Editor.prototype.removeLastColumn = function() { | |
345 var columnCount = this.columnCount(); | |
346 if (columnCount <= this.MIN_COLUMN_COUNT) { | |
347 return; | |
348 } | |
349 // Unhook all the listeners that have been attached to the cells in the | |
350 // last column, then remove the column. | |
351 for (var i = 0; i < this.stampEditorTable_.rows.length; ++i) { | |
352 var row = this.stampEditorTable_.rows[i]; | |
353 var cell = row.columns[columnCount - 1]; | |
354 goog.events.removeAll(cell); | |
355 } | |
356 this.stampEditorTable_.removeColumn(columnCount - 1); | |
357 } | |
358 | |
359 /** | |
360 * Return the number of columns in the stamp editor table. This assumes that | |
361 * there are no merged cells in row[0], and that the number of cells in all | |
362 * rows is the same as the length of row[0]. | |
363 * @return {int} The number of columns. | |
364 */ | |
365 stamp.Editor.prototype.columnCount = function() { | |
366 if (!this.stampEditorTable_) | |
367 return 0; | |
368 if (!this.stampEditorTable_.rows) | |
369 return 0; | |
370 if (!this.stampEditorTable_.rows[0].columns) | |
371 return 0; | |
372 return this.stampEditorTable_.rows[0].columns.length; | |
373 } | |
374 | |
375 /** | |
376 * Add a row at the bottom of the editor table. | |
377 */ | |
378 stamp.Editor.prototype.appendRow = function() { | |
379 var newTableRow = this.stampEditorTable_.insertRow(); | |
380 this.initCells_(goog.editor.Table.getChildCellElements(newTableRow)); | |
381 } | |
382 | |
383 /** | |
384 * Remove the last row of editor table cells. If the number of rows is already | |
385 * at the minumum, do nothing. | |
386 */ | |
387 stamp.Editor.prototype.removeLastRow = function() { | |
388 var rowCount = this.rowCount(); | |
389 if (rowCount <= this.MIN_ROW_COUNT) { | |
390 return; | |
391 } | |
392 // Unhook all the listeners that have been attached to the cells in the | |
393 // last row, then remove the row. | |
394 var lastRow = this.stampEditorTable_.rows[rowCount - 1]; | |
395 for (var i = 0; i < lastRow.columns.length; ++i) { | |
396 var cell = lastRow.columns[i]; | |
397 goog.events.removeAll(cell); | |
398 } | |
399 this.stampEditorTable_.removeRow(rowCount - 1); | |
400 } | |
401 | |
402 /** | |
403 * Return the number of rows in the stamp editor table. Assumes that there are | |
404 * no merged cells in any columns. | |
405 * @return {int} The number of rows. | |
406 */ | |
407 stamp.Editor.prototype.rowCount = function() { | |
408 if (!this.stampEditorTable_) | |
409 return 0; | |
410 if (!this.stampEditorTable_.rows) | |
411 return 0; | |
412 return this.stampEditorTable_.rows.length; | |
413 } | |
414 | |
415 /** | |
416 * Accessor for the is-alive state of a cell. | |
417 * @param {!Element} domCell The DOM element representing the target cell. | |
418 * @return {bool} The is-alive state of |cell|. | |
419 */ | |
420 stamp.Editor.prototype.cellIsAlive = function(domCell) { | |
421 isAlive = domCell.getAttribute(stamp.Editor.CellAttributes_.IS_ALIVE); | |
422 return isAlive != 'false'; | |
423 } | |
424 | |
425 /** | |
426 * Change the is-alive state of a cell to |isAlive|. The appearance of the cell | |
427 * is also changed to match the new state. | |
428 * @param {!Element} domCell The DOM element representing the target cell. | |
429 * @param {bool} isAlive The new is-alive state of the cell. | |
430 */ | |
431 stamp.Editor.prototype.setCellIsAlive = function(domCell, isAlive) { | |
432 domCell.setAttribute(stamp.Editor.CellAttributes_.IS_ALIVE, isAlive); | |
433 var cellImg = isAlive ? 'img/live_cell.png' : 'img/dead_cell.png'; | |
434 domCell.innerHTML = '<img src="' + | |
435 cellImg + | |
436 '" alt="Click to change state." />'; | |
437 } | |
OLD | NEW |