Path: blob/trunk/third_party/closure/goog/editor/plugins/tableeditor.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 Plugin that enables table editing.16*17* @see ../../demos/editor/tableeditor.html18*/1920goog.provide('goog.editor.plugins.TableEditor');2122goog.require('goog.array');23goog.require('goog.dom');24goog.require('goog.dom.Range');25goog.require('goog.dom.TagName');26goog.require('goog.editor.Plugin');27goog.require('goog.editor.Table');28goog.require('goog.editor.node');29goog.require('goog.editor.range');30goog.require('goog.object');31goog.require('goog.userAgent');32333435/**36* Plugin that adds support for table creation and editing commands.37* @constructor38* @extends {goog.editor.Plugin}39* @final40*/41goog.editor.plugins.TableEditor = function() {42goog.editor.plugins.TableEditor.base(this, 'constructor');4344/**45* The array of functions that decide whether a table element could be46* editable by the user or not.47* @type {Array<function(Element):boolean>}48* @private49*/50this.isTableEditableFunctions_ = [];5152/**53* The pre-bound function that decides whether a table element could be54* editable by the user or not overall.55* @type {function(Node):boolean}56* @private57*/58this.isUserEditableTableBound_ = goog.bind(this.isUserEditableTable_, this);59};60goog.inherits(goog.editor.plugins.TableEditor, goog.editor.Plugin);616263/** @override */64// TODO(user): remove this once there's a sensible default65// implementation in the base Plugin.66goog.editor.plugins.TableEditor.prototype.getTrogClassId = function() {67return String(goog.getUid(this.constructor));68};697071/**72* Commands supported by goog.editor.plugins.TableEditor.73* @enum {string}74*/75goog.editor.plugins.TableEditor.COMMAND = {76TABLE: '+table',77INSERT_ROW_AFTER: '+insertRowAfter',78INSERT_ROW_BEFORE: '+insertRowBefore',79INSERT_COLUMN_AFTER: '+insertColumnAfter',80INSERT_COLUMN_BEFORE: '+insertColumnBefore',81REMOVE_ROWS: '+removeRows',82REMOVE_COLUMNS: '+removeColumns',83SPLIT_CELL: '+splitCell',84MERGE_CELLS: '+mergeCells',85REMOVE_TABLE: '+removeTable'86};878889/**90* Inverse map of execCommand strings to91* {@link goog.editor.plugins.TableEditor.COMMAND} constants. Used to92* determine whether a string corresponds to a command this plugin handles93* in O(1) time.94* @type {Object}95* @private96*/97goog.editor.plugins.TableEditor.SUPPORTED_COMMANDS_ =98goog.object.transpose(goog.editor.plugins.TableEditor.COMMAND);99100101/**102* Whether the string corresponds to a command this plugin handles.103* @param {string} command Command string to check.104* @return {boolean} Whether the string corresponds to a command105* this plugin handles.106* @override107*/108goog.editor.plugins.TableEditor.prototype.isSupportedCommand = function(109command) {110return command in goog.editor.plugins.TableEditor.SUPPORTED_COMMANDS_;111};112113114/** @override */115goog.editor.plugins.TableEditor.prototype.enable = function(fieldObject) {116goog.editor.plugins.TableEditor.base(this, 'enable', fieldObject);117118// enableObjectResizing is supported only for Gecko.119// You can refer to http://qooxdoo.org/contrib/project/htmlarea/html_editing120// for a compatibility chart.121if (goog.userAgent.GECKO) {122var doc = this.getFieldDomHelper().getDocument();123doc.execCommand('enableObjectResizing', false, 'true');124}125};126127128/**129* Returns the currently selected table.130* @return {Element?} The table in which the current selection is131* contained, or null if there isn't such a table.132* @private133*/134goog.editor.plugins.TableEditor.prototype.getCurrentTable_ = function() {135var selectedElement = this.getFieldObject().getRange().getContainer();136return this.getAncestorTable_(selectedElement);137};138139140/**141* Finds the first user-editable table element in the input node's ancestors.142* @param {Node?} node The node to start with.143* @return {Element?} The table element that is closest ancestor of the node.144* @private145*/146goog.editor.plugins.TableEditor.prototype.getAncestorTable_ = function(node) {147var ancestor =148goog.dom.getAncestor(node, this.isUserEditableTableBound_, true);149if (goog.editor.node.isEditable(ancestor)) {150return /** @type {Element?} */ (ancestor);151} else {152return null;153}154};155156157/**158* Returns the current value of a given command. Currently this plugin159* only returns a value for goog.editor.plugins.TableEditor.COMMAND.TABLE.160* @override161*/162goog.editor.plugins.TableEditor.prototype.queryCommandValue = function(163command) {164if (command == goog.editor.plugins.TableEditor.COMMAND.TABLE) {165return !!this.getCurrentTable_();166}167};168169170/** @override */171goog.editor.plugins.TableEditor.prototype.execCommandInternal = function(172command, opt_arg) {173var result = null;174// TD/TH in which to place the cursor, if the command destroys the current175// cursor position.176var cursorCell = null;177var range = this.getFieldObject().getRange();178if (command == goog.editor.plugins.TableEditor.COMMAND.TABLE) {179// Don't create a table if the cursor isn't in an editable region.180if (!goog.editor.range.isEditable(range)) {181return null;182}183// Create the table.184var tableProps = opt_arg || {width: 4, height: 2};185var doc = this.getFieldDomHelper().getDocument();186var table = goog.editor.Table.createDomTable(187doc, tableProps.width, tableProps.height);188range.replaceContentsWithNode(table);189// In IE, replaceContentsWithNode uses pasteHTML, so we lose our reference190// to the inserted table.191// TODO(user): use the reference to the table element returned from192// replaceContentsWithNode.193if (!goog.userAgent.IE) {194cursorCell = goog.dom.getElementsByTagName(goog.dom.TagName.TD, table)[0];195}196} else {197var cellSelection = new goog.editor.plugins.TableEditor.CellSelection_(198range, goog.bind(this.getAncestorTable_, this));199var table = cellSelection.getTable();200if (!table) {201return null;202}203switch (command) {204case goog.editor.plugins.TableEditor.COMMAND.INSERT_ROW_BEFORE:205table.insertRow(cellSelection.getFirstRowIndex());206break;207case goog.editor.plugins.TableEditor.COMMAND.INSERT_ROW_AFTER:208table.insertRow(cellSelection.getLastRowIndex() + 1);209break;210case goog.editor.plugins.TableEditor.COMMAND.INSERT_COLUMN_BEFORE:211table.insertColumn(cellSelection.getFirstColumnIndex());212break;213case goog.editor.plugins.TableEditor.COMMAND.INSERT_COLUMN_AFTER:214table.insertColumn(cellSelection.getLastColumnIndex() + 1);215break;216case goog.editor.plugins.TableEditor.COMMAND.REMOVE_ROWS:217var startRow = cellSelection.getFirstRowIndex();218var endRow = cellSelection.getLastRowIndex();219if (startRow == 0 && endRow == (table.rows.length - 1)) {220// Instead of deleting all rows, delete the entire table.221return this.execCommandInternal(222goog.editor.plugins.TableEditor.COMMAND.REMOVE_TABLE);223}224var startColumn = cellSelection.getFirstColumnIndex();225var rowCount = (endRow - startRow) + 1;226for (var i = 0; i < rowCount; i++) {227table.removeRow(startRow);228}229if (table.rows.length > 0) {230// Place cursor in the previous/first row.231var closestRow = Math.min(startRow, table.rows.length - 1);232cursorCell = table.rows[closestRow].columns[startColumn].element;233}234break;235case goog.editor.plugins.TableEditor.COMMAND.REMOVE_COLUMNS:236var startCol = cellSelection.getFirstColumnIndex();237var endCol = cellSelection.getLastColumnIndex();238if (startCol == 0 && endCol == (table.rows[0].columns.length - 1)) {239// Instead of deleting all columns, delete the entire table.240return this.execCommandInternal(241goog.editor.plugins.TableEditor.COMMAND.REMOVE_TABLE);242}243var startRow = cellSelection.getFirstRowIndex();244var removeCount = (endCol - startCol) + 1;245for (var i = 0; i < removeCount; i++) {246table.removeColumn(startCol);247}248var currentRow = table.rows[startRow];249if (currentRow) {250// Place cursor in the previous/first column.251var closestCol = Math.min(startCol, currentRow.columns.length - 1);252cursorCell = currentRow.columns[closestCol].element;253}254break;255case goog.editor.plugins.TableEditor.COMMAND.MERGE_CELLS:256if (cellSelection.isRectangle()) {257table.mergeCells(258cellSelection.getFirstRowIndex(),259cellSelection.getFirstColumnIndex(),260cellSelection.getLastRowIndex(),261cellSelection.getLastColumnIndex());262}263break;264case goog.editor.plugins.TableEditor.COMMAND.SPLIT_CELL:265if (cellSelection.containsSingleCell()) {266table.splitCell(267cellSelection.getFirstRowIndex(),268cellSelection.getFirstColumnIndex());269}270break;271case goog.editor.plugins.TableEditor.COMMAND.REMOVE_TABLE:272table.element.parentNode.removeChild(table.element);273break;274default:275}276}277if (cursorCell) {278range = goog.dom.Range.createFromNodeContents(cursorCell);279range.collapse(false);280range.select();281}282return result;283};284285286/**287* Checks whether the element is a table editable by the user.288* @param {Node} element The element in question.289* @return {boolean} Whether the element is a table editable by the user.290* @private291*/292goog.editor.plugins.TableEditor.prototype.isUserEditableTable_ = function(293element) {294// Default implementation.295if (element.tagName != goog.dom.TagName.TABLE) {296return false;297}298299// Check for extra user-editable filters.300return goog.array.every(this.isTableEditableFunctions_, function(func) {301return func(/** @type {Element} */ (element));302});303};304305306/**307* Adds a function to filter out non-user-editable tables.308* @param {function(Element):boolean} func A function to decide whether the309* table element could be editable by the user or not.310*/311goog.editor.plugins.TableEditor.prototype.addIsTableEditableFunction = function(312func) {313goog.array.insert(this.isTableEditableFunctions_, func);314};315316317318/**319* Class representing the selected cell objects within a single table.320* @param {goog.dom.AbstractRange} range Selected range from which to calculate321* selected cells.322* @param {function(Element):Element?} getParentTableFunction A function that323* finds the user-editable table from a given element.324* @constructor325* @private326*/327goog.editor.plugins.TableEditor.CellSelection_ = function(328range, getParentTableFunction) {329this.cells_ = [];330331// Mozilla lets users select groups of cells, with each cell showing332// up as a separate range in the selection. goog.dom.Range doesn't333// currently support this.334// TODO(user): support this case in range.js335var selectionContainer = range.getContainerElement();336var elementInSelection = function(node) {337// TODO(user): revert to the more liberal containsNode(node, true),338// which will match partially-selected cells. We're using339// containsNode(node, false) at the moment because otherwise it's340// broken in WebKit due to a closure range bug.341return selectionContainer == node ||342selectionContainer.parentNode == node ||343range.containsNode(node, false);344};345346var parentTableElement =347selectionContainer && getParentTableFunction(selectionContainer);348if (!parentTableElement) {349return;350}351352var parentTable = new goog.editor.Table(parentTableElement);353// It's probably not possible to select a table with no cells, but354// do a sanity check anyway.355if (!parentTable.rows.length || !parentTable.rows[0].columns.length) {356return;357}358// Loop through cells to calculate dimensions for this CellSelection.359for (var i = 0, row; row = parentTable.rows[i]; i++) {360for (var j = 0, cell; cell = row.columns[j]; j++) {361if (elementInSelection(cell.element)) {362// Update dimensions based on cell.363if (!this.cells_.length) {364this.firstRowIndex_ = cell.startRow;365this.lastRowIndex_ = cell.endRow;366this.firstColIndex_ = cell.startCol;367this.lastColIndex_ = cell.endCol;368} else {369this.firstRowIndex_ = Math.min(this.firstRowIndex_, cell.startRow);370this.lastRowIndex_ = Math.max(this.lastRowIndex_, cell.endRow);371this.firstColIndex_ = Math.min(this.firstColIndex_, cell.startCol);372this.lastColIndex_ = Math.max(this.lastColIndex_, cell.endCol);373}374this.cells_.push(cell);375}376}377}378this.parentTable_ = parentTable;379};380381382/**383* Returns the EditableTable object of which this selection's cells are a384* subset.385* @return {!goog.editor.Table} the table.386*/387goog.editor.plugins.TableEditor.CellSelection_.prototype.getTable = function() {388return this.parentTable_;389};390391392/**393* Returns the row index of the uppermost cell in this selection.394* @return {number} The row index.395*/396goog.editor.plugins.TableEditor.CellSelection_.prototype.getFirstRowIndex =397function() {398return this.firstRowIndex_;399};400401402/**403* Returns the row index of the lowermost cell in this selection.404* @return {number} The row index.405*/406goog.editor.plugins.TableEditor.CellSelection_.prototype.getLastRowIndex =407function() {408return this.lastRowIndex_;409};410411412/**413* Returns the column index of the farthest left cell in this selection.414* @return {number} The column index.415*/416goog.editor.plugins.TableEditor.CellSelection_.prototype.getFirstColumnIndex =417function() {418return this.firstColIndex_;419};420421422/**423* Returns the column index of the farthest right cell in this selection.424* @return {number} The column index.425*/426goog.editor.plugins.TableEditor.CellSelection_.prototype.getLastColumnIndex =427function() {428return this.lastColIndex_;429};430431432/**433* Returns the cells in this selection.434* @return {!Array<Element>} Cells in this selection.435*/436goog.editor.plugins.TableEditor.CellSelection_.prototype.getCells = function() {437return this.cells_;438};439440441/**442* Returns a boolean value indicating whether or not the cells in this443* selection form a rectangle.444* @return {boolean} Whether the selection forms a rectangle.445*/446goog.editor.plugins.TableEditor.CellSelection_.prototype.isRectangle =447function() {448// TODO(user): check for missing cells. Right now this returns449// whether all cells in the selection are in the rectangle, but doesn't450// verify that every expected cell is present.451if (!this.cells_.length) {452return false;453}454var firstCell = this.cells_[0];455var lastCell = this.cells_[this.cells_.length - 1];456return !(457this.firstRowIndex_ < firstCell.startRow ||458this.lastRowIndex_ > lastCell.endRow ||459this.firstColIndex_ < firstCell.startCol ||460this.lastColIndex_ > lastCell.endCol);461};462463464/**465* Returns a boolean value indicating whether or not there is exactly466* one cell in this selection. Note that this may not be the same as checking467* whether getCells().length == 1; if there is a single cell with468* rowSpan/colSpan set it will appear multiple times.469* @return {boolean} Whether there is exatly one cell in this selection.470*/471goog.editor.plugins.TableEditor.CellSelection_.prototype.containsSingleCell =472function() {473var cellCount = this.cells_.length;474return cellCount > 0 && (this.cells_[0] == this.cells_[cellCount - 1]);475};476477478