Oliverb has uploaded a new change for review.
https://gerrit.wikimedia.org/r/159312
Change subject: Actions for manipulating tables.
......................................................................
Actions for manipulating tables.
ve.ui.TableAction contains actions as well as a generic table manipulation API.
The API does only depend on DM and could be moved to DM world someday.
Change-Id: I261a4169108f5ec64926b9d7af9e352586ab800a
---
A src/ui/actions/ve.ui.TableAction.js
1 file changed, 438 insertions(+), 0 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/VisualEditor/VisualEditor
refs/changes/12/159312/1
diff --git a/src/ui/actions/ve.ui.TableAction.js
b/src/ui/actions/ve.ui.TableAction.js
new file mode 100644
index 0000000..1838f08
--- /dev/null
+++ b/src/ui/actions/ve.ui.TableAction.js
@@ -0,0 +1,438 @@
+/*!
+ * VisualEditor ContentEditable TableNode class.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+/**
+ * Table action.
+ *
+ * @class
+ * @extends ve.ui.Action
+ * @constructor
+ * @param {ve.ui.Surface} surface Surface to act on
+ */
+ve.ui.TableAction = function VeUiTableAction( surface ) {
+ // Parent constructor
+ ve.ui.Action.call( this, surface );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.TableAction, ve.ui.Action );
+
+/* Static Properties */
+
+ve.ui.TableAction.static.name = 'table';
+
+/**
+ * List of allowed methods for the action.
+ *
+ * @static
+ * @property
+ */
+ve.ui.TableAction.static.methods = [ 'create', 'insert', 'delete' ];
+
+/* Methods */
+
+/**
+ * Creates a new table.
+ *
+ * @param {Object} [options] An object with properties 'rows', the number of
rows,
+ * 'cols', the number of columns, and 'header', a boolean indicating whether
a header section should
+ * be created
+ */
+ve.ui.TableAction.prototype.create = function ( options ) {
+ options = options || {};
+ var numberOfCols = options.cols || 4,
+ numberOfRows = options.rows || 3,
+ surface, fragment,
+ data, node, pos, leafs;
+
+ data = [];
+ data.push( { type: 'table' } );
+ if (options.header) {
+ data.push( { type: 'tableSection', 'attributes': { 'style':
'header' } } );
+ data = data.concat(ve.dm.TableRowNode.createData( { style:
'header', cellCount: numberOfCols} ) );
+ data.push( { type: '/tableSection'} );
+ }
+ data.push( { type: 'tableSection', 'attributes': {'style': 'body'} } );
+ for (var i = 0; i < numberOfRows; i++) {
+ data = data.concat(ve.dm.TableRowNode.createData( { style:
'data', cellCount: numberOfCols} ) );
+ }
+ data.push({type: '/tableSection'});
+ data.push({type: '/table'});
+
+ surface = this.surface.getModel();
+ fragment = surface.getFragment(surface.getSelection());
+ fragment.insertContent(data, false).collapseRangeToEnd().select();
+ // set the cursor into the first data cell
+ node = fragment.getSelectedNode();
+ // HACK: there should be a more generic way to retrieve the first data
cell
+ leafs = node.getLeafNodes();
+ pos =
node.children[1].children[0].children[0].children[0].getRange().start;
+ surface.setSelection(new ve.Range(pos, pos));
+};
+
+/**
+ * Inserts a new row or column into the currently focussed table.
+ *
+ * @param {String} [mode] 'row' to insert a new row, and 'col' for a new column
+ * @param {String} [position] 'before' to insert before the current selection,
+ * and 'after' to insert after it
+ */
+ve.ui.TableAction.prototype.insert = function ( mode, position ) {
+ var surface, tableSelection, table, matrix, rect, index;
+ surface = this.surface.getModel();
+ tableSelection = ve.dm.TableNode.lookupSelection(surface.documentModel,
surface.selection);
+ if (!tableSelection) return;
+ // Retrieve the bounding rectangle
+ table = tableSelection.node;
+ matrix = tableSelection.node.matrix;
+ rect = matrix.getRectangle(tableSelection.startCell,
tableSelection.endCell);
+ rect = matrix.getBoundingRectangle(rect);
+ // and insert either before or after
+ index = (position === 'before') ? rect.start[mode] : rect.end[mode];
+ ve.ui.TableAction.insertRowOrCol( surface, table, mode, index, position
);
+};
+
+/**
+ * Deletes selected rows, columns, or the whole table.
+ *
+ * @param {String} [mode] 'row' to delete rows, 'col' for columns, and 'table'
to remove the whole table
+ */
+ve.ui.TableAction.prototype.delete = function ( mode ) {
+ var surface, tableSelection, table, matrix, rect, minIndex, maxIndex;
+ surface = this.surface.getModel();
+ tableSelection = ve.dm.TableNode.lookupSelection(surface.documentModel,
surface.selection);
+ if (!tableSelection) return;
+ // Either delete the table or rows or columns
+ table = tableSelection.node;
+ matrix = tableSelection.node.matrix;
+ if (mode === 'table') {
+ ve.ui.TableAction.deleteTable( surface, tableSelection.node );
+ } else {
+ // Retrieve the bounding rectangle
+ rect = matrix.getRectangle(tableSelection.startCell,
tableSelection.endCell);
+ rect = matrix.getBoundingRectangle(rect);
+ minIndex = rect.start[mode];
+ maxIndex = rect.end[mode];
+ // delete the whole table if all rows or cols get deleted
+ if (minIndex === 0 && maxIndex === table.getSize(mode) - 1) {
+ ve.ui.TableAction.deleteTable( surface, table );
+ } else {
+ ve.ui.TableAction.deleteRowsOrColumns( surface, table,
mode, minIndex, maxIndex );
+ }
+ }
+};
+
+/* Registration */
+
+ve.ui.actionFactory.register( ve.ui.TableAction );
+
+
+// Low-level API
+// -------------
+// TODO This API does only depend on DM code, so it would make sense to
+// move this into DM world later
+
+/**
+ * Deletes a whole table.
+ *
+ * @param {ve.dm.Surface} [surface]
+ * @param {ve.dm.TableNode} [table]
+ */
+ve.ui.TableAction.deleteTable = function( surface, table ) {
+ var tx;
+ tx = ve.dm.Transaction.newFromRemoval(
+ surface.documentModel,
+ table.getOuterRange()
+ );
+ // TODO: maybe a better selection
+ surface.change( tx, new ve.Range(null) );
+};
+
+/**
+ * Inserts a new row or column.
+ *
+ * Example: a new row can be inserted after the 2nd row using
+ *
+ * ve.ui.TableAction.insertRowOrCol( surface, table, 'row', 1, 'after' );
+ *
+ * @param {ve.dm.Surface} [surface]
+ * @param {ve.dm.TableNode} [table]
+ * @param {String} [mode] 'row' or 'col'
+ * @param {Number} [index] row or column index of the base row or column.
+ * @param {String} [insertMode] 'before' or 'after'
+ */
+ve.ui.TableAction.insertRowOrCol = function ( surface, table, mode, index,
insertMode ) {
+ var matrix, refIndex, cells, refCells, before,
+ offset, range, i, txs, updated, inserts, cell, refCell, data,
style;
+
+ before = (insertMode === 'before');
+ matrix = table.matrix;
+
+ // Note: when we insert a new row (or column) we might need to
increment a span property
+ // instead of inserting a new cell.
+ // To achieve this we look at the so called base row and a so called
reference row.
+ // The base row is the one after or before that the new row will be
inserted.
+ // The reference row is the one which is currently at the place of the
new one.
+ // E.g., consider to insert a new row after the second: the base row is
the second, the
+ // reference row is the third.
+ // A span must be increased if the base cell and the reference cell
have the same 'owner'.
+ // E.g.: C* | P**; C | P* | P**, i.e., one of the two cells might be
the owner of the other,
+ // or vice versa, or both a placeholders of a common cell.
+
+
+ // the index of the reference row or column
+ refIndex = index + (before ? -1 : 1);
+ // cells of the selected row or column
+ if (mode === 'row') {
+ cells = matrix.getRow(index) || [];
+ refCells = matrix.getRow(refIndex) || [];
+ } else {
+ cells = matrix.getColumn(index) || [];
+ refCells = matrix.getColumn(refIndex) || [];
+ }
+
+ txs = [];
+ updated = {};
+ inserts = [];
+
+ for (i = 0; i < cells.length; i++) {
+ cell = cells[i];
+ refCell = refCells[i];
+ // detect if span update is necessary
+ if (refCell && (cell.type === 'placeholder' || refCell.type ===
'placeholder') ) {
+ if (cell.node === refCell.node) {
+ cell = cell.owner || cell;
+ if (!updated[cell.key]) {
+ // Note: we can record the span
modification, as it will not mess range indexes.
+
txs.push(ve.ui.TableAction.incrementSpan(surface, cell, mode));
+ updated[cell.key] = true;
+ }
+ continue;
+ }
+ }
+ // If it is not a span changer, we record the base cell as a
reference for insertion
+ inserts.push(cell);
+ }
+
+ // Inserting a new row differs completely from inserting a new column:
+ // For a new row, a new row node is created, and inserted relative to
an existing row node.
+ // For a new column, new cells are inserted into existing row nodes at
appropriate positions,
+ // i.e., relative to an existing cell node.
+ if (mode === 'row') {
+ data = ve.dm.TableRowNode.createData({
+ cellCount: inserts.length,
+ // taking the style of the first cell of the selected
row
+ style: cells[0].node.getStyle()
+ });
+ range = matrix.getRowNode(index).getOuterRange();
+ offset = before ? range.start: range.end;
+ txs.push(ve.dm.Transaction.newFromInsertion(
surface.documentModel, offset, data ));
+ } else {
+ // making sure that the inserts are in descending order
+ // so that the transactions do not interfer with respect to
ranges.
+ inserts.sort(ve.dm.TableMatrix.Cell.sortDescending);
+
+ // For inserting a new cell we need to find a reference cell
node
+ // which we can use to get a proper insertion offset.
+ for (i = 0; i < inserts.length; i++) {
+ cell = inserts[i];
+ // if the cell is a placeholder, this will find a close
cell node in the same row
+ refCell = matrix.findClosestCell(cell);
+ if (refCell) {
+ range = refCell.node.getOuterRange();
+ // if the found cell is before the base cell
the new cell must be placed after it, in any case,
+ // Only if the base cell is not a placeholder
we have to consider the insert mode.
+ if ( refCell.col < cell.col || ( refCell.col
=== cell.col && !before ) ) {
+ offset = range.end;
+ } else {
+ offset = range.start;
+ }
+ style = refCell.node.getStyle();
+ } else {
+ // if there are only placeholders in the row,
we use the row node's inner range
+ // for the insertion offset
+ range = matrix.getRowNode(cell.row).getRange();
+ offset = before ? range.start: range.end;
+ style = cells[0].node.getStyle();
+ }
+ data = ve.dm.TableCellNode.createData({ style: style });
+ txs.push(ve.dm.Transaction.newFromInsertion(
surface.documentModel, offset, data ));
+ }
+ }
+ surface.change(txs);
+};
+
+/**
+ * Increase the span of a cell by one.
+ *
+ * @param {ve.dm.Surface} [surface]
+ * @param {ve.dm.TableMatrix.Cell} [cell]
+ * @param {String} [mode] 'row' or 'col'
+ */
+ve.ui.TableAction.incrementSpan = function( surface, cell, mode ) {
+ var attr = (mode === 'row') ? 'rowspan' : 'colspan',
+ data = {};
+ data[attr] = cell.node.getSpan(mode) + 1;
+ return ve.dm.Transaction.newFromAttributeChanges(
surface.documentModel, cell.node.getOuterRange().start, data);
+};
+
+/**
+ * Deletes currently rows or columns within a given range.
+ *
+ * E.g., the rows 2-4 can be deleted using
+ *
+ * ve.ui.TableAction.deleteRowsOrColumns( surface, table, 'row', 1, 3 );
+ *
+ * @param {ve.dm.Surface} [surface]
+ * @param {ve.dm.TableNode} [table]
+ * @param {String} [mode] 'row' or 'col'
+ * @param {Number} [minIndex] smallest row or column index to be deleted
+ * @param {Number} [maxIndex] largest row or column index to be deleted
(inclusive)
+ */
+ve.ui.TableAction.deleteRowsOrColumns = function ( surface, table, mode,
minIndex, maxIndex ) {
+ var cells, row, col, i, cell, key, matrix,
+ span, startRow, startCol, endRow, endCol, rowNode,
+ txs, adapted, actions;
+
+ cells = [];
+ txs = [];
+ adapted = {};
+ actions = [];
+ matrix = table.matrix;
+
+ // Deleting cells can have two additional consequences:
+ // 1. The cell is a Placeholder. The owner's span might be decreased.
+ // 2. The cell is owner of placeholders which get orphaned by the
deletion.
+ // New, empty cells much be inserted to replace the placeholders and
keep the
+ // table in proper shape.
+ // Insertions and deletions of cells must be done in an appropriate
order, so that the transactions
+ // do not interfer with each other. To achieve that, we record
insertions and deletions and
+ // sort them by the position of the cell (row, column) in the table
matrix.
+
+ if (mode === 'row') {
+ for (row = minIndex; row <= maxIndex; row++) {
+ cells = cells.concat(matrix.getRow(row));
+ }
+ } else {
+ for (col = minIndex; col <= maxIndex; col++) {
+ cells = cells.concat(matrix.getColumn(col));
+ }
+ }
+
+ for (i = 0; i < cells.length; i++) {
+ cell = cells[i];
+
+ if (cell.type === 'placeholder') {
+ key = cell.owner.key;
+ if (!adapted[key]) {
+ // Note: we can record this transaction
already, as it does not have an effect on the
+ // node range
+
txs.push(ve.ui.TableAction.decreaseSpan(surface, cell.owner, mode, minIndex,
maxIndex));
+ adapted[key] = true;
+ }
+ continue;
+ }
+
+ // Detect if the owner of a spanning cell gets deleted and
+ // leaves orpaned placeholders
+ span = cell.node.getSpan(mode);
+ if (cell[mode] + span - 1 > maxIndex) {
+ // add inserts for orphaned place holders
+ startRow = (mode === 'col') ? cell.row : maxIndex +
1;
+ startCol = (mode === 'col') ? maxIndex + 1 : cell.col;
+ endRow = cell.row + cell.node.getSpan('row') - 1;
+ endCol = cell.col + cell.node.getSpan('col') - 1;
+
+ // Record the insertion to apply it later
+ for (row = startRow; row <= endRow; row++) {
+ for (col = startCol; col <= endCol; col++) {
+ actions.push({ action: 'insert', cell:
matrix.getCell(row, col) });
+ }
+ }
+ }
+
+ // Cell nodes only get deleted when deleting columns (otherwise
row nodes)
+ if (mode === 'col') {
+ actions.push({action: 'delete', cell: cell });
+ }
+ }
+
+ // sort recorded actions to make sure the transactions will not
interfer with respect to offsets
+ actions.sort(function(a, b) {
+ // sorts first by row, then by column (corresponding to HTML
flow in tables)
+ return ve.dm.TableMatrix.Cell.sortDescending(a.cell, b.cell);
+ });
+
+ if (mode === 'row') {
+ // first replace orphaned placeholders which are below the last
deleted row,
+ // thus, this works with regard to transaction offsets
+ for (i = 0; i < actions.length; i++) {
+ txs.push(ve.ui.TableAction.replacePlaceholder(surface,
table, actions[i].cell));
+ }
+ // remove rows in reverse order to have valid transaction
offsets
+ for (row = maxIndex; row >= minIndex; row--) {
+ rowNode = matrix.getRowNode(row);
+ txs.push( ve.dm.Transaction.newFromRemoval(
surface.documentModel, rowNode.getOuterRange() ) );
+ }
+ } else {
+ for (i = 0; i < actions.length; i++) {
+ if (actions[i].action === 'insert') {
+ txs.push( ve.ui.TableAction.replacePlaceholder(
surface, table, actions[i].cell ) );
+ } else {
+ txs.push( ve.dm.Transaction.newFromRemoval(
surface.documentModel, actions[i].cell.node.getOuterRange() ) );
+ }
+ }
+ }
+ surface.change( txs, new ve.Range(null) );
+};
+
+/**
+ * Decreases the span of a cell so that the given interval is removed.
+ *
+ * @param {ve.dm.Surface} [surface]
+ * @param {ve.dm.TableMatrix.Cell} [cell]
+ * @param {String} [mode] 'row' or 'col'
+ * @param {Number} [minIndex] smallest row or column index
+ * @param {Number} [maxIndex] largest row or column index
+ */
+ve.ui.TableAction.decreaseSpan = function ( surface, cell, mode, minIndex,
maxIndex) {
+ var newSpan, span, data, attr;
+ attr = (mode === 'row') ? 'rowspan' : 'colspan';
+ span = cell.node.getSpan(mode);
+ data = {};
+ newSpan = (minIndex - cell[mode]) + Math.max(0, cell[mode] + span - 1 -
maxIndex);
+ data[attr] = newSpan;
+ return ve.dm.Transaction.newFromAttributeChanges(
surface.documentModel, cell.node.getOuterRange().start, data );
+};
+
+/**
+ * Inserts a new cell for an orphaned placeholder.
+ *
+ * @param {ve.dm.Surface} [surface]
+ * @param {ve.dm.TableNode} [table]
+ * @param {ve.dm.TableMatrix.Placeholder} [placeholder]
+ */
+ve.ui.TableAction.replacePlaceholder = function ( surface, table, placeholder
) {
+ var refCell, range, offset, data, style, matrix;
+ matrix = table.matrix;
+ // For inserting the new cell a reference cell node
+ // which is used to get an insertion offset.
+ refCell = matrix.findClosestCell(placeholder);
+ if (refCell) {
+ range = refCell.node.getOuterRange();
+ offset = (placeholder.col < refCell.col) ? range.start :
range.end;
+ style = refCell.node.getStyle();
+ } else {
+ // if there are only placeholders in the row, the row node's
inner range is used
+ range = matrix.getRowNode(placeholder.row).getRange();
+ offset = range.start;
+ style = placeholder.node.getStyle();
+ }
+ data = ve.dm.TableCellNode.createData({ style: style });
+ return ve.dm.Transaction.newFromInsertion( surface.documentModel,
offset, data );
+};
--
To view, visit https://gerrit.wikimedia.org/r/159312
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I261a4169108f5ec64926b9d7af9e352586ab800a
Gerrit-PatchSet: 1
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Oliverb <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits