Oliverb has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/159310

Change subject: Data model implementation for tables.
......................................................................

Data model implementation for tables.

Introducing dm.TableMatrix, a companion for dm.TableNode that provides a
non-sparse representation of tables with spanning cells.

Change-Id: Ib4afcbaf157fb64666f52c9650f3260bb42f2a3c
---
M src/dm/nodes/ve.dm.TableCellNode.js
A src/dm/nodes/ve.dm.TableMatrix.js
M src/dm/nodes/ve.dm.TableNode.js
M src/dm/nodes/ve.dm.TableRowNode.js
M src/dm/nodes/ve.dm.TableSectionNode.js
5 files changed, 681 insertions(+), 11 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/VisualEditor/VisualEditor 
refs/changes/10/159310/1

diff --git a/src/dm/nodes/ve.dm.TableCellNode.js 
b/src/dm/nodes/ve.dm.TableCellNode.js
index 5849029..63b4e21 100644
--- a/src/dm/nodes/ve.dm.TableCellNode.js
+++ b/src/dm/nodes/ve.dm.TableCellNode.js
@@ -18,6 +18,14 @@
 ve.dm.TableCellNode = function VeDmTableCellNode() {
        // Parent constructor
        ve.dm.BranchNode.apply( this, arguments );
+
+       /* Events */
+
+       this.connect( this, {
+               'attach': 'onAttach',
+               'detach': 'onDetach',
+               'attributeChange': 'onAttributeChange'
+       } );
 };
 
 /* Inheritance */
@@ -30,20 +38,73 @@
 
 ve.dm.TableCellNode.static.parentNodeTypes = [ 'tableRow' ];
 
-ve.dm.TableCellNode.static.defaultAttributes = {
-       style: 'data'
-};
+ve.dm.TableCellNode.static.defaultAttributes = { style: 'data' };
 
 ve.dm.TableCellNode.static.matchTagNames = [ 'td', 'th' ];
 
+// blacklisting 'colspan' and 'rowspan' as they are managed explicitely
+ve.dm.TableCellNode.static.storeHtmlAttributes = {
+       blacklist: ['colspan', 'rowspan']
+};
+
 ve.dm.TableCellNode.static.toDataElement = function ( domElements ) {
-       var style = domElements[0].nodeName.toLowerCase() === 'th' ? 'header' : 
'data';
-       return { type: this.name, attributes: { style: style } };
+       var attributes = {
+               style: domElements[0].nodeName.toLowerCase() === 'th' ? 
'header' : 'data',
+               colspan: parseInt(domElements[0].getAttribute('colspan'), 10) 
|| undefined,
+               rowspan: parseInt(domElements[0].getAttribute('rowspan'), 10) 
|| undefined
+       };
+       return { type: this.name, attributes:  attributes};
 };
 
 ve.dm.TableCellNode.static.toDomElements = function ( dataElement, doc ) {
-       var tag = dataElement.attributes && dataElement.attributes.style === 
'header' ? 'th' : 'td';
-       return [ doc.createElement( tag ) ];
+       var tag = dataElement.attributes && dataElement.attributes.style === 
'header' ? 'th' : 'td',
+               colspan = dataElement.attributes.colspan,
+               rowspan = dataElement.attributes.rowspan,
+               el = doc.createElement( tag );
+       if (colspan && colspan > 1) el.setAttribute('colspan', colspan);
+       if (rowspan && rowspan > 1) el.setAttribute('rowspan', rowspan);
+       return [ el ];
+};
+
+/* Prototype functions */
+
+ve.dm.TableCellNode.prototype.getSpan = function ( rowOrCol ) {
+       var key = (rowOrCol || 'col') + 'span';
+       return this.element.attributes[key] || 1;
+};
+
+ve.dm.TableCellNode.prototype.getStyle = function () {
+       return this.element.attributes.style || 'data';
+};
+
+ve.dm.TableCellNode.prototype.onAttach = function( to ) {
+       if (to.onStructureChange) to.onStructureChange( { cell: this } );
+};
+
+ve.dm.TableCellNode.prototype.onDetach = function( from ) {
+       if (from.onStructureChange) from.onStructureChange( { cell: this } );
+};
+
+ve.dm.TableCellNode.prototype.onAttributeChange = function( key ) {
+       if ( this.parent && (key === 'colspan' || key === 'rowspan')) {
+               this.parent.onStructureChange({ cell: this });
+       }
+};
+
+/**
+ * Creates data that can be inserted into the model to create a new table cell.
+ *
+ * @param {Object} [options] An object with property 'style' which can be 
either 'header' or 'data'.
+ * @return {Array} Model data for a new table cell
+ */
+ve.dm.TableCellNode.createData = function( options ) {
+       options = options || {};
+       return [
+               {type: 'tableCell', 'attributes': { 'style': options.style || 
'data' } },
+                       {type: 'paragraph'},
+                       {type: '/paragraph'},
+               {type: '/tableCell'}
+       ];
 };
 
 /* Registration */
