tableeditor.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  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 Plugin that enables table editing.
  16. *
  17. * @see ../../demos/editor/tableeditor.html
  18. */
  19. goog.provide('goog.editor.plugins.TableEditor');
  20. goog.require('goog.array');
  21. goog.require('goog.dom');
  22. goog.require('goog.dom.Range');
  23. goog.require('goog.dom.TagName');
  24. goog.require('goog.editor.Plugin');
  25. goog.require('goog.editor.Table');
  26. goog.require('goog.editor.node');
  27. goog.require('goog.editor.range');
  28. goog.require('goog.object');
  29. goog.require('goog.userAgent');
  30. /**
  31. * Plugin that adds support for table creation and editing commands.
  32. * @constructor
  33. * @extends {goog.editor.Plugin}
  34. * @final
  35. */
  36. goog.editor.plugins.TableEditor = function() {
  37. goog.editor.plugins.TableEditor.base(this, 'constructor');
  38. /**
  39. * The array of functions that decide whether a table element could be
  40. * editable by the user or not.
  41. * @type {Array<function(Element):boolean>}
  42. * @private
  43. */
  44. this.isTableEditableFunctions_ = [];
  45. /**
  46. * The pre-bound function that decides whether a table element could be
  47. * editable by the user or not overall.
  48. * @type {function(Node):boolean}
  49. * @private
  50. */
  51. this.isUserEditableTableBound_ = goog.bind(this.isUserEditableTable_, this);
  52. };
  53. goog.inherits(goog.editor.plugins.TableEditor, goog.editor.Plugin);
  54. /** @override */
  55. // TODO(user): remove this once there's a sensible default
  56. // implementation in the base Plugin.
  57. goog.editor.plugins.TableEditor.prototype.getTrogClassId = function() {
  58. return String(goog.getUid(this.constructor));
  59. };
  60. /**
  61. * Commands supported by goog.editor.plugins.TableEditor.
  62. * @enum {string}
  63. */
  64. goog.editor.plugins.TableEditor.COMMAND = {
  65. TABLE: '+table',
  66. INSERT_ROW_AFTER: '+insertRowAfter',
  67. INSERT_ROW_BEFORE: '+insertRowBefore',
  68. INSERT_COLUMN_AFTER: '+insertColumnAfter',
  69. INSERT_COLUMN_BEFORE: '+insertColumnBefore',
  70. REMOVE_ROWS: '+removeRows',
  71. REMOVE_COLUMNS: '+removeColumns',
  72. SPLIT_CELL: '+splitCell',
  73. MERGE_CELLS: '+mergeCells',
  74. REMOVE_TABLE: '+removeTable'
  75. };
  76. /**
  77. * Inverse map of execCommand strings to
  78. * {@link goog.editor.plugins.TableEditor.COMMAND} constants. Used to
  79. * determine whether a string corresponds to a command this plugin handles
  80. * in O(1) time.
  81. * @type {Object}
  82. * @private
  83. */
  84. goog.editor.plugins.TableEditor.SUPPORTED_COMMANDS_ =
  85. goog.object.transpose(goog.editor.plugins.TableEditor.COMMAND);
  86. /**
  87. * Whether the string corresponds to a command this plugin handles.
  88. * @param {string} command Command string to check.
  89. * @return {boolean} Whether the string corresponds to a command
  90. * this plugin handles.
  91. * @override
  92. */
  93. goog.editor.plugins.TableEditor.prototype.isSupportedCommand = function(
  94. command) {
  95. return command in goog.editor.plugins.TableEditor.SUPPORTED_COMMANDS_;
  96. };
  97. /** @override */
  98. goog.editor.plugins.TableEditor.prototype.enable = function(fieldObject) {
  99. goog.editor.plugins.TableEditor.base(this, 'enable', fieldObject);
  100. // enableObjectResizing is supported only for Gecko.
  101. // You can refer to http://qooxdoo.org/contrib/project/htmlarea/html_editing
  102. // for a compatibility chart.
  103. if (goog.userAgent.GECKO) {
  104. var doc = this.getFieldDomHelper().getDocument();
  105. doc.execCommand('enableObjectResizing', false, 'true');
  106. }
  107. };
  108. /**
  109. * Returns the currently selected table.
  110. * @return {Element?} The table in which the current selection is
  111. * contained, or null if there isn't such a table.
  112. * @private
  113. */
  114. goog.editor.plugins.TableEditor.prototype.getCurrentTable_ = function() {
  115. var selectedElement = this.getFieldObject().getRange().getContainer();
  116. return this.getAncestorTable_(selectedElement);
  117. };
  118. /**
  119. * Finds the first user-editable table element in the input node's ancestors.
  120. * @param {Node?} node The node to start with.
  121. * @return {Element?} The table element that is closest ancestor of the node.
  122. * @private
  123. */
  124. goog.editor.plugins.TableEditor.prototype.getAncestorTable_ = function(node) {
  125. var ancestor =
  126. goog.dom.getAncestor(node, this.isUserEditableTableBound_, true);
  127. if (goog.editor.node.isEditable(ancestor)) {
  128. return /** @type {Element?} */ (ancestor);
  129. } else {
  130. return null;
  131. }
  132. };
  133. /**
  134. * Returns the current value of a given command. Currently this plugin
  135. * only returns a value for goog.editor.plugins.TableEditor.COMMAND.TABLE.
  136. * @override
  137. */
  138. goog.editor.plugins.TableEditor.prototype.queryCommandValue = function(
  139. command) {
  140. if (command == goog.editor.plugins.TableEditor.COMMAND.TABLE) {
  141. return !!this.getCurrentTable_();
  142. }
  143. };
  144. /** @override */
  145. goog.editor.plugins.TableEditor.prototype.execCommandInternal = function(
  146. command, opt_arg) {
  147. var result = null;
  148. // TD/TH in which to place the cursor, if the command destroys the current
  149. // cursor position.
  150. var cursorCell = null;
  151. var range = this.getFieldObject().getRange();
  152. if (command == goog.editor.plugins.TableEditor.COMMAND.TABLE) {
  153. // Don't create a table if the cursor isn't in an editable region.
  154. if (!goog.editor.range.isEditable(range)) {
  155. return null;
  156. }
  157. // Create the table.
  158. var tableProps = opt_arg || {width: 4, height: 2};
  159. var doc = this.getFieldDomHelper().getDocument();
  160. var table = goog.editor.Table.createDomTable(
  161. doc, tableProps.width, tableProps.height);
  162. range.replaceContentsWithNode(table);
  163. // In IE, replaceContentsWithNode uses pasteHTML, so we lose our reference
  164. // to the inserted table.
  165. // TODO(user): use the reference to the table element returned from
  166. // replaceContentsWithNode.
  167. if (!goog.userAgent.IE) {
  168. cursorCell = goog.dom.getElementsByTagName(goog.dom.TagName.TD, table)[0];
  169. }
  170. } else {
  171. var cellSelection = new goog.editor.plugins.TableEditor.CellSelection_(
  172. range, goog.bind(this.getAncestorTable_, this));
  173. var table = cellSelection.getTable();
  174. if (!table) {
  175. return null;
  176. }
  177. switch (command) {
  178. case goog.editor.plugins.TableEditor.COMMAND.INSERT_ROW_BEFORE:
  179. table.insertRow(cellSelection.getFirstRowIndex());
  180. break;
  181. case goog.editor.plugins.TableEditor.COMMAND.INSERT_ROW_AFTER:
  182. table.insertRow(cellSelection.getLastRowIndex() + 1);
  183. break;
  184. case goog.editor.plugins.TableEditor.COMMAND.INSERT_COLUMN_BEFORE:
  185. table.insertColumn(cellSelection.getFirstColumnIndex());
  186. break;
  187. case goog.editor.plugins.TableEditor.COMMAND.INSERT_COLUMN_AFTER:
  188. table.insertColumn(cellSelection.getLastColumnIndex() + 1);
  189. break;
  190. case goog.editor.plugins.TableEditor.COMMAND.REMOVE_ROWS:
  191. var startRow = cellSelection.getFirstRowIndex();
  192. var endRow = cellSelection.getLastRowIndex();
  193. if (startRow == 0 && endRow == (table.rows.length - 1)) {
  194. // Instead of deleting all rows, delete the entire table.
  195. return this.execCommandInternal(
  196. goog.editor.plugins.TableEditor.COMMAND.REMOVE_TABLE);
  197. }
  198. var startColumn = cellSelection.getFirstColumnIndex();
  199. var rowCount = (endRow - startRow) + 1;
  200. for (var i = 0; i < rowCount; i++) {
  201. table.removeRow(startRow);
  202. }
  203. if (table.rows.length > 0) {
  204. // Place cursor in the previous/first row.
  205. var closestRow = Math.min(startRow, table.rows.length - 1);
  206. cursorCell = table.rows[closestRow].columns[startColumn].element;
  207. }
  208. break;
  209. case goog.editor.plugins.TableEditor.COMMAND.REMOVE_COLUMNS:
  210. var startCol = cellSelection.getFirstColumnIndex();
  211. var endCol = cellSelection.getLastColumnIndex();
  212. if (startCol == 0 && endCol == (table.rows[0].columns.length - 1)) {
  213. // Instead of deleting all columns, delete the entire table.
  214. return this.execCommandInternal(
  215. goog.editor.plugins.TableEditor.COMMAND.REMOVE_TABLE);
  216. }
  217. var startRow = cellSelection.getFirstRowIndex();
  218. var removeCount = (endCol - startCol) + 1;
  219. for (var i = 0; i < removeCount; i++) {
  220. table.removeColumn(startCol);
  221. }
  222. var currentRow = table.rows[startRow];
  223. if (currentRow) {
  224. // Place cursor in the previous/first column.
  225. var closestCol = Math.min(startCol, currentRow.columns.length - 1);
  226. cursorCell = currentRow.columns[closestCol].element;
  227. }
  228. break;
  229. case goog.editor.plugins.TableEditor.COMMAND.MERGE_CELLS:
  230. if (cellSelection.isRectangle()) {
  231. table.mergeCells(
  232. cellSelection.getFirstRowIndex(),
  233. cellSelection.getFirstColumnIndex(),
  234. cellSelection.getLastRowIndex(),
  235. cellSelection.getLastColumnIndex());
  236. }
  237. break;
  238. case goog.editor.plugins.TableEditor.COMMAND.SPLIT_CELL:
  239. if (cellSelection.containsSingleCell()) {
  240. table.splitCell(
  241. cellSelection.getFirstRowIndex(),
  242. cellSelection.getFirstColumnIndex());
  243. }
  244. break;
  245. case goog.editor.plugins.TableEditor.COMMAND.REMOVE_TABLE:
  246. table.element.parentNode.removeChild(table.element);
  247. break;
  248. default:
  249. }
  250. }
  251. if (cursorCell) {
  252. range = goog.dom.Range.createFromNodeContents(cursorCell);
  253. range.collapse(false);
  254. range.select();
  255. }
  256. return result;
  257. };
  258. /**
  259. * Checks whether the element is a table editable by the user.
  260. * @param {Node} element The element in question.
  261. * @return {boolean} Whether the element is a table editable by the user.
  262. * @private
  263. */
  264. goog.editor.plugins.TableEditor.prototype.isUserEditableTable_ = function(
  265. element) {
  266. // Default implementation.
  267. if (element.tagName != goog.dom.TagName.TABLE) {
  268. return false;
  269. }
  270. // Check for extra user-editable filters.
  271. return goog.array.every(this.isTableEditableFunctions_, function(func) {
  272. return func(/** @type {Element} */ (element));
  273. });
  274. };
  275. /**
  276. * Adds a function to filter out non-user-editable tables.
  277. * @param {function(Element):boolean} func A function to decide whether the
  278. * table element could be editable by the user or not.
  279. */
  280. goog.editor.plugins.TableEditor.prototype.addIsTableEditableFunction = function(
  281. func) {
  282. goog.array.insert(this.isTableEditableFunctions_, func);
  283. };
  284. /**
  285. * Class representing the selected cell objects within a single table.
  286. * @param {goog.dom.AbstractRange} range Selected range from which to calculate
  287. * selected cells.
  288. * @param {function(Element):Element?} getParentTableFunction A function that
  289. * finds the user-editable table from a given element.
  290. * @constructor
  291. * @private
  292. */
  293. goog.editor.plugins.TableEditor.CellSelection_ = function(
  294. range, getParentTableFunction) {
  295. this.cells_ = [];
  296. // Mozilla lets users select groups of cells, with each cell showing
  297. // up as a separate range in the selection. goog.dom.Range doesn't
  298. // currently support this.
  299. // TODO(user): support this case in range.js
  300. var selectionContainer = range.getContainerElement();
  301. var elementInSelection = function(node) {
  302. // TODO(user): revert to the more liberal containsNode(node, true),
  303. // which will match partially-selected cells. We're using
  304. // containsNode(node, false) at the moment because otherwise it's
  305. // broken in WebKit due to a closure range bug.
  306. return selectionContainer == node ||
  307. selectionContainer.parentNode == node ||
  308. range.containsNode(node, false);
  309. };
  310. var parentTableElement =
  311. selectionContainer && getParentTableFunction(selectionContainer);
  312. if (!parentTableElement) {
  313. return;
  314. }
  315. var parentTable = new goog.editor.Table(parentTableElement);
  316. // It's probably not possible to select a table with no cells, but
  317. // do a sanity check anyway.
  318. if (!parentTable.rows.length || !parentTable.rows[0].columns.length) {
  319. return;
  320. }
  321. // Loop through cells to calculate dimensions for this CellSelection.
  322. for (var i = 0, row; row = parentTable.rows[i]; i++) {
  323. for (var j = 0, cell; cell = row.columns[j]; j++) {
  324. if (elementInSelection(cell.element)) {
  325. // Update dimensions based on cell.
  326. if (!this.cells_.length) {
  327. this.firstRowIndex_ = cell.startRow;
  328. this.lastRowIndex_ = cell.endRow;
  329. this.firstColIndex_ = cell.startCol;
  330. this.lastColIndex_ = cell.endCol;
  331. } else {
  332. this.firstRowIndex_ = Math.min(this.firstRowIndex_, cell.startRow);
  333. this.lastRowIndex_ = Math.max(this.lastRowIndex_, cell.endRow);
  334. this.firstColIndex_ = Math.min(this.firstColIndex_, cell.startCol);
  335. this.lastColIndex_ = Math.max(this.lastColIndex_, cell.endCol);
  336. }
  337. this.cells_.push(cell);
  338. }
  339. }
  340. }
  341. this.parentTable_ = parentTable;
  342. };
  343. /**
  344. * Returns the EditableTable object of which this selection's cells are a
  345. * subset.
  346. * @return {!goog.editor.Table} the table.
  347. */
  348. goog.editor.plugins.TableEditor.CellSelection_.prototype.getTable = function() {
  349. return this.parentTable_;
  350. };
  351. /**
  352. * Returns the row index of the uppermost cell in this selection.
  353. * @return {number} The row index.
  354. */
  355. goog.editor.plugins.TableEditor.CellSelection_.prototype.getFirstRowIndex =
  356. function() {
  357. return this.firstRowIndex_;
  358. };
  359. /**
  360. * Returns the row index of the lowermost cell in this selection.
  361. * @return {number} The row index.
  362. */
  363. goog.editor.plugins.TableEditor.CellSelection_.prototype.getLastRowIndex =
  364. function() {
  365. return this.lastRowIndex_;
  366. };
  367. /**
  368. * Returns the column index of the farthest left cell in this selection.
  369. * @return {number} The column index.
  370. */
  371. goog.editor.plugins.TableEditor.CellSelection_.prototype.getFirstColumnIndex =
  372. function() {
  373. return this.firstColIndex_;
  374. };
  375. /**
  376. * Returns the column index of the farthest right cell in this selection.
  377. * @return {number} The column index.
  378. */
  379. goog.editor.plugins.TableEditor.CellSelection_.prototype.getLastColumnIndex =
  380. function() {
  381. return this.lastColIndex_;
  382. };
  383. /**
  384. * Returns the cells in this selection.
  385. * @return {!Array<Element>} Cells in this selection.
  386. */
  387. goog.editor.plugins.TableEditor.CellSelection_.prototype.getCells = function() {
  388. return this.cells_;
  389. };
  390. /**
  391. * Returns a boolean value indicating whether or not the cells in this
  392. * selection form a rectangle.
  393. * @return {boolean} Whether the selection forms a rectangle.
  394. */
  395. goog.editor.plugins.TableEditor.CellSelection_.prototype.isRectangle =
  396. function() {
  397. // TODO(user): check for missing cells. Right now this returns
  398. // whether all cells in the selection are in the rectangle, but doesn't
  399. // verify that every expected cell is present.
  400. if (!this.cells_.length) {
  401. return false;
  402. }
  403. var firstCell = this.cells_[0];
  404. var lastCell = this.cells_[this.cells_.length - 1];
  405. return !(
  406. this.firstRowIndex_ < firstCell.startRow ||
  407. this.lastRowIndex_ > lastCell.endRow ||
  408. this.firstColIndex_ < firstCell.startCol ||
  409. this.lastColIndex_ > lastCell.endCol);
  410. };
  411. /**
  412. * Returns a boolean value indicating whether or not there is exactly
  413. * one cell in this selection. Note that this may not be the same as checking
  414. * whether getCells().length == 1; if there is a single cell with
  415. * rowSpan/colSpan set it will appear multiple times.
  416. * @return {boolean} Whether there is exatly one cell in this selection.
  417. */
  418. goog.editor.plugins.TableEditor.CellSelection_.prototype.containsSingleCell =
  419. function() {
  420. var cellCount = this.cells_.length;
  421. return cellCount > 0 && (this.cells_[0] == this.cells_[cellCount - 1]);
  422. };