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