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