/*!
* MediaWiki Widgets TableWidgetModel class.
*
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Configuration options.
*
* @typedef {Object} mw.widgets.TableWidgetModel~Config
* @property {Array} [rows] An array of objects containing `key` and `label` properties for every row
* @property {Array} [cols] An array of objects containing `key` and `label` properties for every column
* @property {Array} [data] An array containing all values of the table
* @property {RegExp|Function|string} [validate] Validation pattern to apply on every cell
* @property {boolean} [showHeaders=true] Show table header row. Defaults to true.
* @property {boolean} [showRowLabels=true] Show row labels. Defaults to true.
* @property {boolean} [allowRowInsertion=true] Allow row insertion. Defaults to true.
* @property {boolean} [allowRowDeletion=true] Allow row deletion. Defaults to true.
*/
/**
* @classdesc TableWidget model.
*
* @class
* @mixes OO.EventEmitter
*
* @constructor
* @description Create an instance of `mw.widgets.TableWidgetModel`.
* @param {mw.widgets.TableWidgetModel~Config} [config] Configuration options
*/
mw.widgets.TableWidgetModel = function MwWidgetsTableWidgetModel( config ) {
config = config || {};
// Mixin constructors
OO.EventEmitter.call( this, config );
this.data = config.data || [];
this.validate = config.validate;
this.showHeaders = ( config.showHeaders !== undefined ) ? !!config.showHeaders : true;
this.showRowLabels = ( config.showRowLabels !== undefined ) ? !!config.showRowLabels : true;
this.allowRowInsertion = ( config.allowRowInsertion !== undefined ) ?
!!config.allowRowInsertion : true;
this.allowRowDeletion = ( config.allowRowDeletion !== undefined ) ?
!!config.allowRowDeletion : true;
this.initializeProps( config.rows, config.cols );
};
/* Inheritance */
OO.mixinClass( mw.widgets.TableWidgetModel, OO.EventEmitter );
/* Static Methods */
/**
* Get an entry from a props table
*
* @static
* @private
* @param {string|number} handle The key (or numeric index) of the row/column
* @param {Array} table Props table
* @return {Object|null} An object containing the `key`, `index` and `label`
* properties of the row/column. Returns `null` if the row/column can't be found.
*/
mw.widgets.TableWidgetModel.static.getEntryFromPropsTable = function ( handle, table ) {
let row = null,
i, len;
if ( typeof handle === 'string' ) {
for ( i = 0, len = table.length; i < len; i++ ) {
if ( table[ i ].key === handle ) {
row = table[ i ];
break;
}
}
} else if ( typeof handle === 'number' ) {
if ( handle < table.length ) {
row = table[ handle ];
}
}
return row;
};
/* Events */
/**
* Fired when a value inside the table has changed.
*
* @event mw.widgets.TableWidgetModel.valueChange
* @param {number} row The row index of the updated cell
* @param {number} column The column index of the updated cell
* @param {any} value The new value
*/
/**
* Fired when a new row is inserted into the table.
*
* @event mw.widgets.TableWidgetModel.insertRow
* @param {Array} data The initial data
* @param {number} index The index in which to insert the new row
* @param {string} key The row key
* @param {string} label The row label
*/
/**
* Fired when a new row is inserted into the table.
*
* @event mw.widgets.TableWidgetModel.insertColumn
* @param {Array} data The initial data
* @param {number} index The index in which to insert the new column
* @param {string} key The column key
* @param {string} label The column label
*/
/**
* Fired when a row is removed from the table.
*
* @event mw.widgets.TableWidgetModel.removeRow
* @param {number} index The removed row index
* @param {string} key The removed row key
*/
/**
* Fired when a column is removed from the table.
*
* @event mw.widgets.TableWidgetModel.removeColumn
* @param {number} index The removed column index
* @param {string} key The removed column key
*/
/**
* Fired when the table data is wiped.
*
* @event mw.widgets.TableWidgetModel.clear
* @param {boolean} clear Clear row/column properties
*/
/* Methods */
/**
* Initializes and ensures the proper creation of the rows and cols property arrays.
* If data exceeds the number of rows and cols given, new ones will be created.
*
* @private
* @param {Array} rowProps The initial row props
* @param {Array} colProps The initial column props
*/
mw.widgets.TableWidgetModel.prototype.initializeProps = function ( rowProps, colProps ) {
// FIXME: Account for extra data with missing row/col metadata
let i, len;
this.rows = [];
this.cols = [];
if ( Array.isArray( rowProps ) ) {
for ( i = 0, len = rowProps.length; i < len; i++ ) {
this.rows.push( {
index: i,
key: rowProps[ i ].key,
label: rowProps[ i ].label
} );
}
}
if ( Array.isArray( colProps ) ) {
for ( i = 0, len = colProps.length; i < len; i++ ) {
this.cols.push( {
index: i,
key: colProps[ i ].key,
label: colProps[ i ].label
} );
}
}
};
/**
* Triggers the initialization process and builds the initial table.
*
* @fires mw.widgets.TableWidgetModel.insertRow
*/
mw.widgets.TableWidgetModel.prototype.setupTable = function () {
this.verifyData();
this.buildTable();
};
/**
* Verifies if the table data is complete and synced with
* row and column properties, and adds empty strings as
* cell data if cells are missing
*
* @private
*/
mw.widgets.TableWidgetModel.prototype.verifyData = function () {
let i, j, rowLen, colLen;
for ( i = 0, rowLen = this.rows.length; i < rowLen; i++ ) {
if ( this.data[ i ] === undefined ) {
this.data.push( [] );
}
for ( j = 0, colLen = this.cols.length; j < colLen; j++ ) {
if ( this.data[ i ][ j ] === undefined ) {
this.data[ i ].push( '' );
}
}
}
};
/**
* Build initial table
*
* @private
* @fires mw.widgets.TableWidgetModel.insertRow
*/
mw.widgets.TableWidgetModel.prototype.buildTable = function () {
let i, len;
for ( i = 0, len = this.rows.length; i < len; i++ ) {
this.emit( 'insertRow', this.data[ i ], i, this.rows[ i ].key, this.rows[ i ].label );
}
};
/**
* Refresh the entire table with new data
*
* @private
* @fires mw.widgets.TableWidgetModel.insertRow
*/
mw.widgets.TableWidgetModel.prototype.refreshTable = function () {
// TODO: Clear existing table
this.buildTable();
};
/**
* Set the value of a particular cell.
*
* @param {number|string} row The index or key of the row
* @param {number|string} col The index or key of the column
* @param {any} value The new value
* @fires mw.widgets.TableWidgetModel.valueChange
*/
mw.widgets.TableWidgetModel.prototype.setValue = function ( row, col, value ) {
let rowIndex, colIndex;
if ( typeof row === 'number' ) {
rowIndex = row;
} else if ( typeof row === 'string' ) {
rowIndex = this.getRowProperties( row ).index;
}
if ( typeof col === 'number' ) {
colIndex = col;
} else if ( typeof col === 'string' ) {
colIndex = this.getColumnProperties( col ).index;
}
if ( typeof rowIndex === 'number' && typeof colIndex === 'number' &&
this.data[ rowIndex ] !== undefined && this.data[ rowIndex ][ colIndex ] !== undefined &&
this.data[ rowIndex ][ colIndex ] !== value ) {
this.data[ rowIndex ][ colIndex ] = value;
this.emit( 'valueChange', rowIndex, colIndex, value );
}
};
/**
* Set the table data.
*
* @param {Array} data The new table data
*/
mw.widgets.TableWidgetModel.prototype.setData = function ( data ) {
if ( Array.isArray( data ) ) {
this.data = data;
this.verifyData();
this.refreshTable();
}
};
/**
* Inserts a row into the table. If the row isn't added at the end of the table,
* all the following data will be shifted back one row.
*
* @param {Array} [data] The data to insert to the row.
* @param {number} [index] The index in which to insert the new row.
* If unset or set to null, the row will be added at the end of the table.
* @param {string} [key] A key to quickly select this row.
* If unset or set to null, no key will be set.
* @param {string} [label] A label to display next to the row.
* If unset or set to null, the key will be used if there is one.
* @fires mw.widgets.TableWidgetModel.insertRow
*/
mw.widgets.TableWidgetModel.prototype.insertRow = function ( data, index, key, label ) {
const insertIndex = ( typeof index === 'number' ) ? index : this.rows.length;
// Add the new row metadata
this.rows.splice( insertIndex, 0, {
index: insertIndex,
key: key || undefined,
label: label || undefined
} );
const newRowData = [];
let insertDataCell;
// Add the new row data
const insertData = ( Array.isArray( data ) ) ? data : [];
// Ensure that all columns of data for this row have been supplied,
// otherwise fill the remaining data with empty strings
for ( let i = 0, len = this.cols.length; i < len; i++ ) {
insertDataCell = '';
if ( typeof insertData[ i ] === 'string' || typeof insertData[ i ] === 'number' ) {
insertDataCell = insertData[ i ];
}
newRowData.push( insertDataCell );
}
this.data.splice( insertIndex, 0, newRowData );
// Update all indexes in following rows
for ( let i = insertIndex + 1, len = this.rows.length; i < len; i++ ) {
this.rows[ i ].index++;
}
this.emit( 'insertRow', data, insertIndex, key, label );
};
/**
* Inserts a column into the table. If the column isn't added at the end of the table,
* all the following data will be shifted back one column.
*
* @param {Array} [data] The data to insert to the column.
* @param {number} [index] The index in which to insert the new column.
* If unset or set to null, the column will be added at the end of the table.
* @param {string} [key] A key to quickly select this column.
* If unset or set to null, no key will be set.
* @param {string} [label] A label to display next to the column.
* If unset or set to null, the key will be used if there is one.
* @fires mw.widgets.TableWidgetModel.insertColumn
*/
mw.widgets.TableWidgetModel.prototype.insertColumn = function ( data, index, key, label ) {
const insertIndex = ( typeof index === 'number' ) ? index : this.cols.length;
// Add the new column metadata
this.cols.splice( insertIndex, 0, {
index: insertIndex,
key: key || undefined,
label: label || undefined
} );
// Add the new column data
const insertData = ( Array.isArray( data ) ) ? data : [];
// Ensure that all rows of data for this column have been supplied,
// otherwise fill the remaining data with empty strings
let insertDataCell;
for ( let i = 0, len = this.rows.length; i < len; i++ ) {
insertDataCell = '';
if ( typeof insertData[ i ] === 'string' || typeof insertData[ i ] === 'number' ) {
insertDataCell = insertData[ i ];
}
this.data[ i ].splice( insertIndex, 0, insertDataCell );
}
// Update all indexes in following cols
for ( let i = insertIndex + 1, len = this.cols.length; i < len; i++ ) {
this.cols[ i ].index++;
}
this.emit( 'insertColumn', data, index, key, label );
};
/**
* Removes a row from the table. If the row removed isn't at the end of the table,
* all the following rows will be shifted back one row.
*
* @param {number|string} handle The key or numerical index of the row to remove
* @fires mw.widgets.TableWidgetModel.removeRow
*/
mw.widgets.TableWidgetModel.prototype.removeRow = function ( handle ) {
const rowProps = this.getRowProperties( handle );
// Exit early if the row couldn't be found
if ( rowProps === null ) {
return;
}
this.rows.splice( rowProps.index, 1 );
this.data.splice( rowProps.index, 1 );
// Update all indexes in following rows
for ( let i = rowProps.index, len = this.rows.length; i < len; i++ ) {
this.rows[ i ].index--;
}
this.emit( 'removeRow', rowProps.index, rowProps.key );
};
/**
* Removes a column from the table. If the column removed isn't at the end of the table,
* all the following columns will be shifted back one column.
*
* @param {number|string} handle The key or numerical index of the column to remove
* @fires mw.widgets.TableWidgetModel.removeColumn
*/
mw.widgets.TableWidgetModel.prototype.removeColumn = function ( handle ) {
const colProps = this.getColumnProperties( handle );
// Exit early if the column couldn't be found
if ( colProps === null ) {
return;
}
this.cols.splice( colProps.index, 1 );
for ( let i = 0, len = this.data.length; i < len; i++ ) {
this.data[ i ].splice( colProps.index, 1 );
}
// Update all indexes in following columns
for ( let i = colProps.index, len = this.cols.length; i < len; i++ ) {
this.cols[ i ].index--;
}
this.emit( 'removeColumn', colProps.index, colProps.key );
};
/**
* Clears the table data.
*
* @fires mw.widgets.TableWidgetModel.clear
*/
mw.widgets.TableWidgetModel.prototype.clear = function () {
this.data = [];
this.verifyData();
this.emit( 'clear', false );
};
/**
* Clears the table data, as well as all row and column properties.
*
* @fires mw.widgets.TableWidgetModel.clear
*/
mw.widgets.TableWidgetModel.prototype.clearWithProperties = function () {
this.data = [];
this.rows = [];
this.cols = [];
this.emit( 'clear', true );
};
/**
* Get all table properties.
*
* @return {Object}
*/
mw.widgets.TableWidgetModel.prototype.getTableProperties = function () {
return {
showHeaders: this.showHeaders,
showRowLabels: this.showRowLabels,
allowRowInsertion: this.allowRowInsertion,
allowRowDeletion: this.allowRowDeletion
};
};
/**
* Get the validation pattern to test cells against.
*
* @return {RegExp|Function|string}
*/
mw.widgets.TableWidgetModel.prototype.getValidationPattern = function () {
return this.validate;
};
/**
* Get properties of a given row.
*
* @param {string|number} handle The key (or numeric index) of the row
* @return {Object|null} An object containing the `key`, `index` and `label` properties of the row.
* Returns `null` if the row can't be found.
*/
mw.widgets.TableWidgetModel.prototype.getRowProperties = function ( handle ) {
return mw.widgets.TableWidgetModel.static.getEntryFromPropsTable( handle, this.rows );
};
/**
* Get properties of all rows.
*
* @return {Array} An array of objects containing `key`, `index` and `label` properties for each row
*/
mw.widgets.TableWidgetModel.prototype.getAllRowProperties = function () {
return this.rows.slice();
};
/**
* Get properties of a given column.
*
* @param {string|number} handle The key (or numeric index) of the column
* @return {Object|null} An object containing the `key`, `index` and
* `label` properties of the column.
* Returns `null` if the column can't be found.
*/
mw.widgets.TableWidgetModel.prototype.getColumnProperties = function ( handle ) {
return mw.widgets.TableWidgetModel.static.getEntryFromPropsTable( handle, this.cols );
};
/**
* Get properties of all columns.
*
* @return {Array} An array of objects containing `key`, `index` and
* `label` properties for each column
*/
mw.widgets.TableWidgetModel.prototype.getAllColumnProperties = function () {
return this.cols.slice();
};