diff --git a/src/dm/nodes/ve.dm.TableMatrix.js 
b/src/dm/nodes/ve.dm.TableMatrix.js
new file mode 100644
index 0000000..ebbec6c
--- /dev/null
+++ b/src/dm/nodes/ve.dm.TableMatrix.js
@@ -0,0 +1,362 @@
+/**
+ * A helper class that allows random access to the table cells
+ * and introduces place-holders for fields occupied by spanning cells,
+ * making it a non-sparse representation of the sparse HTML model.
+ * This is essential for the implementation of table manipulations, such as 
row insertions or deletions.
+ *
+ * Example:
+ *
+ * <table>
+ *   <tr><td rowspan=2>1</td><td colspan=2>2</td><td rowspan=2 
colspan=2>3</td></tr>
+ *   <tr><td>4</td><td>5</td></tr>
+ * </table>
+ *
+ * Visually this table would look like:
+ *
+ *  -------------------
+ * | 1 | 2     | 3     |
+ * |   |-------|       |
+ * |   | 4 | 5 |       |
+ *  -------------------
+ *
+ * The HTML model is sparse which makes it hard to read but also difficult to 
work with programmatically.
+ * The corresponding TableCellMatrix would look like:
+ *
+ * | C[1] | C[2] | P[2] | C[3] | P[3] |
+ * | P[1] | C[4] | C[5] | P[3] | P[3] |
+ *
+ * Where C[1] represents a Cell instance wrapping cell 1,
+ * and P[1] a PlaceHolder instance owned by that cell.
+ *
+ * @class
+ * @constructor
+ * @param {ve.dm.TableNode} [tableNode] Reference to a table instance
+ */
+ve.dm.TableMatrix = function VeDmTableMatrix( tableNode ) {
+       this.tableNode = tableNode;
+       // Do not access these directly as they get invalidated on structural 
changes
+       // Use the accessor methods instead.
+       this._matrix = null;
+       this._rowNodes = null;
+};
+
+/**
+ * Invalidates the matrix structure.
+ *
+ * This is called by ve.dm.TableNode on structural changes.
+ */
+ve.dm.TableMatrix.prototype.invalidate = function() {
+       this._matrix = null;
+       this._rowNodes = null;
+};
+
+/**
+ * Recreates the matrix structure.
+ */
+ve.dm.TableMatrix.prototype.update = function() {
+       var cellNode, cell,
+               rowSpan, colSpan, i, j, _row, _col,
+               matrix = [],
+               rowNodes = [],
+               iterator = this.tableNode.getIterator(),
+               row = -1, col = -1;
+
+       // hook to react on row transitions
+       iterator.onNewRow = function( rowNode ) {
+               row++; col = -1;
+               // initialize a matrix row
+               matrix[row] = matrix[row] || [];
+               // store the row node
+               rowNodes.push(rowNode);
+       };
+
+       // Iterates through all cells and stores the cells as well as
+       // so called placeholders into the matrix.
+       while ((cellNode = iterator.next()) !== null)  {
+               col++;
+               // skip placeholders
+               while (matrix[row][col]) {
+                       col++;
+               }
+               cell = new ve.dm.TableMatrix.Cell(cellNode, row, col);
+               // store the cell in the matrix
+               matrix[row][col] = cell;
+               // add place holders for spanned cells
+               rowSpan = cellNode.getSpan('row');
+               colSpan = cellNode.getSpan('col');
+               if (rowSpan === 1 && colSpan === 1) continue;
+               for (i = 0; i < rowSpan; i++) {
+                       for (j = 0; j < colSpan; j++) {
+                               if (i===0 && j===0) continue;
+                               _row = row + i;
+                               _col = col + j;
+                               // initialize the cell matrix row if not yet 
present
+                               matrix[_row] = matrix[_row] || [];
+                               matrix[_row][_col] = new 
ve.dm.TableMatrix.Placeholder(cell, _row, _col);
+                       }
+               }
+       }
+       this._matrix = matrix;
+       this._rowNodes = rowNodes;
+};
+
+/**
+ * Retrieves a single cell.
+ *
+ * @param {Number} [row]
+ * @param {Number} [col]
+ * @return {ve.dm.TableMatrix.Cell}
+ */
+ve.dm.TableMatrix.prototype.getCell = function(row, col) {
+       var matrix = this.getMatrix();
+       return matrix[row][col];
+};
+
+/**
+ * Retrieves all cells of a column with given index.
+ *
+ * @param {Number} [col]
+ * @return {ve.dm.TableMatrix.Cell[]} The cells of a column
+ */
+ve.dm.TableMatrix.prototype.getColumn = function(col) {
+       var cells, row,
+               matrix = this.getMatrix();
+       cells = [];
+       for (row = 0; row < matrix.length; row++) {
+               cells.push(matrix[row][col]);
+       }
+       return cells;
+};
+
+/**
+ * Retrieves all cells of a row with given index.
+ *
+ * @param {Number} [row]
+ * @return {ve.dm.TableMatrix.Cell[]} The cells of a row
+ */
+ve.dm.TableMatrix.prototype.getRow = function(row) {
+       var matrix = this.getMatrix();
+       return matrix[row];
+};
+
+/**
+ * Retrieves the row node of a row with given index.
+ *
+ * @param {Number} [row]
+ * @return {ve.dm.TableRowNode}
+ */
+ve.dm.TableMatrix.prototype.getRowNode = function(row) {
+       var rowNodes = this.getRowNodes();
+       return rowNodes[row];
+};
+
+/**
+ * Provides a reference to the internal cell matrix.
+ *
+ * Note: this is primarily for internal use. Do not change the delivered matrix
+ * and do not store as it may be invalidated.
+ *
+ * @return {ve.dm.TableMatrix.Cell[]}
+ */
+ve.dm.TableMatrix.prototype.getMatrix = function() {
+       if (!this._matrix) this.update();
+       return this._matrix;
+};
+
+/**
+ * Provides a reference to the internal array of row nodes.
+ *
+ * Note: this is primarily for internal use. Do not change the delivered array
+ * and do not store it as it may be invalidated.
+ *
+ * @return {ve.dm.TableRowNode[]}
+ */
+ve.dm.TableMatrix.prototype.getRowNodes = function() {
+       if (!this._rowNodes) this.update();
+       return this._rowNodes;
+};
+
+/**
+ * Computes a the rectangle for a given start and end cell node.
+ *
+ *
+ * @param {ve.dm.TableCellNode} [startCellNode] start anchor
+ * @param {ve.dm.TableCellNode} [endCellNode] end anchor
+ * @return {ve.dm.TableMatrix.Rectangle}
+ */
+ve.dm.TableMatrix.prototype.getRectangle = function ( startCellNode, 
endCellNode ) {
+       var startCell, endCell, minRow, maxRow, minCol, maxCol;
+       startCell = this.lookupCell(startCellNode);
+       if (!startCell) return null;
+       if (startCellNode === endCellNode) {
+               endCell = startCell;
+       } else {
+               endCell = this.lookupCell(endCellNode);
+       }
+       minRow = Math.min(startCell.row, endCell.row);
+       maxRow = Math.max(startCell.row, endCell.row);
+       minCol = Math.min(startCell.col, endCell.col);
+       maxCol = Math.max(startCell.col, endCell.col);
+       return new ve.dm.TableMatrix.Rectangle(minRow, minCol, maxRow, maxCol);
+};
+
+/**
+ * Retrieves all cells (no placeholders) within a given rectangle.
+ *
+ * @param {ve.dm.TableMatrix.Rectangle} [rect]
+ * @return {ve.dm.TableMatrix.Cell[]}
+ */
+ve.dm.TableMatrix.prototype.getCellsForRectangle = function ( rect ) {
+       var row, col, cells, visited, cell;
+       cells = [];
+       visited = {};
+       for (row = rect.start.row; row <= rect.end.row; row++) {
+               for (col = rect.start.col; col <= rect.end.col; col++) {
+                       cell = this.getCell(row, col);
+                       if (cell.type === 'placeholder') cell = cell.owner;
+                       if (!visited[cell.key]) {
+                               cells.push(cell);
+                               visited[cell.key] = true;
+                       }
+               }
+       }
+       return cells;
+};
+
+/**
+ * Retrieves a bounding rectangle for all cells described by a given rectangle.
+ * This takes spanning cells into account.
+ *
+ * @param {ve.dm.TableMatrix.Rectangle} [rect]
+ * @return {ve.dm.TableMatrix.Rectangle} A new rectangle
+ */
+ve.dm.TableMatrix.prototype.getBoundingRectangle = function (rect) {
+       var cells, cell, i;
+       rect = ve.dm.TableMatrix.Rectangle.copy(rect);
+       cells = this.getCellsForRectangle(rect);
+       if (!cells || cells.length === 0) return null;
+       for (i = 0; i < cells.length; i++) {
+               cell = cells[i];
+               rect.start.row = Math.min(rect.start.row, cell.row);
+               rect.start.col = Math.min(rect.start.col, cell.col);
+               rect.end.row = Math.max(rect.end.row, cell.row + 
cell.node.getSpan('row') - 1);
+               rect.end.col = Math.max(rect.end.col, cell.col + 
cell.node.getSpan('col') - 1);
+       }
+       return rect;
+};
+
+/**
+ * Provides a tuple with number of rows and columns.
+ *
+ * @return {Number[2]} (number of rows) X (number of columns)
+ */
+ve.dm.TableMatrix.prototype.getSize = function () {
+       var matrix = this.getMatrix();
+       if (matrix.length === 0) {
+               return [0, 0];
+       } else {
+               return [matrix.length, matrix[0].length];
+       }
+};
+
+/**
+ * Looks up the cell for a given cell node.
+ *
+ * @return {ve.dm.TableMatrix.Cell} The cell or null if not found
+ */
+ve.dm.TableMatrix.prototype.lookupCell = function( cellNode ) {
+       var row, col, cell, rowCells,
+               matrix = this.getMatrix(),
+               rowNodes = this.getRowNodes();
+       row = rowNodes.indexOf(cellNode.parent);
+       if (row < 0) return null;
+       cell = null;
+       rowCells = matrix[row];
+       for (col = 0; col < rowCells.length; col++) {
+               cell = rowCells[col];
+               if (cell.node === cellNode) {
+                       break;
+               }
+       }
+       return cell;
+};
+
+/**
+ * Finds the closest cell not being a placeholder for a given cell.
+ *
+ * @return {ve.dm.TableMatrix.Cell}
+ */
+ve.dm.TableMatrix.prototype.findClosestCell = function(cell) {
+       var col, rowCells,
+               matrix = this.getMatrix();
+       rowCells = matrix[cell.row];
+       for (col = cell.col; col >= 0; col--) {
+               if (rowCells[col].type === 'cell') return rowCells[col];
+       }
+       for (col = cell.col + 1; col < rowCells.length; col++) {
+               if (rowCells[col].type === 'cell') return rowCells[col];
+       }
+       return null;
+};
+
+/**
+ * An object wrapping a table cell node, augmenting it with row and column 
indexes.
+ *
+ * @class
+ * @constructor
+ * @param {ve.dm.TableCellNode} [node]
+ * @param {Number} [row] row index
+ * @param {Number} [col] column index
+ */
+ve.dm.TableMatrix.Cell = function Cell(node, row, col) {
+       this.type = 'cell';
+       this.node = node;
+       this.row = row;
+       this.col = col;
+       this.key = row + '_' + col;
+};
+
+ve.dm.TableMatrix.Cell.sortDescending = function( a, b ) {
+       if (a.row !== b.row) return b.row - a.row;
+       return b.col - a.col;
+};
+
+/**
+ * An object representing a cell which is occupied by another cell with 
'rowspan' or 'colspan' attribute.
+ * Placeholders are used to create a dense representation of the sparse HTML 
table model.
+ *
+ * @class
+ * @constructor
+ * @param {ve.dm.TableMatrix.Cell} [owner]
+ * @param {Number} [row] row index
+ * @param {Number} [col] column index
+ */
+ve.dm.TableMatrix.Placeholder = function PlaceHolder( owner, row, col ) {
+       ve.dm.TableMatrix.Cell.call(this, owner.node, row, col);
+       this.type = 'placeholder';
+       this.owner = owner;
+};
+
+OO.inheritClass( ve.dm.TableMatrix.Placeholder, ve.dm.TableMatrix.Cell );
+
+/**
+ * An object describing a rectangular selection in a table matrix.
+ * It has two properties, 'start' and 'end', which both are objects with
+ * properties 'row' and 'col'. 'start' describes the upper-left, and
+ * 'end' the lower-right corner of the rectangle.
+ *
+ * @class
+ * @constructor
+ * @param {Number} [minRow] row index of upper-left corner
+ * @param {Number} [minCol] column index of upper-left corner
+ * @param {Number} [maxRow] row index of lower-left corner
+ * @param {Number} [maxCol] column index of lower-left corner
+ */
+ve.dm.TableMatrix.Rectangle = function( minRow, minCol, maxRow, maxCol ) {
+       this.start = { row: minRow, col: minCol };
+       this.end = { row: maxRow, col: maxCol };
+};
+
+ve.dm.TableMatrix.Rectangle.copy = function( rect ) {
+       return new ve.dm.TableMatrix.Rectangle(rect.start.row, rect.start.col, 
rect.end.row, rect.end.col);
+};
diff --git a/src/dm/nodes/ve.dm.TableNode.js b/src/dm/nodes/ve.dm.TableNode.js
index 28f0c32..1107fe6 100644
--- a/src/dm/nodes/ve.dm.TableNode.js
+++ b/src/dm/nodes/ve.dm.TableNode.js
@@ -12,12 +12,15 @@
  * @extends ve.dm.BranchNode
  *
  * @constructor
- * @param {Object} [element] Reference to element in linear model
  * @param {ve.dm.Node[]} [children]
  */
 ve.dm.TableNode = function VeDmTableNode() {
        // Parent constructor
        ve.dm.BranchNode.apply( this, arguments );
+
+       // A dense representation of the sparse model to make manipulations
+       // in presence of spanning cells feasible.
+       this.matrix = new ve.dm.TableMatrix(this);
 };
 
 /* Inheritance */
@@ -32,6 +35,184 @@
 
 ve.dm.TableNode.static.matchTagNames = [ 'table' ];
 
+/* Prototype functions */
+
+ve.dm.TableNode.prototype.onStructureChange = function(context) {
+       this.matrix.invalidate();
+       this.emit('tableStructureChange', context);
+};
+
+/**
+ * Provides a cell iterator that allows convenient traversal regardless of
+ * the structure with respect to sections.
+ *
+ * @return {ve.dm.TableNode.CellIterator}
+ */
+ve.dm.TableNode.prototype.getIterator = function() {
+       return new ve.dm.TableNode.CellIterator(this);
+};
+
+/**
+ * Provides a cell iterator that allows convenient traversal regardless of
+ * the structure with respect to sections.
+ *
+ * @param { String|undefined } [dimension] The dimension asked for; 'row' for 
number of rows,
+ *     'col' for number of cols, or no argument for retrieving a tuple.
+ * @return { Number|Array[2] } The size in the given dimension or a tuple.
+ */
+ve.dm.TableNode.prototype.getSize = function ( dimension ) {
+       var dim = this.matrix.getSize();
+       if ( dimension === 'row' ) {
+               return dim[0];
+       } else if ( dimension === 'col' ) {
+               return dim[1];
+       } else {
+               return dim;
+       }
+};
+
+ve.dm.TableNode.prototype.getRectangle = function ( startCellNode, endCellNode 
) {
+       return this.matrix.getRectangle(startCellNode, endCellNode);
+};
+
+/**
+ * Find a table in the document which contains a given selection.
+ *
+ * @param {ve.dm.DocumentNode} [documentNode] The document model
+ * @param {Object} [selection] A dm.Range that must be contained by the table
+ * @return {Object} An object with properties 'node', 'startCell', 'endCell'
+ */
+ve.dm.TableNode.lookupSelection = function ( documentNode, selection ) {
+       var start, end;
+       if (!selection) {
+               return null;
+       }
+       if (selection.isBackwards()) {
+               selection = selection.flip();
+       }
+       // find the outer-most table which includes both selection anchors
+       if (selection.isCollapsed()) {
+               start = ve.dm.TableNode.findTableForOffset(documentNode, 
selection.start);
+               end = start;
+       } else {
+               start = ve.dm.TableNode.findTableForOffset(documentNode, 
selection.start, selection.end);
+               end = ve.dm.TableNode.findTableForOffset(documentNode, 
selection.end, selection.start);
+       }
+       if (!start || !end) {
+               return null;
+       }
+       return {
+               node: start.tableNode,
+               startCell: start.cellNode,
+               endCell: end.cellNode
+       };
+};
+
+/**
+ * Find a table starting from a node with given offset that contains another 
constraint offset.
+ *
+ * @param {ve.dm.DocumentNode} [documentNode] The document model
+ * @param {Number} [offset] Offset of the node to start from
+ * @param {Number} [constraint] Offset which must be contained too
+ * @return {Object} An object with properties 'tableNode', 'cellNode'
+ */
+ve.dm.TableNode.findTableForOffset = function ( documentNode, offset, 
constraint ) {
+       var cellNode, node;
+       node = documentNode.getNodeFromOffset(offset);
+       while (node) {
+               switch (node.type) {
+                       case 'tableCell':
+                               cellNode = node;
+                               break;
+                       case 'table':
+                               if (constraint && 
!node.getRange().containsOffset(constraint)) {
+                                       break;
+                               } else {
+                                       return {
+                                               tableNode: node,
+                                               cellNode: cellNode
+                                       };
+                               }
+               }
+               node = node.parent;
+       }
+       return null;
+};
+
+/**
+ * A helper class to iterate over the cells of a table node.
+ *
+ * It provides a unified interface to iterate cells in presence of table 
sections,
+ * e.g., providing consecutive row indexes.
+ *
+ * @class
+ * @constructor
+ * @param {ve.dm.TableNode} [tableNode]
+ */
+ve.dm.TableNode.CellIterator = function VeCeTableNodeCellIterator( tableNode ) 
{
+       this.table = tableNode;
+
+       this.__it = {
+               sectionIndex: -1,
+               rowIndex: -1,
+               rowNode: null,
+               cellIndex: -1,
+               cellNode: null,
+               sectionNode: null,
+               finished: false
+       };
+
+       // hooks
+       this.onNewSection = function() {};
+       this.onNewRow = function() {};
+};
+
+ve.dm.TableNode.CellIterator.prototype.next = function() {
+       if (this.__it.finished) throw new Error("TableCellIterator has no more 
cells left.");
+       this.nextCell(this.__it);
+       if (this.__it.finished) return null;
+       else return this.__it.cellNode;
+};
+
+ve.dm.TableNode.CellIterator.prototype.nextSection = function(it) {
+       it.sectionIndex++;
+       it.sectionNode = this.table.children[it.sectionIndex];
+       if (!it.sectionNode) {
+               it.finished = true;
+       } else {
+               it.rowIndex = 0;
+               it.rowNode = it.sectionNode.children[0];
+               this.onNewSection(it.sectionNode);
+       }
+};
+
+ve.dm.TableNode.CellIterator.prototype.nextRow = function(it) {
+       it.rowIndex++;
+       if (it.sectionNode) {
+               it.rowNode = it.sectionNode.children[it.rowIndex];
+       }
+       while (!it.rowNode && !it.finished) {
+               this.nextSection(it);
+       }
+       if (it.rowNode) {
+               it.cellIndex = 0;
+               it.cellNode = it.rowNode.children[0];
+               this.onNewRow(it.rowNode);
+       }
+};
+
+ve.dm.TableNode.CellIterator.prototype.nextCell = function(it) {
+       if (it.cellNode) {
+               it.cellIndex++;
+               it.cellNode = it.rowNode.children[it.cellIndex];
+       }
+       // step into the next row if there is no next cell or if the column is
+       // beyond the rectangle boundaries
+       while (!it.cellNode && !it.finished) {
+               this.nextRow(it);
+       }
+};
+
 /* Registration */
 
 ve.dm.modelRegistry.register( ve.dm.TableNode );
diff --git a/src/dm/nodes/ve.dm.TableRowNode.js 
b/src/dm/nodes/ve.dm.TableRowNode.js
index cfb3523..7d53836 100644
--- a/src/dm/nodes/ve.dm.TableRowNode.js
+++ b/src/dm/nodes/ve.dm.TableRowNode.js
@@ -18,6 +18,13 @@
 ve.dm.TableRowNode = function VeDmTableRowNode() {
        // Parent constructor
        ve.dm.BranchNode.apply( this, arguments );
+
+       /* Events */
+
+       this.connect( this, {
+               'attach': 'onAttach',
+               'detach': 'onDetach'
+       } );
 };
 
 /* Inheritance */
@@ -34,6 +41,42 @@
 
 ve.dm.TableRowNode.static.matchTagNames = [ 'tr' ];
 
+/* Prototype functions */
+
+ve.dm.TableRowNode.prototype.onAttach = function( to ) {
+       if (to.onStructureChange) to.onStructureChange( { row: this } );
+};
+
+ve.dm.TableRowNode.prototype.onDetach = function( from ) {
+       if (from.onStructureChange) from.onStructureChange( { row: this } );
+};
+
+ve.dm.TableRowNode.prototype.onStructureChange = function( context ) {
+       if ( this.parent ) {
+               context.row = this;
+               this.parent.onStructureChange(context);
+       }
+};
+
+/**
+ * Creates data that can be inserted into the model to create a new table row.
+ *
+ * @param {Object} [options] An object with properties 'style' which can be 
either 'header' or 'data',
+ *   'cellCount' the number of cells in the row
+ * @return {Array} Model data for a new table row
+ */
+ve.dm.TableRowNode.createData = function( options ) {
+       options = options || {};
+       var data = [];
+       var cellCount = options.cellCount || 1;
+       data.push({ type: 'tableRow'});
+       for (var i = 0; i < cellCount; i++) {
+               data = data.concat(ve.dm.TableCellNode.createData(options));
+       }
+       data.push({ type: '/tableRow'});
+       return data;
+};
+
 /* Registration */
 
 ve.dm.modelRegistry.register( ve.dm.TableRowNode );
diff --git a/src/dm/nodes/ve.dm.TableSectionNode.js 
b/src/dm/nodes/ve.dm.TableSectionNode.js
index 9b1b685..32c0a6f 100644
--- a/src/dm/nodes/ve.dm.TableSectionNode.js
+++ b/src/dm/nodes/ve.dm.TableSectionNode.js
@@ -18,6 +18,13 @@
 ve.dm.TableSectionNode = function VeDmTableSectionNode() {
        // Parent constructor
        ve.dm.BranchNode.apply( this, arguments );
+
+       /* Events */
+
+       this.connect( this, {
+               'attach': 'onAttach',
+               'detach': 'onDetach'
+       } );
 };
 
 /* Inheritance */
@@ -32,9 +39,7 @@
 
 ve.dm.TableSectionNode.static.parentNodeTypes = [ 'table' ];
 
-ve.dm.TableSectionNode.static.defaultAttributes = {
-       style: 'body'
-};
+ve.dm.TableSectionNode.static.defaultAttributes = { style: 'body' };
 
 ve.dm.TableSectionNode.static.matchTagNames = [ 'thead', 'tbody', 'tfoot' ];
 
@@ -57,6 +62,24 @@
                tag = tags[dataElement.attributes && 
dataElement.attributes.style || 'body'];
        return [ doc.createElement( tag ) ];
 };
+
+/* Prototype functions */
+
+ve.dm.TableSectionNode.prototype.onAttach = function( to ) {
+       if (to.onStructureChange) to.onStructureChange( { section: this } );
+};
+
+ve.dm.TableSectionNode.prototype.onDetach = function( from ) {
+       if (from.onStructureChange) from.onStructureChange( { section: this } );
+};
+
+ve.dm.TableSectionNode.prototype.onStructureChange = function( context ) {
+       if ( this.parent ) {
+               context.section = this;
+               this.parent.onStructureChange(context);
+       }
+};
+
 /* Registration */
 
 ve.dm.modelRegistry.register( ve.dm.TableSectionNode );

-- 
To view, visit https://gerrit.wikimedia.org/r/159310
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ib4afcbaf157fb64666f52c9650f3260bb42f2a3c
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

Reply via email to