/**
* 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
* @mixes OO.EventEmitter
* @constructor
* @param {ve.dm.TableNode} tableNode Reference to a table instance
*/
ve.dm.TableMatrix = function VeDmTableMatrix( tableNode ) {
// Mixin constructors
OO.EventEmitter.call( this );
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;
};
/* Inheritance */
OO.mixinClass( ve.dm.TableMatrix, OO.EventEmitter );
/**
* @event ve.dm.TableMatrix#structureChange
*/
/**
* Invalidates the matrix structure.
*
* This is called by ve.dm.TableNode on structural changes.
*
* @fires ve.dm.TableMatrix#structureChange
*/
ve.dm.TableMatrix.prototype.invalidate = function () {
this.matrix = null;
this.rowNodes = null;
this.emit( 'structureChange' );
};
/**
* Recreates the matrix structure.
*/
ve.dm.TableMatrix.prototype.update = function () {
const matrix = [],
rowNodes = [],
iterator = this.tableNode.getIterator();
let row = -1,
col = -1;
// Handle row transitions
iterator.on( 'newRow', ( 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.
let cellNode;
while ( ( cellNode = iterator.next() ) !== undefined ) {
col++;
// Skip placeholders
while ( matrix[ row ][ col ] ) {
col++;
}
if ( !cellNode ) {
matrix[ row ][ col ] = null;
continue;
}
const cell = new ve.dm.TableMatrixCell( cellNode, row, col );
// Store the cell in the matrix
matrix[ row ][ col ] = cell;
// Add place holders for spanned cells
const rowSpan = cellNode.getRowspan();
const colSpan = cellNode.getColspan();
if ( rowSpan === 1 && colSpan === 1 ) {
continue;
}
for ( let i = 0; i < rowSpan; i++ ) {
for ( let j = 0; j < colSpan; j++ ) {
if ( i === 0 && j === 0 ) {
continue;
}
const r = row + i;
const c = col + j;
// Initialize the cell matrix row if not yet present
matrix[ r ] = matrix[ r ] || [];
matrix[ r ][ c ] = new ve.dm.TableMatrixCell( cellNode, r, c, cell );
}
}
}
this.matrix = matrix;
this.rowNodes = rowNodes;
};
/**
* Retrieves a single cell.
*
* @param {number} row Row index
* @param {number} col Column index
* @return {ve.dm.TableMatrixCell|undefined} Cell, or undefined if out of bounds
*/
ve.dm.TableMatrix.prototype.getCell = function ( row, col ) {
const matrix = this.getMatrix();
return matrix[ row ] ? matrix[ row ][ col ] : undefined;
};
/**
* Retrieves all cells of a column with given index.
*
* @param {number} col Column index
* @return {ve.dm.TableMatrixCell[]} The cells of a column
*/
ve.dm.TableMatrix.prototype.getColumn = function ( col ) {
const matrix = this.getMatrix();
const cells = [];
for ( let 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 Row index
* @return {ve.dm.TableMatrixCell[]} The cells of a row
*/
ve.dm.TableMatrix.prototype.getRow = function ( row ) {
const matrix = this.getMatrix();
return matrix[ row ];
};
/**
* Retrieves the row node of a row with given index.
*
* It is possible for bad tables to be constructed with rowspans
* that exceed the last actual row node. In these cases, the number
* of rows in the table matrix will exceed the number of row
* nodes, so users should expected getRowNode to sometimes
* return undefined. (T191858)
*
* @param {number} row Row index
* @return {ve.dm.TableRowNode|undefined} Node at given index, if found
*/
ve.dm.TableMatrix.prototype.getRowNode = function ( row ) {
const 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.TableMatrixCell[][]} Table matrix
*/
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[]} Table row nodes
*/
ve.dm.TableMatrix.prototype.getRowNodes = function () {
if ( !this.rowNodes ) {
this.update();
}
return this.rowNodes;
};
/**
* Get number of rows in the table
*
* @return {number} Number of rows
*/
ve.dm.TableMatrix.prototype.getRowCount = function () {
return this.getMatrix().length;
};
/**
* Get number of columns in a row
*
* To get the number of columns in a table use #getMaxColCount
*
* @param {number} row Row to count columns in
* @return {number} Number of columns
*/
ve.dm.TableMatrix.prototype.getColCount = function ( row ) {
const matrix = this.getMatrix();
return matrix.length ? matrix[ row ].length : 0;
};
/**
* Get the maximum number of columns in a table
*
* This is required because in sparse tables the column count is variable.
*
* @return {number} Number of columns
*/
ve.dm.TableMatrix.prototype.getMaxColCount = function () {
let colCount = 0;
for ( let row = this.getRowCount() - 1; row >= 0; row-- ) {
colCount = Math.max( colCount, this.getColCount( row ) );
}
return colCount;
};
/**
* Look up the matrix cell for a given cell node.
*
* @param {ve.dm.TableCellNode} cellNode Cell node
* @return {ve.dm.TableMatrixCell|null} The cell or null if not found
*/
ve.dm.TableMatrix.prototype.lookupCell = function ( cellNode ) {
const matrix = this.getMatrix(),
rowNodes = this.getRowNodes();
const row = rowNodes.indexOf( cellNode.getParent() );
if ( row < 0 ) {
return null;
}
const rowCells = matrix[ row ];
for ( let col = 0, cols = rowCells.length; col < cols; col++ ) {
if ( rowCells[ col ] && rowCells[ col ].node === cellNode ) {
return rowCells[ col ];
}
}
return null;
};
/**
* Finds the closest cell not being a placeholder for a given cell.
*
* @param {ve.dm.TableMatrixCell} cell Table cell
* @return {ve.dm.TableMatrixCell|null} Closest cell
*/
ve.dm.TableMatrix.prototype.findClosestCell = function ( cell ) {
const matrix = this.getMatrix();
const rowCells = matrix[ cell.row ];
let col;
for ( col = cell.col; col >= 0; col-- ) {
if ( !rowCells[ col ].isPlaceholder() ) {
return rowCells[ col ];
}
}
let cols;
for ( col = cell.col + 1, cols = rowCells.length; col < cols; col++ ) {
if ( !rowCells[ col ].isPlaceholder() ) {
return rowCells[ col ];
}
}
return null;
};
/**
* An object wrapping a table cell node, augmenting it with row and column indexes.
*
* Cells which are occupied by another cell's with 'rowspan' or 'colspan' attributes are
* placeholders and have an owner property other than themselves.
* Placeholders are used to create a dense representation of the sparse HTML table model.
*
* @class
* @constructor
* @param {ve.dm.TableCellNode} node DM Node
* @param {number} row Row index
* @param {number} col Column index
* @param {ve.dm.TableMatrixCell} [owner] Owner cell if this is a placeholder
*/
ve.dm.TableMatrixCell = function VeDmTableMatrixCell( node, row, col, owner ) {
this.node = node;
this.row = row;
this.col = col;
this.key = row + '_' + col;
this.owner = owner || this;
// Used when moving cells
this.data = null;
};
/* Inheritance */
OO.initClass( ve.dm.TableMatrixCell );
/* Static Methods */
/**
* Comparison function for sorting cells in text flow order
*
* @param {ve.dm.TableMatrixCell} a First cell
* @param {ve.dm.TableMatrixCell} b Second cell
* @return {number} Positive, negative or zero, depending on relative position
*/
ve.dm.TableMatrixCell.static.sortDescending = function ( a, b ) {
if ( a.row !== b.row ) {
return b.row - a.row;
}
return b.col - a.col;
};
/* Methods */
/**
* Check if this cell is a placeholder
*
* @return {boolean} This cell is a placeholder
*/
ve.dm.TableMatrixCell.prototype.isPlaceholder = function () {
return this.owner !== this;
};
/**
* Get owner matrix cell
*
* @return {ve.dm.TableMatrixCell} Owner cell
*/
ve.dm.TableMatrixCell.prototype.getOwner = function () {
return this.owner;
};
/**
* Compare to another cell
*
* Cells are considered equal to their placeholders
*
* @param {ve.dm.TableMatrixCell} other Cell to compare
* @return {boolean} Cells are equal
*/
ve.dm.TableMatrixCell.prototype.equals = function ( other ) {
return this.getOwner().key === other.getOwner().key;
};