Path: blob/trunk/third_party/closure/goog/editor/table.js
2868 views
// Copyright 2008 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Table editing support.16* This file provides the class goog.editor.Table and two17* supporting classes, goog.editor.TableRow and18* goog.editor.TableCell. Together these provide support for19* high level table modifications: Adding and deleting rows and columns,20* and merging and splitting cells.21*22*/2324goog.provide('goog.editor.Table');25goog.provide('goog.editor.TableCell');26goog.provide('goog.editor.TableRow');2728goog.require('goog.asserts');29goog.require('goog.dom');30goog.require('goog.dom.DomHelper');31goog.require('goog.dom.NodeType');32goog.require('goog.dom.TagName');33goog.require('goog.log');34goog.require('goog.string.Unicode');35goog.require('goog.style');36373839/**40* Class providing high level table editing functions.41* @param {Element} node Element that is a table or descendant of a table.42* @constructor43* @final44*/45goog.editor.Table = function(node) {46this.element =47goog.dom.getAncestorByTagNameAndClass(node, goog.dom.TagName.TABLE);48if (!this.element) {49goog.log.error(50this.logger_, "Can't create Table based on a node " +51"that isn't a table, or descended from a table.");52}53this.dom_ = goog.dom.getDomHelper(this.element);54this.refresh();55};565758/**59* Logger object for debugging and error messages.60* @type {goog.log.Logger}61* @private62*/63goog.editor.Table.prototype.logger_ = goog.log.getLogger('goog.editor.Table');646566/**67* Walks the dom structure of this object's table element and populates68* this.rows with goog.editor.TableRow objects. This is done initially69* to populate the internal data structures, and also after each time the70* DOM structure is modified. Currently this means that the all existing71* information is discarded and re-read from the DOM.72*/73// TODO(user): support partial refresh to save cost of full update74// every time there is a change to the DOM.75goog.editor.Table.prototype.refresh = function() {76var rows = this.rows = [];77var tbody = goog.dom.getElementsByTagName(78goog.dom.TagName.TBODY, goog.asserts.assert(this.element))[0];79if (!tbody) {80return;81}82var trs = [];83for (var child = tbody.firstChild; child; child = child.nextSibling) {84if (child.nodeName == goog.dom.TagName.TR) {85trs.push(child);86}87}8889for (var rowNum = 0, tr; tr = trs[rowNum]; rowNum++) {90var existingRow = rows[rowNum];91var tds = goog.editor.Table.getChildCellElements(tr);92var columnNum = 0;93// A note on cellNum vs. columnNum: A cell is a td/th element. Cells may94// use colspan/rowspan to extend over multiple rows/columns. cellNum95// is the dom element number, columnNum is the logical column number.96for (var cellNum = 0, td; td = tds[cellNum]; cellNum++) {97// If there's already a cell extending into this column98// (due to that cell's colspan/rowspan), increment the column counter.99while (existingRow && existingRow.columns[columnNum]) {100columnNum++;101}102var cell = new goog.editor.TableCell(td, rowNum, columnNum);103// Place this cell in every row and column into which it extends.104for (var i = 0; i < cell.rowSpan; i++) {105var cellRowNum = rowNum + i;106// Create TableRow objects in this.rows as needed.107var cellRow = rows[cellRowNum];108if (!cellRow) {109// TODO(user): try to avoid second trs[] lookup.110rows.push(111cellRow = new goog.editor.TableRow(trs[cellRowNum], cellRowNum));112}113// Extend length of column array to make room for this cell.114var minimumColumnLength = columnNum + cell.colSpan;115if (cellRow.columns.length < minimumColumnLength) {116cellRow.columns.length = minimumColumnLength;117}118for (var j = 0; j < cell.colSpan; j++) {119var cellColumnNum = columnNum + j;120cellRow.columns[cellColumnNum] = cell;121}122}123columnNum += cell.colSpan;124}125}126};127128129/**130* Returns all child elements of a TR element that are of type TD or TH.131* @param {Element} tr TR element in which to find children.132* @return {!Array<Element>} array of child cell elements.133*/134goog.editor.Table.getChildCellElements = function(tr) {135var cells = [];136for (var i = 0, cell; cell = tr.childNodes[i]; i++) {137if (cell.nodeName == goog.dom.TagName.TD ||138cell.nodeName == goog.dom.TagName.TH) {139cells.push(cell);140}141}142return cells;143};144145146/**147* Inserts a new row in the table. The row will be populated with new148* cells, and existing rowspanned cells that overlap the new row will149* be extended.150* @param {number=} opt_rowIndex Index at which to insert the row. If151* this is omitted the row will be appended to the end of the table.152* @return {!Element} The new row.153*/154goog.editor.Table.prototype.insertRow = function(opt_rowIndex) {155var rowIndex =156goog.isDefAndNotNull(opt_rowIndex) ? opt_rowIndex : this.rows.length;157var refRow;158var insertAfter;159if (rowIndex == 0) {160refRow = this.rows[0];161insertAfter = false;162} else {163refRow = this.rows[rowIndex - 1];164insertAfter = true;165}166var newTr = this.dom_.createElement(goog.dom.TagName.TR);167for (var i = 0, cell; cell = refRow.columns[i]; i += 1) {168// Check whether the existing cell will span this new row.169// If so, instead of creating a new cell, extend170// the rowspan of the existing cell.171if ((insertAfter && cell.endRow > rowIndex) ||172(!insertAfter && cell.startRow < rowIndex)) {173cell.setRowSpan(cell.rowSpan + 1);174if (cell.colSpan > 1) {175i += cell.colSpan - 1;176}177} else {178newTr.appendChild(this.createEmptyTd());179}180if (insertAfter) {181goog.dom.insertSiblingAfter(newTr, refRow.element);182} else {183goog.dom.insertSiblingBefore(newTr, refRow.element);184}185}186this.refresh();187return newTr;188};189190191/**192* Inserts a new column in the table. The column will be created by193* inserting new TD elements in each row, or extending the colspan194* of existing TD elements.195* @param {number=} opt_colIndex Index at which to insert the column. If196* this is omitted the column will be appended to the right side of197* the table.198* @return {!Array<Element>} Array of new cell elements that were created199* to populate the new column.200*/201goog.editor.Table.prototype.insertColumn = function(opt_colIndex) {202// TODO(user): set column widths in a way that makes sense.203var colIndex = goog.isDefAndNotNull(opt_colIndex) ?204opt_colIndex :205(this.rows[0] && this.rows[0].columns.length) || 0;206var newTds = [];207for (var rowNum = 0, row; row = this.rows[rowNum]; rowNum++) {208var existingCell = row.columns[colIndex];209if (existingCell && existingCell.endCol >= colIndex &&210existingCell.startCol < colIndex) {211existingCell.setColSpan(existingCell.colSpan + 1);212rowNum += existingCell.rowSpan - 1;213} else {214var newTd = this.createEmptyTd();215// TODO(user): figure out a way to intelligently size new columns.216newTd.style.width = goog.editor.Table.OPTIMUM_EMPTY_CELL_WIDTH + 'px';217this.insertCellElement(newTd, rowNum, colIndex);218newTds.push(newTd);219}220}221this.refresh();222return newTds;223};224225226/**227* Removes a row from the table, removing the TR element and228* decrementing the rowspan of any cells in other rows that overlap the row.229* @param {number} rowIndex Index of the row to delete.230*/231goog.editor.Table.prototype.removeRow = function(rowIndex) {232var row = this.rows[rowIndex];233if (!row) {234goog.log.warning(235this.logger_,236"Can't remove row at position " + rowIndex + ': no such row.');237}238for (var i = 0, cell; cell = row.columns[i]; i += cell.colSpan) {239if (cell.rowSpan > 1) {240cell.setRowSpan(cell.rowSpan - 1);241if (cell.startRow == rowIndex) {242// Rowspanned cell started in this row - move it down to the next row.243this.insertCellElement(cell.element, rowIndex + 1, cell.startCol);244}245}246}247row.element.parentNode.removeChild(row.element);248this.refresh();249};250251252/**253* Removes a column from the table. This is done by removing cell elements,254* or shrinking the colspan of elements that span multiple columns.255* @param {number} colIndex Index of the column to delete.256*/257goog.editor.Table.prototype.removeColumn = function(colIndex) {258for (var i = 0, row; row = this.rows[i]; i++) {259var cell = row.columns[colIndex];260if (!cell) {261goog.log.error(262this.logger_, "Can't remove cell at position " + i + ', ' + colIndex +263': no such cell.');264}265if (cell.colSpan > 1) {266cell.setColSpan(cell.colSpan - 1);267} else {268cell.element.parentNode.removeChild(cell.element);269}270// Skip over following rows that contain this same cell.271i += cell.rowSpan - 1;272}273this.refresh();274};275276277/**278* Merges multiple cells into a single cell, and sets the rowSpan and colSpan279* attributes of the cell to take up the same space as the original cells.280* @param {number} startRowIndex Top coordinate of the cells to merge.281* @param {number} startColIndex Left coordinate of the cells to merge.282* @param {number} endRowIndex Bottom coordinate of the cells to merge.283* @param {number} endColIndex Right coordinate of the cells to merge.284* @return {boolean} Whether or not the merge was possible. If the cells285* in the supplied coordinates can't be merged this will return false.286*/287goog.editor.Table.prototype.mergeCells = function(288startRowIndex, startColIndex, endRowIndex, endColIndex) {289// TODO(user): take a single goog.math.Rect parameter instead?290var cells = [];291var cell;292if (startRowIndex == endRowIndex && startColIndex == endColIndex) {293goog.log.warning(this.logger_, "Can't merge single cell");294return false;295}296// Gather cells and do sanity check.297for (var i = startRowIndex; i <= endRowIndex; i++) {298for (var j = startColIndex; j <= endColIndex; j++) {299cell = this.rows[i].columns[j];300if (cell.startRow < startRowIndex || cell.endRow > endRowIndex ||301cell.startCol < startColIndex || cell.endCol > endColIndex) {302goog.log.warning(303this.logger_, "Can't merge cells: the cell in row " + i +304', column ' + j + 'extends outside the supplied rectangle.');305return false;306}307// TODO(user): this is somewhat inefficient, as we will add308// a reference for a cell for each position, even if it's a single309// cell with row/colspan.310cells.push(cell);311}312}313var targetCell = cells[0];314var targetTd = targetCell.element;315var doc = this.dom_.getDocument();316317// Merge cell contents and discard other cells.318for (var i = 1; cell = cells[i]; i++) {319var td = cell.element;320if (!td.parentNode || td == targetTd) {321// We've already handled this cell at one of its previous positions.322continue;323}324// Add a space if needed, to keep merged content from getting squished325// together.326if (targetTd.lastChild &&327targetTd.lastChild.nodeType == goog.dom.NodeType.TEXT) {328targetTd.appendChild(doc.createTextNode(' '));329}330var childNode;331while ((childNode = td.firstChild)) {332targetTd.appendChild(childNode);333}334td.parentNode.removeChild(td);335}336targetCell.setColSpan((endColIndex - startColIndex) + 1);337targetCell.setRowSpan((endRowIndex - startRowIndex) + 1);338if (endColIndex > startColIndex) {339// Clear width on target cell.340// TODO(user): instead of clearing width, calculate width341// based on width of input cells342targetTd.removeAttribute('width');343targetTd.style.width = null;344}345this.refresh();346347return true;348};349350351/**352* Splits a cell with colspans or rowspans into multiple descrete cells.353* @param {number} rowIndex y coordinate of the cell to split.354* @param {number} colIndex x coordinate of the cell to split.355* @return {!Array<Element>} Array of new cell elements created by splitting356* the cell.357*/358// TODO(user): support splitting only horizontally or vertically,359// support splitting cells that aren't already row/colspanned.360goog.editor.Table.prototype.splitCell = function(rowIndex, colIndex) {361var row = this.rows[rowIndex];362var cell = row.columns[colIndex];363var newTds = [];364for (var i = 0; i < cell.rowSpan; i++) {365for (var j = 0; j < cell.colSpan; j++) {366if (i > 0 || j > 0) {367var newTd = this.createEmptyTd();368this.insertCellElement(newTd, rowIndex + i, colIndex + j);369newTds.push(newTd);370}371}372}373cell.setColSpan(1);374cell.setRowSpan(1);375this.refresh();376return newTds;377};378379380/**381* Inserts a cell element at the given position. The colIndex is the logical382* column index, not the position in the dom. This takes into consideration383* that cells in a given logical row may actually be children of a previous384* DOM row that have used rowSpan to extend into the row.385* @param {Element} td The new cell element to insert.386* @param {number} rowIndex Row in which to insert the element.387* @param {number} colIndex Column in which to insert the element.388*/389goog.editor.Table.prototype.insertCellElement = function(390td, rowIndex, colIndex) {391var row = this.rows[rowIndex];392var nextSiblingElement = null;393for (var i = colIndex, cell; cell = row.columns[i]; i += cell.colSpan) {394if (cell.startRow == rowIndex) {395nextSiblingElement = cell.element;396break;397}398}399row.element.insertBefore(td, nextSiblingElement);400};401402403/**404* Creates an empty TD element and fill it with some empty content so it will405* show up with borders even in IE pre-7 or if empty-cells is set to 'hide'406* @return {!Element} a new TD element.407*/408goog.editor.Table.prototype.createEmptyTd = function() {409// TODO(user): more cross-browser testing to determine best410// and least annoying filler content.411return this.dom_.createDom(goog.dom.TagName.TD, {}, goog.string.Unicode.NBSP);412};413414415416/**417* Class representing a logical table row: a tr element and any cells418* that appear in that row.419* @param {Element} trElement This rows's underlying TR element.420* @param {number} rowIndex This row's index in its parent table.421* @constructor422* @final423*/424goog.editor.TableRow = function(trElement, rowIndex) {425this.index = rowIndex;426this.element = trElement;427this.columns = [];428};429430431432/**433* Class representing a table cell, which may span across multiple434* rows and columns435* @param {Element} td This cell's underlying TD or TH element.436* @param {number} startRow Index of the row where this cell begins.437* @param {number} startCol Index of the column where this cell begins.438* @constructor439* @final440*/441goog.editor.TableCell = function(td, startRow, startCol) {442this.element = td;443this.colSpan = parseInt(td.colSpan, 10) || 1;444this.rowSpan = parseInt(td.rowSpan, 10) || 1;445this.startRow = startRow;446this.startCol = startCol;447this.updateCoordinates_();448};449450451/**452* Calculates this cell's endRow/endCol coordinates based on rowSpan/colSpan453* @private454*/455goog.editor.TableCell.prototype.updateCoordinates_ = function() {456this.endCol = this.startCol + this.colSpan - 1;457this.endRow = this.startRow + this.rowSpan - 1;458};459460461/**462* Set this cell's colSpan, updating both its colSpan property and the463* underlying element's colSpan attribute.464* @param {number} colSpan The new colSpan.465*/466goog.editor.TableCell.prototype.setColSpan = function(colSpan) {467if (colSpan != this.colSpan) {468if (colSpan > 1) {469this.element.colSpan = colSpan;470} else {471this.element.colSpan = 1, this.element.removeAttribute('colSpan');472}473this.colSpan = colSpan;474this.updateCoordinates_();475}476};477478479/**480* Set this cell's rowSpan, updating both its rowSpan property and the481* underlying element's rowSpan attribute.482* @param {number} rowSpan The new rowSpan.483*/484goog.editor.TableCell.prototype.setRowSpan = function(rowSpan) {485if (rowSpan != this.rowSpan) {486if (rowSpan > 1) {487this.element.rowSpan = rowSpan.toString();488} else {489this.element.rowSpan = '1';490this.element.removeAttribute('rowSpan');491}492this.rowSpan = rowSpan;493this.updateCoordinates_();494}495};496497498/**499* Optimum size of empty cells (in pixels), if possible.500* @type {number}501*/502goog.editor.Table.OPTIMUM_EMPTY_CELL_WIDTH = 60;503504505/**506* Maximum width for new tables.507* @type {number}508*/509goog.editor.Table.OPTIMUM_MAX_NEW_TABLE_WIDTH = 600;510511512/**513* Default color for table borders.514* @type {string}515*/516goog.editor.Table.DEFAULT_BORDER_COLOR = '#888';517518519/**520* Creates a new table element, populated with cells and formatted.521* @param {Document} doc Document in which to create the table element.522* @param {number} columns Number of columns in the table.523* @param {number} rows Number of rows in the table.524* @param {Object=} opt_tableStyle Object containing borderWidth and borderColor525* properties, used to set the initial style of the table.526* @return {!Element} a table element.527*/528goog.editor.Table.createDomTable = function(529doc, columns, rows, opt_tableStyle) {530// TODO(user): define formatting properties as constants,531// make separate formatTable() function532var style = {533borderWidth: '1',534borderColor: goog.editor.Table.DEFAULT_BORDER_COLOR535};536for (var prop in opt_tableStyle) {537style[prop] = opt_tableStyle[prop];538}539var dom = new goog.dom.DomHelper(doc);540var tableElement = dom.createTable(rows, columns, true);541542var minimumCellWidth = 10;543// Calculate a good cell width.544var cellWidth = Math.max(545minimumCellWidth,546Math.min(547goog.editor.Table.OPTIMUM_EMPTY_CELL_WIDTH,548goog.editor.Table.OPTIMUM_MAX_NEW_TABLE_WIDTH / columns));549550var tds = goog.dom.getElementsByTagName(goog.dom.TagName.TD, tableElement);551for (var i = 0, td; td = tds[i]; i++) {552td.style.width = cellWidth + 'px';553}554555// Set border somewhat redundantly to make sure they show556// up correctly in all browsers.557goog.style.setStyle(tableElement, {558'borderCollapse': 'collapse',559'borderColor': style.borderColor,560'borderWidth': style.borderWidth + 'px'561});562tableElement.border = style.borderWidth;563tableElement.setAttribute('bordercolor', style.borderColor);564tableElement.setAttribute('cellspacing', '0');565566return tableElement;567};568569570