Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/editor/table.js
2868 views
1
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
// http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS-IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
/**
16
* @fileoverview Table editing support.
17
* This file provides the class goog.editor.Table and two
18
* supporting classes, goog.editor.TableRow and
19
* goog.editor.TableCell. Together these provide support for
20
* high level table modifications: Adding and deleting rows and columns,
21
* and merging and splitting cells.
22
*
23
*/
24
25
goog.provide('goog.editor.Table');
26
goog.provide('goog.editor.TableCell');
27
goog.provide('goog.editor.TableRow');
28
29
goog.require('goog.asserts');
30
goog.require('goog.dom');
31
goog.require('goog.dom.DomHelper');
32
goog.require('goog.dom.NodeType');
33
goog.require('goog.dom.TagName');
34
goog.require('goog.log');
35
goog.require('goog.string.Unicode');
36
goog.require('goog.style');
37
38
39
40
/**
41
* Class providing high level table editing functions.
42
* @param {Element} node Element that is a table or descendant of a table.
43
* @constructor
44
* @final
45
*/
46
goog.editor.Table = function(node) {
47
this.element =
48
goog.dom.getAncestorByTagNameAndClass(node, goog.dom.TagName.TABLE);
49
if (!this.element) {
50
goog.log.error(
51
this.logger_, "Can't create Table based on a node " +
52
"that isn't a table, or descended from a table.");
53
}
54
this.dom_ = goog.dom.getDomHelper(this.element);
55
this.refresh();
56
};
57
58
59
/**
60
* Logger object for debugging and error messages.
61
* @type {goog.log.Logger}
62
* @private
63
*/
64
goog.editor.Table.prototype.logger_ = goog.log.getLogger('goog.editor.Table');
65
66
67
/**
68
* Walks the dom structure of this object's table element and populates
69
* this.rows with goog.editor.TableRow objects. This is done initially
70
* to populate the internal data structures, and also after each time the
71
* DOM structure is modified. Currently this means that the all existing
72
* information is discarded and re-read from the DOM.
73
*/
74
// TODO(user): support partial refresh to save cost of full update
75
// every time there is a change to the DOM.
76
goog.editor.Table.prototype.refresh = function() {
77
var rows = this.rows = [];
78
var tbody = goog.dom.getElementsByTagName(
79
goog.dom.TagName.TBODY, goog.asserts.assert(this.element))[0];
80
if (!tbody) {
81
return;
82
}
83
var trs = [];
84
for (var child = tbody.firstChild; child; child = child.nextSibling) {
85
if (child.nodeName == goog.dom.TagName.TR) {
86
trs.push(child);
87
}
88
}
89
90
for (var rowNum = 0, tr; tr = trs[rowNum]; rowNum++) {
91
var existingRow = rows[rowNum];
92
var tds = goog.editor.Table.getChildCellElements(tr);
93
var columnNum = 0;
94
// A note on cellNum vs. columnNum: A cell is a td/th element. Cells may
95
// use colspan/rowspan to extend over multiple rows/columns. cellNum
96
// is the dom element number, columnNum is the logical column number.
97
for (var cellNum = 0, td; td = tds[cellNum]; cellNum++) {
98
// If there's already a cell extending into this column
99
// (due to that cell's colspan/rowspan), increment the column counter.
100
while (existingRow && existingRow.columns[columnNum]) {
101
columnNum++;
102
}
103
var cell = new goog.editor.TableCell(td, rowNum, columnNum);
104
// Place this cell in every row and column into which it extends.
105
for (var i = 0; i < cell.rowSpan; i++) {
106
var cellRowNum = rowNum + i;
107
// Create TableRow objects in this.rows as needed.
108
var cellRow = rows[cellRowNum];
109
if (!cellRow) {
110
// TODO(user): try to avoid second trs[] lookup.
111
rows.push(
112
cellRow = new goog.editor.TableRow(trs[cellRowNum], cellRowNum));
113
}
114
// Extend length of column array to make room for this cell.
115
var minimumColumnLength = columnNum + cell.colSpan;
116
if (cellRow.columns.length < minimumColumnLength) {
117
cellRow.columns.length = minimumColumnLength;
118
}
119
for (var j = 0; j < cell.colSpan; j++) {
120
var cellColumnNum = columnNum + j;
121
cellRow.columns[cellColumnNum] = cell;
122
}
123
}
124
columnNum += cell.colSpan;
125
}
126
}
127
};
128
129
130
/**
131
* Returns all child elements of a TR element that are of type TD or TH.
132
* @param {Element} tr TR element in which to find children.
133
* @return {!Array<Element>} array of child cell elements.
134
*/
135
goog.editor.Table.getChildCellElements = function(tr) {
136
var cells = [];
137
for (var i = 0, cell; cell = tr.childNodes[i]; i++) {
138
if (cell.nodeName == goog.dom.TagName.TD ||
139
cell.nodeName == goog.dom.TagName.TH) {
140
cells.push(cell);
141
}
142
}
143
return cells;
144
};
145
146
147
/**
148
* Inserts a new row in the table. The row will be populated with new
149
* cells, and existing rowspanned cells that overlap the new row will
150
* be extended.
151
* @param {number=} opt_rowIndex Index at which to insert the row. If
152
* this is omitted the row will be appended to the end of the table.
153
* @return {!Element} The new row.
154
*/
155
goog.editor.Table.prototype.insertRow = function(opt_rowIndex) {
156
var rowIndex =
157
goog.isDefAndNotNull(opt_rowIndex) ? opt_rowIndex : this.rows.length;
158
var refRow;
159
var insertAfter;
160
if (rowIndex == 0) {
161
refRow = this.rows[0];
162
insertAfter = false;
163
} else {
164
refRow = this.rows[rowIndex - 1];
165
insertAfter = true;
166
}
167
var newTr = this.dom_.createElement(goog.dom.TagName.TR);
168
for (var i = 0, cell; cell = refRow.columns[i]; i += 1) {
169
// Check whether the existing cell will span this new row.
170
// If so, instead of creating a new cell, extend
171
// the rowspan of the existing cell.
172
if ((insertAfter && cell.endRow > rowIndex) ||
173
(!insertAfter && cell.startRow < rowIndex)) {
174
cell.setRowSpan(cell.rowSpan + 1);
175
if (cell.colSpan > 1) {
176
i += cell.colSpan - 1;
177
}
178
} else {
179
newTr.appendChild(this.createEmptyTd());
180
}
181
if (insertAfter) {
182
goog.dom.insertSiblingAfter(newTr, refRow.element);
183
} else {
184
goog.dom.insertSiblingBefore(newTr, refRow.element);
185
}
186
}
187
this.refresh();
188
return newTr;
189
};
190
191
192
/**
193
* Inserts a new column in the table. The column will be created by
194
* inserting new TD elements in each row, or extending the colspan
195
* of existing TD elements.
196
* @param {number=} opt_colIndex Index at which to insert the column. If
197
* this is omitted the column will be appended to the right side of
198
* the table.
199
* @return {!Array<Element>} Array of new cell elements that were created
200
* to populate the new column.
201
*/
202
goog.editor.Table.prototype.insertColumn = function(opt_colIndex) {
203
// TODO(user): set column widths in a way that makes sense.
204
var colIndex = goog.isDefAndNotNull(opt_colIndex) ?
205
opt_colIndex :
206
(this.rows[0] && this.rows[0].columns.length) || 0;
207
var newTds = [];
208
for (var rowNum = 0, row; row = this.rows[rowNum]; rowNum++) {
209
var existingCell = row.columns[colIndex];
210
if (existingCell && existingCell.endCol >= colIndex &&
211
existingCell.startCol < colIndex) {
212
existingCell.setColSpan(existingCell.colSpan + 1);
213
rowNum += existingCell.rowSpan - 1;
214
} else {
215
var newTd = this.createEmptyTd();
216
// TODO(user): figure out a way to intelligently size new columns.
217
newTd.style.width = goog.editor.Table.OPTIMUM_EMPTY_CELL_WIDTH + 'px';
218
this.insertCellElement(newTd, rowNum, colIndex);
219
newTds.push(newTd);
220
}
221
}
222
this.refresh();
223
return newTds;
224
};
225
226
227
/**
228
* Removes a row from the table, removing the TR element and
229
* decrementing the rowspan of any cells in other rows that overlap the row.
230
* @param {number} rowIndex Index of the row to delete.
231
*/
232
goog.editor.Table.prototype.removeRow = function(rowIndex) {
233
var row = this.rows[rowIndex];
234
if (!row) {
235
goog.log.warning(
236
this.logger_,
237
"Can't remove row at position " + rowIndex + ': no such row.');
238
}
239
for (var i = 0, cell; cell = row.columns[i]; i += cell.colSpan) {
240
if (cell.rowSpan > 1) {
241
cell.setRowSpan(cell.rowSpan - 1);
242
if (cell.startRow == rowIndex) {
243
// Rowspanned cell started in this row - move it down to the next row.
244
this.insertCellElement(cell.element, rowIndex + 1, cell.startCol);
245
}
246
}
247
}
248
row.element.parentNode.removeChild(row.element);
249
this.refresh();
250
};
251
252
253
/**
254
* Removes a column from the table. This is done by removing cell elements,
255
* or shrinking the colspan of elements that span multiple columns.
256
* @param {number} colIndex Index of the column to delete.
257
*/
258
goog.editor.Table.prototype.removeColumn = function(colIndex) {
259
for (var i = 0, row; row = this.rows[i]; i++) {
260
var cell = row.columns[colIndex];
261
if (!cell) {
262
goog.log.error(
263
this.logger_, "Can't remove cell at position " + i + ', ' + colIndex +
264
': no such cell.');
265
}
266
if (cell.colSpan > 1) {
267
cell.setColSpan(cell.colSpan - 1);
268
} else {
269
cell.element.parentNode.removeChild(cell.element);
270
}
271
// Skip over following rows that contain this same cell.
272
i += cell.rowSpan - 1;
273
}
274
this.refresh();
275
};
276
277
278
/**
279
* Merges multiple cells into a single cell, and sets the rowSpan and colSpan
280
* attributes of the cell to take up the same space as the original cells.
281
* @param {number} startRowIndex Top coordinate of the cells to merge.
282
* @param {number} startColIndex Left coordinate of the cells to merge.
283
* @param {number} endRowIndex Bottom coordinate of the cells to merge.
284
* @param {number} endColIndex Right coordinate of the cells to merge.
285
* @return {boolean} Whether or not the merge was possible. If the cells
286
* in the supplied coordinates can't be merged this will return false.
287
*/
288
goog.editor.Table.prototype.mergeCells = function(
289
startRowIndex, startColIndex, endRowIndex, endColIndex) {
290
// TODO(user): take a single goog.math.Rect parameter instead?
291
var cells = [];
292
var cell;
293
if (startRowIndex == endRowIndex && startColIndex == endColIndex) {
294
goog.log.warning(this.logger_, "Can't merge single cell");
295
return false;
296
}
297
// Gather cells and do sanity check.
298
for (var i = startRowIndex; i <= endRowIndex; i++) {
299
for (var j = startColIndex; j <= endColIndex; j++) {
300
cell = this.rows[i].columns[j];
301
if (cell.startRow < startRowIndex || cell.endRow > endRowIndex ||
302
cell.startCol < startColIndex || cell.endCol > endColIndex) {
303
goog.log.warning(
304
this.logger_, "Can't merge cells: the cell in row " + i +
305
', column ' + j + 'extends outside the supplied rectangle.');
306
return false;
307
}
308
// TODO(user): this is somewhat inefficient, as we will add
309
// a reference for a cell for each position, even if it's a single
310
// cell with row/colspan.
311
cells.push(cell);
312
}
313
}
314
var targetCell = cells[0];
315
var targetTd = targetCell.element;
316
var doc = this.dom_.getDocument();
317
318
// Merge cell contents and discard other cells.
319
for (var i = 1; cell = cells[i]; i++) {
320
var td = cell.element;
321
if (!td.parentNode || td == targetTd) {
322
// We've already handled this cell at one of its previous positions.
323
continue;
324
}
325
// Add a space if needed, to keep merged content from getting squished
326
// together.
327
if (targetTd.lastChild &&
328
targetTd.lastChild.nodeType == goog.dom.NodeType.TEXT) {
329
targetTd.appendChild(doc.createTextNode(' '));
330
}
331
var childNode;
332
while ((childNode = td.firstChild)) {
333
targetTd.appendChild(childNode);
334
}
335
td.parentNode.removeChild(td);
336
}
337
targetCell.setColSpan((endColIndex - startColIndex) + 1);
338
targetCell.setRowSpan((endRowIndex - startRowIndex) + 1);
339
if (endColIndex > startColIndex) {
340
// Clear width on target cell.
341
// TODO(user): instead of clearing width, calculate width
342
// based on width of input cells
343
targetTd.removeAttribute('width');
344
targetTd.style.width = null;
345
}
346
this.refresh();
347
348
return true;
349
};
350
351
352
/**
353
* Splits a cell with colspans or rowspans into multiple descrete cells.
354
* @param {number} rowIndex y coordinate of the cell to split.
355
* @param {number} colIndex x coordinate of the cell to split.
356
* @return {!Array<Element>} Array of new cell elements created by splitting
357
* the cell.
358
*/
359
// TODO(user): support splitting only horizontally or vertically,
360
// support splitting cells that aren't already row/colspanned.
361
goog.editor.Table.prototype.splitCell = function(rowIndex, colIndex) {
362
var row = this.rows[rowIndex];
363
var cell = row.columns[colIndex];
364
var newTds = [];
365
for (var i = 0; i < cell.rowSpan; i++) {
366
for (var j = 0; j < cell.colSpan; j++) {
367
if (i > 0 || j > 0) {
368
var newTd = this.createEmptyTd();
369
this.insertCellElement(newTd, rowIndex + i, colIndex + j);
370
newTds.push(newTd);
371
}
372
}
373
}
374
cell.setColSpan(1);
375
cell.setRowSpan(1);
376
this.refresh();
377
return newTds;
378
};
379
380
381
/**
382
* Inserts a cell element at the given position. The colIndex is the logical
383
* column index, not the position in the dom. This takes into consideration
384
* that cells in a given logical row may actually be children of a previous
385
* DOM row that have used rowSpan to extend into the row.
386
* @param {Element} td The new cell element to insert.
387
* @param {number} rowIndex Row in which to insert the element.
388
* @param {number} colIndex Column in which to insert the element.
389
*/
390
goog.editor.Table.prototype.insertCellElement = function(
391
td, rowIndex, colIndex) {
392
var row = this.rows[rowIndex];
393
var nextSiblingElement = null;
394
for (var i = colIndex, cell; cell = row.columns[i]; i += cell.colSpan) {
395
if (cell.startRow == rowIndex) {
396
nextSiblingElement = cell.element;
397
break;
398
}
399
}
400
row.element.insertBefore(td, nextSiblingElement);
401
};
402
403
404
/**
405
* Creates an empty TD element and fill it with some empty content so it will
406
* show up with borders even in IE pre-7 or if empty-cells is set to 'hide'
407
* @return {!Element} a new TD element.
408
*/
409
goog.editor.Table.prototype.createEmptyTd = function() {
410
// TODO(user): more cross-browser testing to determine best
411
// and least annoying filler content.
412
return this.dom_.createDom(goog.dom.TagName.TD, {}, goog.string.Unicode.NBSP);
413
};
414
415
416
417
/**
418
* Class representing a logical table row: a tr element and any cells
419
* that appear in that row.
420
* @param {Element} trElement This rows's underlying TR element.
421
* @param {number} rowIndex This row's index in its parent table.
422
* @constructor
423
* @final
424
*/
425
goog.editor.TableRow = function(trElement, rowIndex) {
426
this.index = rowIndex;
427
this.element = trElement;
428
this.columns = [];
429
};
430
431
432
433
/**
434
* Class representing a table cell, which may span across multiple
435
* rows and columns
436
* @param {Element} td This cell's underlying TD or TH element.
437
* @param {number} startRow Index of the row where this cell begins.
438
* @param {number} startCol Index of the column where this cell begins.
439
* @constructor
440
* @final
441
*/
442
goog.editor.TableCell = function(td, startRow, startCol) {
443
this.element = td;
444
this.colSpan = parseInt(td.colSpan, 10) || 1;
445
this.rowSpan = parseInt(td.rowSpan, 10) || 1;
446
this.startRow = startRow;
447
this.startCol = startCol;
448
this.updateCoordinates_();
449
};
450
451
452
/**
453
* Calculates this cell's endRow/endCol coordinates based on rowSpan/colSpan
454
* @private
455
*/
456
goog.editor.TableCell.prototype.updateCoordinates_ = function() {
457
this.endCol = this.startCol + this.colSpan - 1;
458
this.endRow = this.startRow + this.rowSpan - 1;
459
};
460
461
462
/**
463
* Set this cell's colSpan, updating both its colSpan property and the
464
* underlying element's colSpan attribute.
465
* @param {number} colSpan The new colSpan.
466
*/
467
goog.editor.TableCell.prototype.setColSpan = function(colSpan) {
468
if (colSpan != this.colSpan) {
469
if (colSpan > 1) {
470
this.element.colSpan = colSpan;
471
} else {
472
this.element.colSpan = 1, this.element.removeAttribute('colSpan');
473
}
474
this.colSpan = colSpan;
475
this.updateCoordinates_();
476
}
477
};
478
479
480
/**
481
* Set this cell's rowSpan, updating both its rowSpan property and the
482
* underlying element's rowSpan attribute.
483
* @param {number} rowSpan The new rowSpan.
484
*/
485
goog.editor.TableCell.prototype.setRowSpan = function(rowSpan) {
486
if (rowSpan != this.rowSpan) {
487
if (rowSpan > 1) {
488
this.element.rowSpan = rowSpan.toString();
489
} else {
490
this.element.rowSpan = '1';
491
this.element.removeAttribute('rowSpan');
492
}
493
this.rowSpan = rowSpan;
494
this.updateCoordinates_();
495
}
496
};
497
498
499
/**
500
* Optimum size of empty cells (in pixels), if possible.
501
* @type {number}
502
*/
503
goog.editor.Table.OPTIMUM_EMPTY_CELL_WIDTH = 60;
504
505
506
/**
507
* Maximum width for new tables.
508
* @type {number}
509
*/
510
goog.editor.Table.OPTIMUM_MAX_NEW_TABLE_WIDTH = 600;
511
512
513
/**
514
* Default color for table borders.
515
* @type {string}
516
*/
517
goog.editor.Table.DEFAULT_BORDER_COLOR = '#888';
518
519
520
/**
521
* Creates a new table element, populated with cells and formatted.
522
* @param {Document} doc Document in which to create the table element.
523
* @param {number} columns Number of columns in the table.
524
* @param {number} rows Number of rows in the table.
525
* @param {Object=} opt_tableStyle Object containing borderWidth and borderColor
526
* properties, used to set the initial style of the table.
527
* @return {!Element} a table element.
528
*/
529
goog.editor.Table.createDomTable = function(
530
doc, columns, rows, opt_tableStyle) {
531
// TODO(user): define formatting properties as constants,
532
// make separate formatTable() function
533
var style = {
534
borderWidth: '1',
535
borderColor: goog.editor.Table.DEFAULT_BORDER_COLOR
536
};
537
for (var prop in opt_tableStyle) {
538
style[prop] = opt_tableStyle[prop];
539
}
540
var dom = new goog.dom.DomHelper(doc);
541
var tableElement = dom.createTable(rows, columns, true);
542
543
var minimumCellWidth = 10;
544
// Calculate a good cell width.
545
var cellWidth = Math.max(
546
minimumCellWidth,
547
Math.min(
548
goog.editor.Table.OPTIMUM_EMPTY_CELL_WIDTH,
549
goog.editor.Table.OPTIMUM_MAX_NEW_TABLE_WIDTH / columns));
550
551
var tds = goog.dom.getElementsByTagName(goog.dom.TagName.TD, tableElement);
552
for (var i = 0, td; td = tds[i]; i++) {
553
td.style.width = cellWidth + 'px';
554
}
555
556
// Set border somewhat redundantly to make sure they show
557
// up correctly in all browsers.
558
goog.style.setStyle(tableElement, {
559
'borderCollapse': 'collapse',
560
'borderColor': style.borderColor,
561
'borderWidth': style.borderWidth + 'px'
562
});
563
tableElement.border = style.borderWidth;
564
tableElement.setAttribute('bordercolor', style.borderColor);
565
tableElement.setAttribute('cellspacing', '0');
566
567
return tableElement;
568
};
569
570