| // 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 Plugin that enables table editing. |
| * |
| * @see ../../demos/editor/tableeditor.html |
| */ |
| |
| goog.provide('goog.editor.plugins.TableEditor'); |
| |
| goog.require('goog.array'); |
| goog.require('goog.dom'); |
| goog.require('goog.dom.Range'); |
| goog.require('goog.dom.TagName'); |
| goog.require('goog.editor.Plugin'); |
| goog.require('goog.editor.Table'); |
| goog.require('goog.editor.node'); |
| goog.require('goog.editor.range'); |
| goog.require('goog.object'); |
| goog.require('goog.userAgent'); |
| |
| |
| |
| /** |
| * Plugin that adds support for table creation and editing commands. |
| * @constructor |
| * @extends {goog.editor.Plugin} |
| * @final |
| */ |
| goog.editor.plugins.TableEditor = function() { |
| goog.editor.plugins.TableEditor.base(this, 'constructor'); |
| |
| /** |
| * The array of functions that decide whether a table element could be |
| * editable by the user or not. |
| * @type {Array<function(Element):boolean>} |
| * @private |
| */ |
| this.isTableEditableFunctions_ = []; |
| |
| /** |
| * The pre-bound function that decides whether a table element could be |
| * editable by the user or not overall. |
| * @type {function(Node):boolean} |
| * @private |
| */ |
| this.isUserEditableTableBound_ = goog.bind(this.isUserEditableTable_, this); |
| }; |
| goog.inherits(goog.editor.plugins.TableEditor, goog.editor.Plugin); |
| |
| |
| /** @override */ |
| // TODO(user): remove this once there's a sensible default |
| // implementation in the base Plugin. |
| goog.editor.plugins.TableEditor.prototype.getTrogClassId = function() { |
| return String(goog.getUid(this.constructor)); |
| }; |
| |
| |
| /** |
| * Commands supported by goog.editor.plugins.TableEditor. |
| * @enum {string} |
| */ |
| goog.editor.plugins.TableEditor.COMMAND = { |
| TABLE: '+table', |
| INSERT_ROW_AFTER: '+insertRowAfter', |
| INSERT_ROW_BEFORE: '+insertRowBefore', |
| INSERT_COLUMN_AFTER: '+insertColumnAfter', |
| INSERT_COLUMN_BEFORE: '+insertColumnBefore', |
| REMOVE_ROWS: '+removeRows', |
| REMOVE_COLUMNS: '+removeColumns', |
| SPLIT_CELL: '+splitCell', |
| MERGE_CELLS: '+mergeCells', |
| REMOVE_TABLE: '+removeTable' |
| }; |
| |
| |
| /** |
| * Inverse map of execCommand strings to |
| * {@link goog.editor.plugins.TableEditor.COMMAND} constants. Used to |
| * determine whether a string corresponds to a command this plugin handles |
| * in O(1) time. |
| * @type {Object} |
| * @private |
| */ |
| goog.editor.plugins.TableEditor.SUPPORTED_COMMANDS_ = |
| goog.object.transpose(goog.editor.plugins.TableEditor.COMMAND); |
| |
| |
| /** |
| * Whether the string corresponds to a command this plugin handles. |
| * @param {string} command Command string to check. |
| * @return {boolean} Whether the string corresponds to a command |
| * this plugin handles. |
| * @override |
| */ |
| goog.editor.plugins.TableEditor.prototype.isSupportedCommand = |
| function(command) { |
| return command in goog.editor.plugins.TableEditor.SUPPORTED_COMMANDS_; |
| }; |
| |
| |
| /** @override */ |
| goog.editor.plugins.TableEditor.prototype.enable = function(fieldObject) { |
| goog.editor.plugins.TableEditor.base(this, 'enable', fieldObject); |
| |
| // enableObjectResizing is supported only for Gecko. |
| // You can refer to http://qooxdoo.org/contrib/project/htmlarea/html_editing |
| // for a compatibility chart. |
| if (goog.userAgent.GECKO) { |
| var doc = this.getFieldDomHelper().getDocument(); |
| doc.execCommand('enableObjectResizing', false, 'true'); |
| } |
| }; |
| |
| |
| /** |
| * Returns the currently selected table. |
| * @return {Element?} The table in which the current selection is |
| * contained, or null if there isn't such a table. |
| * @private |
| */ |
| goog.editor.plugins.TableEditor.prototype.getCurrentTable_ = function() { |
| var selectedElement = this.getFieldObject().getRange().getContainer(); |
| return this.getAncestorTable_(selectedElement); |
| }; |
| |
| |
| /** |
| * Finds the first user-editable table element in the input node's ancestors. |
| * @param {Node?} node The node to start with. |
| * @return {Element?} The table element that is closest ancestor of the node. |
| * @private |
| */ |
| goog.editor.plugins.TableEditor.prototype.getAncestorTable_ = function(node) { |
| var ancestor = goog.dom.getAncestor(node, this.isUserEditableTableBound_, |
| true); |
| if (goog.editor.node.isEditable(ancestor)) { |
| return /** @type {Element?} */(ancestor); |
| } else { |
| return null; |
| } |
| }; |
| |
| |
| /** |
| * Returns the current value of a given command. Currently this plugin |
| * only returns a value for goog.editor.plugins.TableEditor.COMMAND.TABLE. |
| * @override |
| */ |
| goog.editor.plugins.TableEditor.prototype.queryCommandValue = |
| function(command) { |
| if (command == goog.editor.plugins.TableEditor.COMMAND.TABLE) { |
| return !!this.getCurrentTable_(); |
| } |
| }; |
| |
| |
| /** @override */ |
| goog.editor.plugins.TableEditor.prototype.execCommandInternal = function( |
| command, opt_arg) { |
| var result = null; |
| // TD/TH in which to place the cursor, if the command destroys the current |
| // cursor position. |
| var cursorCell = null; |
| var range = this.getFieldObject().getRange(); |
| if (command == goog.editor.plugins.TableEditor.COMMAND.TABLE) { |
| // Don't create a table if the cursor isn't in an editable region. |
| if (!goog.editor.range.isEditable(range)) { |
| return null; |
| } |
| // Create the table. |
| var tableProps = opt_arg || {width: 4, height: 2}; |
| var doc = this.getFieldDomHelper().getDocument(); |
| var table = goog.editor.Table.createDomTable( |
| doc, tableProps.width, tableProps.height); |
| range.replaceContentsWithNode(table); |
| // In IE, replaceContentsWithNode uses pasteHTML, so we lose our reference |
| // to the inserted table. |
| // TODO(user): use the reference to the table element returned from |
| // replaceContentsWithNode. |
| if (!goog.userAgent.IE) { |
| cursorCell = table.getElementsByTagName('td')[0]; |
| } |
| } else { |
| var cellSelection = new goog.editor.plugins.TableEditor.CellSelection_( |
| range, goog.bind(this.getAncestorTable_, this)); |
| var table = cellSelection.getTable(); |
| if (!table) { |
| return null; |
| } |
| switch (command) { |
| case goog.editor.plugins.TableEditor.COMMAND.INSERT_ROW_BEFORE: |
| table.insertRow(cellSelection.getFirstRowIndex()); |
| break; |
| case goog.editor.plugins.TableEditor.COMMAND.INSERT_ROW_AFTER: |
| table.insertRow(cellSelection.getLastRowIndex() + 1); |
| break; |
| case goog.editor.plugins.TableEditor.COMMAND.INSERT_COLUMN_BEFORE: |
| table.insertColumn(cellSelection.getFirstColumnIndex()); |
| break; |
| case goog.editor.plugins.TableEditor.COMMAND.INSERT_COLUMN_AFTER: |
| table.insertColumn(cellSelection.getLastColumnIndex() + 1); |
| break; |
| case goog.editor.plugins.TableEditor.COMMAND.REMOVE_ROWS: |
| var startRow = cellSelection.getFirstRowIndex(); |
| var endRow = cellSelection.getLastRowIndex(); |
| if (startRow == 0 && endRow == (table.rows.length - 1)) { |
| // Instead of deleting all rows, delete the entire table. |
| return this.execCommandInternal( |
| goog.editor.plugins.TableEditor.COMMAND.REMOVE_TABLE); |
| } |
| var startColumn = cellSelection.getFirstColumnIndex(); |
| var rowCount = (endRow - startRow) + 1; |
| for (var i = 0; i < rowCount; i++) { |
| table.removeRow(startRow); |
| } |
| if (table.rows.length > 0) { |
| // Place cursor in the previous/first row. |
| var closestRow = Math.min(startRow, table.rows.length - 1); |
| cursorCell = table.rows[closestRow].columns[startColumn].element; |
| } |
| break; |
| case goog.editor.plugins.TableEditor.COMMAND.REMOVE_COLUMNS: |
| var startCol = cellSelection.getFirstColumnIndex(); |
| var endCol = cellSelection.getLastColumnIndex(); |
| if (startCol == 0 && endCol == (table.rows[0].columns.length - 1)) { |
| // Instead of deleting all columns, delete the entire table. |
| return this.execCommandInternal( |
| goog.editor.plugins.TableEditor.COMMAND.REMOVE_TABLE); |
| } |
| var startRow = cellSelection.getFirstRowIndex(); |
| var removeCount = (endCol - startCol) + 1; |
| for (var i = 0; i < removeCount; i++) { |
| table.removeColumn(startCol); |
| } |
| var currentRow = table.rows[startRow]; |
| if (currentRow) { |
| // Place cursor in the previous/first column. |
| var closestCol = Math.min(startCol, currentRow.columns.length - 1); |
| cursorCell = currentRow.columns[closestCol].element; |
| } |
| break; |
| case goog.editor.plugins.TableEditor.COMMAND.MERGE_CELLS: |
| if (cellSelection.isRectangle()) { |
| table.mergeCells(cellSelection.getFirstRowIndex(), |
| cellSelection.getFirstColumnIndex(), |
| cellSelection.getLastRowIndex(), |
| cellSelection.getLastColumnIndex()); |
| } |
| break; |
| case goog.editor.plugins.TableEditor.COMMAND.SPLIT_CELL: |
| if (cellSelection.containsSingleCell()) { |
| table.splitCell(cellSelection.getFirstRowIndex(), |
| cellSelection.getFirstColumnIndex()); |
| } |
| break; |
| case goog.editor.plugins.TableEditor.COMMAND.REMOVE_TABLE: |
| table.element.parentNode.removeChild(table.element); |
| break; |
| default: |
| } |
| } |
| if (cursorCell) { |
| range = goog.dom.Range.createFromNodeContents(cursorCell); |
| range.collapse(false); |
| range.select(); |
| } |
| return result; |
| }; |
| |
| |
| /** |
| * Checks whether the element is a table editable by the user. |
| * @param {Node} element The element in question. |
| * @return {boolean} Whether the element is a table editable by the user. |
| * @private |
| */ |
| goog.editor.plugins.TableEditor.prototype.isUserEditableTable_ = |
| function(element) { |
| // Default implementation. |
| if (element.tagName != goog.dom.TagName.TABLE) { |
| return false; |
| } |
| |
| // Check for extra user-editable filters. |
| return goog.array.every(this.isTableEditableFunctions_, function(func) { |
| return func(/** @type {Element} */ (element)); |
| }); |
| }; |
| |
| |
| /** |
| * Adds a function to filter out non-user-editable tables. |
| * @param {function(Element):boolean} func A function to decide whether the |
| * table element could be editable by the user or not. |
| */ |
| goog.editor.plugins.TableEditor.prototype.addIsTableEditableFunction = |
| function(func) { |
| goog.array.insert(this.isTableEditableFunctions_, func); |
| }; |
| |
| |
| |
| /** |
| * Class representing the selected cell objects within a single table. |
| * @param {goog.dom.AbstractRange} range Selected range from which to calculate |
| * selected cells. |
| * @param {function(Element):Element?} getParentTableFunction A function that |
| * finds the user-editable table from a given element. |
| * @constructor |
| * @private |
| */ |
| goog.editor.plugins.TableEditor.CellSelection_ = |
| function(range, getParentTableFunction) { |
| this.cells_ = []; |
| |
| // Mozilla lets users select groups of cells, with each cell showing |
| // up as a separate range in the selection. goog.dom.Range doesn't |
| // currently support this. |
| // TODO(user): support this case in range.js |
| var selectionContainer = range.getContainerElement(); |
| var elementInSelection = function(node) { |
| // TODO(user): revert to the more liberal containsNode(node, true), |
| // which will match partially-selected cells. We're using |
| // containsNode(node, false) at the moment because otherwise it's |
| // broken in WebKit due to a closure range bug. |
| return selectionContainer == node || |
| selectionContainer.parentNode == node || |
| range.containsNode(node, false); |
| }; |
| |
| var parentTableElement = selectionContainer && |
| getParentTableFunction(selectionContainer); |
| if (!parentTableElement) { |
| return; |
| } |
| |
| var parentTable = new goog.editor.Table(parentTableElement); |
| // It's probably not possible to select a table with no cells, but |
| // do a sanity check anyway. |
| if (!parentTable.rows.length || !parentTable.rows[0].columns.length) { |
| return; |
| } |
| // Loop through cells to calculate dimensions for this CellSelection. |
| for (var i = 0, row; row = parentTable.rows[i]; i++) { |
| for (var j = 0, cell; cell = row.columns[j]; j++) { |
| if (elementInSelection(cell.element)) { |
| // Update dimensions based on cell. |
| if (!this.cells_.length) { |
| this.firstRowIndex_ = cell.startRow; |
| this.lastRowIndex_ = cell.endRow; |
| this.firstColIndex_ = cell.startCol; |
| this.lastColIndex_ = cell.endCol; |
| } else { |
| this.firstRowIndex_ = Math.min(this.firstRowIndex_, cell.startRow); |
| this.lastRowIndex_ = Math.max(this.lastRowIndex_, cell.endRow); |
| this.firstColIndex_ = Math.min(this.firstColIndex_, cell.startCol); |
| this.lastColIndex_ = Math.max(this.lastColIndex_, cell.endCol); |
| } |
| this.cells_.push(cell); |
| } |
| } |
| } |
| this.parentTable_ = parentTable; |
| }; |
| |
| |
| /** |
| * Returns the EditableTable object of which this selection's cells are a |
| * subset. |
| * @return {!goog.editor.Table} the table. |
| */ |
| goog.editor.plugins.TableEditor.CellSelection_.prototype.getTable = |
| function() { |
| return this.parentTable_; |
| }; |
| |
| |
| /** |
| * Returns the row index of the uppermost cell in this selection. |
| * @return {number} The row index. |
| */ |
| goog.editor.plugins.TableEditor.CellSelection_.prototype.getFirstRowIndex = |
| function() { |
| return this.firstRowIndex_; |
| }; |
| |
| |
| /** |
| * Returns the row index of the lowermost cell in this selection. |
| * @return {number} The row index. |
| */ |
| goog.editor.plugins.TableEditor.CellSelection_.prototype.getLastRowIndex = |
| function() { |
| return this.lastRowIndex_; |
| }; |
| |
| |
| /** |
| * Returns the column index of the farthest left cell in this selection. |
| * @return {number} The column index. |
| */ |
| goog.editor.plugins.TableEditor.CellSelection_.prototype.getFirstColumnIndex = |
| function() { |
| return this.firstColIndex_; |
| }; |
| |
| |
| /** |
| * Returns the column index of the farthest right cell in this selection. |
| * @return {number} The column index. |
| */ |
| goog.editor.plugins.TableEditor.CellSelection_.prototype.getLastColumnIndex = |
| function() { |
| return this.lastColIndex_; |
| }; |
| |
| |
| /** |
| * Returns the cells in this selection. |
| * @return {!Array<Element>} Cells in this selection. |
| */ |
| goog.editor.plugins.TableEditor.CellSelection_.prototype.getCells = function() { |
| return this.cells_; |
| }; |
| |
| |
| /** |
| * Returns a boolean value indicating whether or not the cells in this |
| * selection form a rectangle. |
| * @return {boolean} Whether the selection forms a rectangle. |
| */ |
| goog.editor.plugins.TableEditor.CellSelection_.prototype.isRectangle = |
| function() { |
| // TODO(user): check for missing cells. Right now this returns |
| // whether all cells in the selection are in the rectangle, but doesn't |
| // verify that every expected cell is present. |
| if (!this.cells_.length) { |
| return false; |
| } |
| var firstCell = this.cells_[0]; |
| var lastCell = this.cells_[this.cells_.length - 1]; |
| return !(this.firstRowIndex_ < firstCell.startRow || |
| this.lastRowIndex_ > lastCell.endRow || |
| this.firstColIndex_ < firstCell.startCol || |
| this.lastColIndex_ > lastCell.endCol); |
| }; |
| |
| |
| /** |
| * Returns a boolean value indicating whether or not there is exactly |
| * one cell in this selection. Note that this may not be the same as checking |
| * whether getCells().length == 1; if there is a single cell with |
| * rowSpan/colSpan set it will appear multiple times. |
| * @return {boolean} Whether there is exatly one cell in this selection. |
| */ |
| goog.editor.plugins.TableEditor.CellSelection_.prototype.containsSingleCell = |
| function() { |
| var cellCount = this.cells_.length; |
| return cellCount > 0 && |
| (this.cells_[0] == this.cells_[cellCount - 1]); |
| }; |