123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568 |
- // Copyright 2008 The Closure Library Authors. All Rights Reserved.
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS-IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- /**
- * @fileoverview Table editing support.
- * This file provides the class goog.editor.Table and two
- * supporting classes, goog.editor.TableRow and
- * goog.editor.TableCell. Together these provide support for
- * high level table modifications: Adding and deleting rows and columns,
- * and merging and splitting cells.
- *
- */
- goog.provide('goog.editor.Table');
- goog.provide('goog.editor.TableCell');
- goog.provide('goog.editor.TableRow');
- goog.require('goog.asserts');
- goog.require('goog.dom');
- goog.require('goog.dom.DomHelper');
- goog.require('goog.dom.NodeType');
- goog.require('goog.dom.TagName');
- goog.require('goog.log');
- goog.require('goog.string.Unicode');
- goog.require('goog.style');
- /**
- * Class providing high level table editing functions.
- * @param {Element} node Element that is a table or descendant of a table.
- * @constructor
- * @final
- */
- goog.editor.Table = function(node) {
- this.element =
- goog.dom.getAncestorByTagNameAndClass(node, goog.dom.TagName.TABLE);
- if (!this.element) {
- goog.log.error(
- this.logger_, "Can't create Table based on a node " +
- "that isn't a table, or descended from a table.");
- }
- this.dom_ = goog.dom.getDomHelper(this.element);
- this.refresh();
- };
- /**
- * Logger object for debugging and error messages.
- * @type {goog.log.Logger}
- * @private
- */
- goog.editor.Table.prototype.logger_ = goog.log.getLogger('goog.editor.Table');
- /**
- * Walks the dom structure of this object's table element and populates
- * this.rows with goog.editor.TableRow objects. This is done initially
- * to populate the internal data structures, and also after each time the
- * DOM structure is modified. Currently this means that the all existing
- * information is discarded and re-read from the DOM.
- */
- // TODO(user): support partial refresh to save cost of full update
- // every time there is a change to the DOM.
- goog.editor.Table.prototype.refresh = function() {
- var rows = this.rows = [];
- var tbody = goog.dom.getElementsByTagName(
- goog.dom.TagName.TBODY, goog.asserts.assert(this.element))[0];
- if (!tbody) {
- return;
- }
- var trs = [];
- for (var child = tbody.firstChild; child; child = child.nextSibling) {
- if (child.nodeName == goog.dom.TagName.TR) {
- trs.push(child);
- }
- }
- for (var rowNum = 0, tr; tr = trs[rowNum]; rowNum++) {
- var existingRow = rows[rowNum];
- var tds = goog.editor.Table.getChildCellElements(tr);
- var columnNum = 0;
- // A note on cellNum vs. columnNum: A cell is a td/th element. Cells may
- // use colspan/rowspan to extend over multiple rows/columns. cellNum
- // is the dom element number, columnNum is the logical column number.
- for (var cellNum = 0, td; td = tds[cellNum]; cellNum++) {
- // If there's already a cell extending into this column
- // (due to that cell's colspan/rowspan), increment the column counter.
- while (existingRow && existingRow.columns[columnNum]) {
- columnNum++;
- }
- var cell = new goog.editor.TableCell(td, rowNum, columnNum);
- // Place this cell in every row and column into which it extends.
- for (var i = 0; i < cell.rowSpan; i++) {
- var cellRowNum = rowNum + i;
- // Create TableRow objects in this.rows as needed.
- var cellRow = rows[cellRowNum];
- if (!cellRow) {
- // TODO(user): try to avoid second trs[] lookup.
- rows.push(
- cellRow = new goog.editor.TableRow(trs[cellRowNum], cellRowNum));
- }
- // Extend length of column array to make room for this cell.
- var minimumColumnLength = columnNum + cell.colSpan;
- if (cellRow.columns.length < minimumColumnLength) {
- cellRow.columns.length = minimumColumnLength;
- }
- for (var j = 0; j < cell.colSpan; j++) {
- var cellColumnNum = columnNum + j;
- cellRow.columns[cellColumnNum] = cell;
- }
- }
- columnNum += cell.colSpan;
- }
- }
- };
- /**
- * Returns all child elements of a TR element that are of type TD or TH.
- * @param {Element} tr TR element in which to find children.
- * @return {!Array<Element>} array of child cell elements.
- */
- goog.editor.Table.getChildCellElements = function(tr) {
- var cells = [];
- for (var i = 0, cell; cell = tr.childNodes[i]; i++) {
- if (cell.nodeName == goog.dom.TagName.TD ||
- cell.nodeName == goog.dom.TagName.TH) {
- cells.push(cell);
- }
- }
- return cells;
- };
- /**
- * Inserts a new row in the table. The row will be populated with new
- * cells, and existing rowspanned cells that overlap the new row will
- * be extended.
- * @param {number=} opt_rowIndex Index at which to insert the row. If
- * this is omitted the row will be appended to the end of the table.
- * @return {!Element} The new row.
- */
- goog.editor.Table.prototype.insertRow = function(opt_rowIndex) {
- var rowIndex =
- goog.isDefAndNotNull(opt_rowIndex) ? opt_rowIndex : this.rows.length;
- var refRow;
- var insertAfter;
- if (rowIndex == 0) {
- refRow = this.rows[0];
- insertAfter = false;
- } else {
- refRow = this.rows[rowIndex - 1];
- insertAfter = true;
- }
- var newTr = this.dom_.createElement(goog.dom.TagName.TR);
- for (var i = 0, cell; cell = refRow.columns[i]; i += 1) {
- // Check whether the existing cell will span this new row.
- // If so, instead of creating a new cell, extend
- // the rowspan of the existing cell.
- if ((insertAfter && cell.endRow > rowIndex) ||
- (!insertAfter && cell.startRow < rowIndex)) {
- cell.setRowSpan(cell.rowSpan + 1);
- if (cell.colSpan > 1) {
- i += cell.colSpan - 1;
- }
- } else {
- newTr.appendChild(this.createEmptyTd());
- }
- if (insertAfter) {
- goog.dom.insertSiblingAfter(newTr, refRow.element);
- } else {
- goog.dom.insertSiblingBefore(newTr, refRow.element);
- }
- }
- this.refresh();
- return newTr;
- };
- /**
- * Inserts a new column in the table. The column will be created by
- * inserting new TD elements in each row, or extending the colspan
- * of existing TD elements.
- * @param {number=} opt_colIndex Index at which to insert the column. If
- * this is omitted the column will be appended to the right side of
- * the table.
- * @return {!Array<Element>} Array of new cell elements that were created
- * to populate the new column.
- */
- goog.editor.Table.prototype.insertColumn = function(opt_colIndex) {
- // TODO(user): set column widths in a way that makes sense.
- var colIndex = goog.isDefAndNotNull(opt_colIndex) ?
- opt_colIndex :
- (this.rows[0] && this.rows[0].columns.length) || 0;
- var newTds = [];
- for (var rowNum = 0, row; row = this.rows[rowNum]; rowNum++) {
- var existingCell = row.columns[colIndex];
- if (existingCell && existingCell.endCol >= colIndex &&
- existingCell.startCol < colIndex) {
- existingCell.setColSpan(existingCell.colSpan + 1);
- rowNum += existingCell.rowSpan - 1;
- } else {
- var newTd = this.createEmptyTd();
- // TODO(user): figure out a way to intelligently size new columns.
- newTd.style.width = goog.editor.Table.OPTIMUM_EMPTY_CELL_WIDTH + 'px';
- this.insertCellElement(newTd, rowNum, colIndex);
- newTds.push(newTd);
- }
- }
- this.refresh();
- return newTds;
- };
- /**
- * Removes a row from the table, removing the TR element and
- * decrementing the rowspan of any cells in other rows that overlap the row.
- * @param {number} rowIndex Index of the row to delete.
- */
- goog.editor.Table.prototype.removeRow = function(rowIndex) {
- var row = this.rows[rowIndex];
- if (!row) {
- goog.log.warning(
- this.logger_,
- "Can't remove row at position " + rowIndex + ': no such row.');
- }
- for (var i = 0, cell; cell = row.columns[i]; i += cell.colSpan) {
- if (cell.rowSpan > 1) {
- cell.setRowSpan(cell.rowSpan - 1);
- if (cell.startRow == rowIndex) {
- // Rowspanned cell started in this row - move it down to the next row.
- this.insertCellElement(cell.element, rowIndex + 1, cell.startCol);
- }
- }
- }
- row.element.parentNode.removeChild(row.element);
- this.refresh();
- };
- /**
- * Removes a column from the table. This is done by removing cell elements,
- * or shrinking the colspan of elements that span multiple columns.
- * @param {number} colIndex Index of the column to delete.
- */
- goog.editor.Table.prototype.removeColumn = function(colIndex) {
- for (var i = 0, row; row = this.rows[i]; i++) {
- var cell = row.columns[colIndex];
- if (!cell) {
- goog.log.error(
- this.logger_, "Can't remove cell at position " + i + ', ' + colIndex +
- ': no such cell.');
- }
- if (cell.colSpan > 1) {
- cell.setColSpan(cell.colSpan - 1);
- } else {
- cell.element.parentNode.removeChild(cell.element);
- }
- // Skip over following rows that contain this same cell.
- i += cell.rowSpan - 1;
- }
- this.refresh();
- };
- /**
- * Merges multiple cells into a single cell, and sets the rowSpan and colSpan
- * attributes of the cell to take up the same space as the original cells.
- * @param {number} startRowIndex Top coordinate of the cells to merge.
- * @param {number} startColIndex Left coordinate of the cells to merge.
- * @param {number} endRowIndex Bottom coordinate of the cells to merge.
- * @param {number} endColIndex Right coordinate of the cells to merge.
- * @return {boolean} Whether or not the merge was possible. If the cells
- * in the supplied coordinates can't be merged this will return false.
- */
- goog.editor.Table.prototype.mergeCells = function(
- startRowIndex, startColIndex, endRowIndex, endColIndex) {
- // TODO(user): take a single goog.math.Rect parameter instead?
- var cells = [];
- var cell;
- if (startRowIndex == endRowIndex && startColIndex == endColIndex) {
- goog.log.warning(this.logger_, "Can't merge single cell");
- return false;
- }
- // Gather cells and do sanity check.
- for (var i = startRowIndex; i <= endRowIndex; i++) {
- for (var j = startColIndex; j <= endColIndex; j++) {
- cell = this.rows[i].columns[j];
- if (cell.startRow < startRowIndex || cell.endRow > endRowIndex ||
- cell.startCol < startColIndex || cell.endCol > endColIndex) {
- goog.log.warning(
- this.logger_, "Can't merge cells: the cell in row " + i +
- ', column ' + j + 'extends outside the supplied rectangle.');
- return false;
- }
- // TODO(user): this is somewhat inefficient, as we will add
- // a reference for a cell for each position, even if it's a single
- // cell with row/colspan.
- cells.push(cell);
- }
- }
- var targetCell = cells[0];
- var targetTd = targetCell.element;
- var doc = this.dom_.getDocument();
- // Merge cell contents and discard other cells.
- for (var i = 1; cell = cells[i]; i++) {
- var td = cell.element;
- if (!td.parentNode || td == targetTd) {
- // We've already handled this cell at one of its previous positions.
- continue;
- }
- // Add a space if needed, to keep merged content from getting squished
- // together.
- if (targetTd.lastChild &&
- targetTd.lastChild.nodeType == goog.dom.NodeType.TEXT) {
- targetTd.appendChild(doc.createTextNode(' '));
- }
- var childNode;
- while ((childNode = td.firstChild)) {
- targetTd.appendChild(childNode);
- }
- td.parentNode.removeChild(td);
- }
- targetCell.setColSpan((endColIndex - startColIndex) + 1);
- targetCell.setRowSpan((endRowIndex - startRowIndex) + 1);
- if (endColIndex > startColIndex) {
- // Clear width on target cell.
- // TODO(user): instead of clearing width, calculate width
- // based on width of input cells
- targetTd.removeAttribute('width');
- targetTd.style.width = null;
- }
- this.refresh();
- return true;
- };
- /**
- * Splits a cell with colspans or rowspans into multiple descrete cells.
- * @param {number} rowIndex y coordinate of the cell to split.
- * @param {number} colIndex x coordinate of the cell to split.
- * @return {!Array<Element>} Array of new cell elements created by splitting
- * the cell.
- */
- // TODO(user): support splitting only horizontally or vertically,
- // support splitting cells that aren't already row/colspanned.
- goog.editor.Table.prototype.splitCell = function(rowIndex, colIndex) {
- var row = this.rows[rowIndex];
- var cell = row.columns[colIndex];
- var newTds = [];
- for (var i = 0; i < cell.rowSpan; i++) {
- for (var j = 0; j < cell.colSpan; j++) {
- if (i > 0 || j > 0) {
- var newTd = this.createEmptyTd();
- this.insertCellElement(newTd, rowIndex + i, colIndex + j);
- newTds.push(newTd);
- }
- }
- }
- cell.setColSpan(1);
- cell.setRowSpan(1);
- this.refresh();
- return newTds;
- };
- /**
- * Inserts a cell element at the given position. The colIndex is the logical
- * column index, not the position in the dom. This takes into consideration
- * that cells in a given logical row may actually be children of a previous
- * DOM row that have used rowSpan to extend into the row.
- * @param {Element} td The new cell element to insert.
- * @param {number} rowIndex Row in which to insert the element.
- * @param {number} colIndex Column in which to insert the element.
- */
- goog.editor.Table.prototype.insertCellElement = function(
- td, rowIndex, colIndex) {
- var row = this.rows[rowIndex];
- var nextSiblingElement = null;
- for (var i = colIndex, cell; cell = row.columns[i]; i += cell.colSpan) {
- if (cell.startRow == rowIndex) {
- nextSiblingElement = cell.element;
- break;
- }
- }
- row.element.insertBefore(td, nextSiblingElement);
- };
- /**
- * Creates an empty TD element and fill it with some empty content so it will
- * show up with borders even in IE pre-7 or if empty-cells is set to 'hide'
- * @return {!Element} a new TD element.
- */
- goog.editor.Table.prototype.createEmptyTd = function() {
- // TODO(user): more cross-browser testing to determine best
- // and least annoying filler content.
- return this.dom_.createDom(goog.dom.TagName.TD, {}, goog.string.Unicode.NBSP);
- };
- /**
- * Class representing a logical table row: a tr element and any cells
- * that appear in that row.
- * @param {Element} trElement This rows's underlying TR element.
- * @param {number} rowIndex This row's index in its parent table.
- * @constructor
- * @final
- */
- goog.editor.TableRow = function(trElement, rowIndex) {
- this.index = rowIndex;
- this.element = trElement;
- this.columns = [];
- };
- /**
- * Class representing a table cell, which may span across multiple
- * rows and columns
- * @param {Element} td This cell's underlying TD or TH element.
- * @param {number} startRow Index of the row where this cell begins.
- * @param {number} startCol Index of the column where this cell begins.
- * @constructor
- * @final
- */
- goog.editor.TableCell = function(td, startRow, startCol) {
- this.element = td;
- this.colSpan = parseInt(td.colSpan, 10) || 1;
- this.rowSpan = parseInt(td.rowSpan, 10) || 1;
- this.startRow = startRow;
- this.startCol = startCol;
- this.updateCoordinates_();
- };
- /**
- * Calculates this cell's endRow/endCol coordinates based on rowSpan/colSpan
- * @private
- */
- goog.editor.TableCell.prototype.updateCoordinates_ = function() {
- this.endCol = this.startCol + this.colSpan - 1;
- this.endRow = this.startRow + this.rowSpan - 1;
- };
- /**
- * Set this cell's colSpan, updating both its colSpan property and the
- * underlying element's colSpan attribute.
- * @param {number} colSpan The new colSpan.
- */
- goog.editor.TableCell.prototype.setColSpan = function(colSpan) {
- if (colSpan != this.colSpan) {
- if (colSpan > 1) {
- this.element.colSpan = colSpan;
- } else {
- this.element.colSpan = 1, this.element.removeAttribute('colSpan');
- }
- this.colSpan = colSpan;
- this.updateCoordinates_();
- }
- };
- /**
- * Set this cell's rowSpan, updating both its rowSpan property and the
- * underlying element's rowSpan attribute.
- * @param {number} rowSpan The new rowSpan.
- */
- goog.editor.TableCell.prototype.setRowSpan = function(rowSpan) {
- if (rowSpan != this.rowSpan) {
- if (rowSpan > 1) {
- this.element.rowSpan = rowSpan.toString();
- } else {
- this.element.rowSpan = '1';
- this.element.removeAttribute('rowSpan');
- }
- this.rowSpan = rowSpan;
- this.updateCoordinates_();
- }
- };
- /**
- * Optimum size of empty cells (in pixels), if possible.
- * @type {number}
- */
- goog.editor.Table.OPTIMUM_EMPTY_CELL_WIDTH = 60;
- /**
- * Maximum width for new tables.
- * @type {number}
- */
- goog.editor.Table.OPTIMUM_MAX_NEW_TABLE_WIDTH = 600;
- /**
- * Default color for table borders.
- * @type {string}
- */
- goog.editor.Table.DEFAULT_BORDER_COLOR = '#888';
- /**
- * Creates a new table element, populated with cells and formatted.
- * @param {Document} doc Document in which to create the table element.
- * @param {number} columns Number of columns in the table.
- * @param {number} rows Number of rows in the table.
- * @param {Object=} opt_tableStyle Object containing borderWidth and borderColor
- * properties, used to set the initial style of the table.
- * @return {!Element} a table element.
- */
- goog.editor.Table.createDomTable = function(
- doc, columns, rows, opt_tableStyle) {
- // TODO(user): define formatting properties as constants,
- // make separate formatTable() function
- var style = {
- borderWidth: '1',
- borderColor: goog.editor.Table.DEFAULT_BORDER_COLOR
- };
- for (var prop in opt_tableStyle) {
- style[prop] = opt_tableStyle[prop];
- }
- var dom = new goog.dom.DomHelper(doc);
- var tableElement = dom.createTable(rows, columns, true);
- var minimumCellWidth = 10;
- // Calculate a good cell width.
- var cellWidth = Math.max(
- minimumCellWidth,
- Math.min(
- goog.editor.Table.OPTIMUM_EMPTY_CELL_WIDTH,
- goog.editor.Table.OPTIMUM_MAX_NEW_TABLE_WIDTH / columns));
- var tds = goog.dom.getElementsByTagName(goog.dom.TagName.TD, tableElement);
- for (var i = 0, td; td = tds[i]; i++) {
- td.style.width = cellWidth + 'px';
- }
- // Set border somewhat redundantly to make sure they show
- // up correctly in all browsers.
- goog.style.setStyle(tableElement, {
- 'borderCollapse': 'collapse',
- 'borderColor': style.borderColor,
- 'borderWidth': style.borderWidth + 'px'
- });
- tableElement.border = style.borderWidth;
- tableElement.setAttribute('bordercolor', style.borderColor);
- tableElement.setAttribute('cellspacing', '0');
- return tableElement;
- };
|