table.js 19 KB

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