Merge "Introduce table widget, upstreamed from the Graph extension"

This commit is contained in:
jenkins-bot 2020-05-05 19:35:34 +00:00 committed by Gerrit Code Review
commit 64f09f737a
11 changed files with 1964 additions and 0 deletions

View file

@ -4105,6 +4105,7 @@
"mw-widgets-dateinput-placeholder-month": "YYYY-MM",
"mw-widgets-mediasearch-input-placeholder": "Search for media",
"mw-widgets-mediasearch-noresults": "No results found.",
"mw-widgets-table-row-delete": "Delete row",
"mw-widgets-titleinput-description-new-page": "page does not exist yet",
"mw-widgets-titleinput-description-redirect": "redirect to $1",
"mw-widgets-categoryselector-add-category-placeholder": "Add a category...",

View file

@ -4320,6 +4320,7 @@
"mw-widgets-dateinput-placeholder-month": "Placeholder displayed in a date input field when it's empty, representing a date format with 4 digits for year and 2 digits for month, separated with hyphens (without a day). This should be uppercase, if possible, and must not include any additional explanations. If there is no good way to translate it, make this message blank.",
"mw-widgets-mediasearch-input-placeholder": "Place holder text for media search input",
"mw-widgets-mediasearch-noresults": "Label notifying the user no results were found for the media search.",
"mw-widgets-table-row-delete": "Tooltip for the delete row button in table widgets",
"mw-widgets-titleinput-description-new-page": "Description label for a new page in the title input widget.",
"mw-widgets-titleinput-description-redirect": "Description label for a redirect in the title input widget.",
"mw-widgets-categoryselector-add-category-placeholder": "Placeholder displayed in the category selector widget after the capsules of already added categories.",

View file

@ -2564,6 +2564,25 @@ return [
],
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.widgets.Table' => [
'scripts' => [
'resources/src/mediawiki.widgets/Table/mw.widgets.RowWidget.js',
'resources/src/mediawiki.widgets/Table/mw.widgets.RowWidgetModel.js',
'resources/src/mediawiki.widgets/Table/mw.widgets.TableWidget.js',
'resources/src/mediawiki.widgets/Table/mw.widgets.TableWidgetModel.js'
],
'styles' => [
'resources/src/mediawiki.widgets/Table/mw.widgets.RowWidget.css',
'resources/src/mediawiki.widgets/Table/mw.widgets.TableWidget.css',
],
'dependencies' => [
'oojs-ui-widgets'
],
'messages' => [
'mw-widgets-table-row-delete',
],
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.widgets.UserInputWidget' => [
'scripts' => [
'resources/src/mediawiki.widgets/mw.widgets.UserInputWidget.js',

View file

@ -0,0 +1,33 @@
.mw-widgets-rowWidget {
clear: left;
float: left;
margin-bottom: -1px;
width: 100%;
}
.mw-widgets-rowWidget-label {
display: block;
margin-right: 5%;
padding-top: 0.5em;
width: 35%;
}
.mw-widgets-rowWidget > .mw-widgets-rowWidget-label {
float: left;
}
.mw-widgets-rowWidget > .mw-widgets-rowWidget-cells {
float: left;
}
.mw-widgets-rowWidget > .mw-widgets-rowWidget-cells > .oo-ui-inputWidget {
float: left;
margin-right: -1px;
width: 8em;
}
.mw-widgets-rowWidget > .mw-widgets-rowWidget-cells > .oo-ui-inputWidget > input,
.mw-widgets-rowWidget > .mw-widgets-rowWidget-delete-button > .oo-ui-buttonElement-button {
margin: 0;
border-radius: 0;
}

View file

@ -0,0 +1,336 @@
/**
* A RowWidget is used in conjunction with {@link mw.widgets.TableWidget table widgets}
* and should not be instantiated by themselves. They group together
* {@link OO.ui.TextInputWidget text input widgets} to form a unified row of
* editable data.
*
* @class
* @extends OO.ui.Widget
* @mixins OO.ui.mixin.GroupElement
*
* @constructor
* @param {Object} [config] Configuration options
* @cfg {Array} [data] The data of the cells
* @cfg {Array} [keys] An array of keys for easy cell selection
* @cfg {RegExp|Function|string} [validate] Validation pattern to apply on every cell
* @cfg {number} [index] The row index.
* @cfg {string} [label] The row label to display. If not provided, the row index will
* be used be default. If set to null, no label will be displayed.
* @cfg {boolean} [showLabel=true] Show row label. Defaults to true.
* @cfg {boolean} [deletable=true] Whether the table should provide deletion UI tools
* for this row or not. Defaults to true.
*/
mw.widgets.RowWidget = function MwWidgetsRowWidget( config ) {
config = config || {};
// Parent constructor
mw.widgets.RowWidget.super.call( this, config );
// Mixin constructor
OO.ui.mixin.GroupElement.call( this, config );
// Set up model
this.model = new mw.widgets.RowWidgetModel( config );
// Set up group element
this.setGroupElement(
$( '<div>' )
.addClass( 'mw-widgets-rowWidget-cells' )
);
// Set up label
this.labelCell = new OO.ui.LabelWidget( {
classes: [ 'mw-widgets-rowWidget-label' ]
} );
// Set up delete button
if ( this.model.getRowProperties().isDeletable ) {
this.deleteButton = new OO.ui.ButtonWidget( {
icon: 'trash',
classes: [ 'mw-widgets-rowWidget-delete-button' ],
flags: 'destructive',
title: mw.msg( 'mw-widgets-table-row-delete' )
} );
}
// Events
this.model.connect( this, {
valueChange: 'onValueChange',
insertCell: 'onInsertCell',
removeCell: 'onRemoveCell',
clear: 'onClear',
labelUpdate: 'onLabelUpdate'
} );
this.aggregate( {
change: 'cellChange'
} );
this.connect( this, {
cellChange: 'onCellChange'
} );
if ( this.model.getRowProperties().isDeletable ) {
this.deleteButton.connect( this, {
click: 'onDeleteButtonClick'
} );
}
// Initialization
this.$element.addClass( 'mw-widgets-rowWidget' );
this.$element.append(
this.labelCell.$element,
this.$group
);
if ( this.model.getRowProperties().isDeletable ) {
this.$element.append( this.deleteButton.$element );
}
this.setLabel( this.model.getRowProperties().label );
this.model.setupRow();
};
/* Inheritance */
OO.inheritClass( mw.widgets.RowWidget, OO.ui.Widget );
OO.mixinClass( mw.widgets.RowWidget, OO.ui.mixin.GroupElement );
/* Events */
/**
* @event inputChange
*
* Change when an input contained within the row is updated
*
* @param {number} index The index of the cell that changed
* @param {string} value The new value of the cell
*/
/**
* @event deleteButtonClick
*
* Fired when the delete button for the row is pressed
*/
/* Methods */
/**
* @private
* @inheritdoc
*/
mw.widgets.RowWidget.prototype.addItems = function ( items, index ) {
var i, len;
OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
for ( i = index, len = items.length; i < len; i++ ) {
items[ i ].setData( i );
}
};
/**
* @private
* @inheritdoc
*/
mw.widgets.RowWidget.prototype.removeItems = function ( items ) {
var i, len, cells;
OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
cells = this.getItems();
for ( i = 0, len = cells.length; i < len; i++ ) {
cells[ i ].setData( i );
}
};
/**
* Get the row index
*
* @return {number} The row index
*/
mw.widgets.RowWidget.prototype.getIndex = function () {
return this.model.getRowProperties().index;
};
/**
* Set the row index
*
* @param {number} index The new index
*/
mw.widgets.RowWidget.prototype.setIndex = function ( index ) {
this.model.setIndex( index );
};
/**
* Get the label displayed on the row. If no custom label is set, the
* row index is used instead.
*
* @return {string} The row label
*/
mw.widgets.RowWidget.prototype.getLabel = function () {
var props = this.model.getRowProperties();
if ( props.label === null ) {
return '';
} else if ( !props.label ) {
return props.index.toString();
} else {
return props.label;
}
};
/**
* Set the label to be displayed on the widget.
*
* @param {string} label The new label
* @fires labelUpdate
*/
mw.widgets.RowWidget.prototype.setLabel = function ( label ) {
this.model.setLabel( label );
};
/**
* Set the value of a particular cell
*
* @param {number} index The cell index
* @param {string} value The new value
*/
mw.widgets.RowWidget.prototype.setValue = function ( index, value ) {
this.model.setValue( index, value );
};
/**
* Insert a cell at a specified index
*
* @param {string} data The cell data
* @param {number} index The index to insert the cell at
* @param {string} key A key for easy cell selection
*/
mw.widgets.RowWidget.prototype.insertCell = function ( data, index, key ) {
this.model.insertCell( data, index, key );
};
/**
* Removes a column at a specified index
*
* @param {number} index The index to removeColumn
*/
mw.widgets.RowWidget.prototype.removeCell = function ( index ) {
this.model.removeCell( index );
};
/**
* Clear the field values
*/
mw.widgets.RowWidget.prototype.clear = function () {
this.model.clear();
};
/**
* Handle model value changes
*
* @param {number} index The column index of the updated cell
* @param {number} value The new value
*
* @fires inputChange
*/
mw.widgets.RowWidget.prototype.onValueChange = function ( index, value ) {
this.getItems()[ index ].setValue( value );
this.emit( 'inputChange', index, value );
};
/**
* Handle model cell insertions
*
* @param {string} data The initial data
* @param {number} index The index in which to insert the new cell
*/
mw.widgets.RowWidget.prototype.onInsertCell = function ( data, index ) {
this.addItems( [
new OO.ui.TextInputWidget( {
data: index,
value: data,
validate: this.model.getValidationPattern()
} )
], index );
};
/**
* Handle model cell removals
*
* @param {number} index The removed cell index
*/
mw.widgets.RowWidget.prototype.onRemoveCell = function ( index ) {
this.removeItems( [ index ] );
};
/**
* Handle clear requests
*/
mw.widgets.RowWidget.prototype.onClear = function () {
var i, len,
cells = this.getItems();
for ( i = 0, len = cells.length; i < len; i++ ) {
cells[ i ].setValue( '' );
}
};
/**
* Update model label changes
*/
mw.widgets.RowWidget.prototype.onLabelUpdate = function () {
this.labelCell.setLabel( this.getLabel() );
};
/**
* React to cell input change
*
* @private
* @param {OO.ui.TextInputWidget} input The input that fired the event
* @param {string} value The value of the input
*/
mw.widgets.RowWidget.prototype.onCellChange = function ( input, value ) {
// FIXME: The table itself should know if it contains invalid data
// in order to pass form state to the dialog when it asks if the Apply
// button should be enabled or not. This probably requires the table
// and each individual row to handle validation through an array of promises
// fed from the cells within.
// Right now, the table can't know if it's valid or not because the events
// don't get passed through.
var self = this;
input.getValidity().done( function () {
self.model.setValue( input.getData(), value );
} );
};
/**
* Handle delete button clicks
*
* @private
* @fires deleteButtonClick
*/
mw.widgets.RowWidget.prototype.onDeleteButtonClick = function () {
this.emit( 'deleteButtonClick' );
};
/**
* @inheritdoc
*/
mw.widgets.RowWidget.prototype.setDisabled = function ( disabled ) {
// Parent method
mw.widgets.RowWidget.super.prototype.setDisabled.call( this, disabled );
if ( !this.items ) {
return;
}
this.deleteButton.setDisabled( disabled );
this.getItems().forEach( function ( cell ) {
cell.setDisabled( disabled );
} );
};

View file

@ -0,0 +1,362 @@
/*!
* MediaWiki Widgets RowWidgetModel class
*
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* RowWidget model.
*
* @class
* @mixins OO.EventEmitter
*
* @constructor
* @param {Object} [config] Configuration options
* @cfg {Array} [data] An array containing all values of the row
* @cfg {Array} [keys] An array of keys for easy cell selection
* @cfg {RegExp|Function|string} [validate] Validation pattern to apply on every cell
* @cfg {string} [label=''] Row label. Defaults to empty string.
* @cfg {boolean} [showLabel=true] Show row label. Defaults to true.
* @cfg {boolean} [deletable=true] Allow row to be deleted. Defaults to true.
*/
mw.widgets.RowWidgetModel = function MwWidgetsRowWidgetModel( config ) {
config = config || {};
// Mixin constructors
OO.EventEmitter.call( this, config );
this.data = config.data || [];
this.validate = config.validate;
this.index = ( config.index !== undefined ) ? config.index : -1;
this.label = ( config.label !== undefined ) ? config.label : '';
this.showLabel = ( config.showLabel !== undefined ) ? !!config.showLabel : true;
this.isDeletable = ( config.deletable !== undefined ) ? !!config.deletable : true;
this.initializeProps( config.keys );
};
/* Inheritance */
OO.mixinClass( mw.widgets.RowWidgetModel, OO.EventEmitter );
/* Events */
/**
* @event valueChange
*
* Fired when a value inside the row has changed.
*
* @param {number} index The column index of the updated cell
* @param {number} value The new value
*/
/**
* @event insertCell
*
* Fired when a new cell is inserted into the row.
*
* @param {Array} data The initial data
* @param {number} index The index in which to insert the new cell
*/
/**
* @event removeCell
*
* Fired when a cell is removed from the row.
*
* @param {number} index The removed cell index
*/
/**
* @event clear
*
* Fired when the row is cleared
*
* @param {boolean} clear Clear cell properties
*/
/**
* @event labelUpdate
*
* Fired when the row label might need to be updated
*/
/* Methods */
/**
* Initializes and ensures the proper creation of the cell property array.
* If data exceeds the number of cells given, new ones will be created.
*
* @private
* @param {Array} props The initial cell props
*/
mw.widgets.RowWidgetModel.prototype.initializeProps = function ( props ) {
var i, len;
this.cells = [];
if ( Array.isArray( props ) ) {
for ( i = 0, len = props.length; i < len; i++ ) {
this.cells.push( {
index: i,
key: props[ i ]
} );
}
}
};
/**
* Triggers the initialization process and builds the initial row.
*
* @fires insertCell
*/
mw.widgets.RowWidgetModel.prototype.setupRow = function () {
this.verifyData();
this.buildRow();
};
/**
* Verifies if the table data is complete and synced with
* cell properties, and adds empty strings as cell data if
* cells are missing
*
* @private
*/
mw.widgets.RowWidgetModel.prototype.verifyData = function () {
var i, len;
for ( i = 0, len = this.cells.length; i < len; i++ ) {
if ( this.data[ i ] === undefined ) {
this.data.push( '' );
}
}
};
/**
* Build initial row
*
* @private
* @fires insertCell
*/
mw.widgets.RowWidgetModel.prototype.buildRow = function () {
var i, len;
for ( i = 0, len = this.cells.length; i < len; i++ ) {
this.emit( 'insertCell', this.data[ i ], i );
}
};
/**
* Refresh the entire row with new data
*
* @private
* @fires insertCell
*/
mw.widgets.RowWidgetModel.prototype.refreshRow = function () {
// TODO: Clear existing table
this.buildRow();
};
/**
* Set the value of a particular cell
*
* @param {number|string} handle The index or key of the cell
* @param {Mixed} value The new value
* @fires valueChange
*/
mw.widgets.RowWidgetModel.prototype.setValue = function ( handle, value ) {
var index;
if ( typeof handle === 'number' ) {
index = handle;
} else if ( typeof handle === 'string' ) {
index = this.getCellProperties( handle ).index;
}
if ( typeof index === 'number' && this.data[ index ] !== undefined &&
this.data[ index ] !== value ) {
this.data[ index ] = value;
this.emit( 'valueChange', index, value );
}
};
/**
* Set the row data
*
* @param {Array} data The new row data
*/
mw.widgets.RowWidgetModel.prototype.setData = function ( data ) {
if ( Array.isArray( data ) ) {
this.data = data;
this.verifyData();
this.refreshRow();
}
};
/**
* Set the row index
*
* @param {number} index The new row index
* @fires labelUpdate
*/
mw.widgets.RowWidgetModel.prototype.setIndex = function ( index ) {
this.index = index;
this.emit( 'labelUpdate' );
};
/**
* Set the row label
*
* @param {number} label The new row label
* @fires labelUpdate
*/
mw.widgets.RowWidgetModel.prototype.setLabel = function ( label ) {
this.label = label;
this.emit( 'labelUpdate' );
};
/**
* 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 {number|string} [data] The data to insert to the cell.
* @param {number} [index] The index in which to insert the new cell.
* If unset or set to null, the cell will be added at the end of the row.
* @param {string} [key] A key to quickly select this cell.
* If unset or set to null, no key will be set.
* @fires insertCell
*/
mw.widgets.RowWidgetModel.prototype.insertCell = function ( data, index, key ) {
var insertIndex = ( typeof index === 'number' ) ? index : this.cells.length,
insertData, i, len;
// Add the new cell metadata
this.cells.splice( insertIndex, 0, {
index: insertIndex,
key: key || undefined
} );
// Add the new row data
insertData = ( typeof data === 'string' || typeof data === 'number' ) ? data : '';
this.data.splice( insertIndex, 0, insertData );
// Update all indexes in following cells
for ( i = insertIndex + 1, len = this.cells.length; i < len; i++ ) {
this.cells[ i ].index++;
}
this.emit( 'insertCell', data, insertIndex );
};
/**
* Removes a cell from the table. If the cell removed isn't at the end of the table,
* all the following cells will be shifted back one cell.
*
* @param {number|string} handle The key or numerical index of the cell to remove
* @fires removeCell
*/
mw.widgets.RowWidgetModel.prototype.removeCell = function ( handle ) {
var cellProps = this.getCellProperties( handle ),
i, len;
// Exit early if the row couldn't be found
if ( cellProps === null ) {
return;
}
this.cells.splice( cellProps.index, 1 );
this.data.splice( cellProps.index, 1 );
// Update all indexes in following cells
for ( i = cellProps.index, len = this.cells.length; i < len; i++ ) {
this.cells[ i ].index--;
}
this.emit( 'removeCell', cellProps.index );
};
/**
* Clears the row data
*
* @fires clear
*/
mw.widgets.RowWidgetModel.prototype.clear = function () {
this.data = [];
this.verifyData();
this.emit( 'clear', false );
};
/**
* Clears the row data, as well as all cell properties
*
* @fires clear
*/
mw.widgets.RowWidgetModel.prototype.clearWithProperties = function () {
this.data = [];
this.cells = [];
this.emit( 'clear', true );
};
/**
* Get the validation pattern to test cells against
*
* @return {RegExp|Function|string}
*/
mw.widgets.RowWidgetModel.prototype.getValidationPattern = function () {
return this.validate;
};
/**
* Get all row properties
*
* @return {Object}
*/
mw.widgets.RowWidgetModel.prototype.getRowProperties = function () {
return {
index: this.index,
label: this.label,
showLabel: this.showLabel,
isDeletable: this.isDeletable
};
};
/**
* Get properties of a given cell
*
* @param {string|number} handle The key (or numeric index) of the cell
* @return {Object|null} An object containing the `key` and `index` properties of the cell.
* Returns `null` if the cell can't be found.
*/
mw.widgets.RowWidgetModel.prototype.getCellProperties = function ( handle ) {
var cell = null,
i, len;
if ( typeof handle === 'string' ) {
for ( i = 0, len = this.cells.length; i < len; i++ ) {
if ( this.cells[ i ].key === handle ) {
cell = this.cells[ i ];
break;
}
}
} else if ( typeof handle === 'number' ) {
if ( handle < this.cells.length ) {
cell = this.cells[ handle ];
}
}
return cell;
};
/**
* Get properties of all cells
*
* @return {Array} An array of objects containing `key` and `index` properties for each cell
*/
mw.widgets.RowWidgetModel.prototype.getAllCellProperties = function () {
return this.cells.slice();
};

View file

@ -0,0 +1,9 @@
.mw-widgets-tableWidget > .mw-widgets-tableWidget-rows {
float: left;
clear: left;
width: 100%;
}
.mw-widgets-tableWidget.mw-widgets-tableWidget-no-labels .mw-widgets-rowWidget-label {
display: none;
}

View file

@ -0,0 +1,587 @@
/**
* A TableWidget groups {@link mw.widgets.RowWidget row widgets} together to form a bidimensional
* grid of text inputs.
*
* @class
* @extends OO.ui.Widget
* @mixins OO.ui.mixin.GroupElement
*
* @constructor
* @param {Object} [config] Configuration options
* @cfg {Array} [rows] An array of objects containing `key` and `label` properties for every row
* @cfg {Array} [cols] An array of objects containing `key` and `label` properties for every column
* @cfg {Array} [data] An array containing all values of the table
* @cfg {RegExp|Function|string} [validate] Validation pattern to apply on every cell
* @cfg {boolean} [showHeaders=true] Whether or not to show table headers. Defaults to true.
* @cfg {boolean} [showRowLabels=true] Whether or not to show row labels. Defaults to true.
* @cfg {boolean} [allowRowInsertion=true] Whether or not to enable row insertion. Defaults to true.
* @cfg {boolean} [allowRowDeletion=true] Allow row deletion. Defaults to true.
*/
mw.widgets.TableWidget = function MwWidgetsTableWidget( config ) {
var headerRowItems = [],
insertionRowItems = [],
columnProps, prop, i, len;
// Configuration initialization
config = config || {};
// Parent constructor
mw.widgets.TableWidget.super.call( this, config );
// Mixin constructors
OO.ui.mixin.GroupElement.call( this, config );
// Set up model
this.model = new mw.widgets.TableWidgetModel( config );
// Properties
this.listeningToInsertionRowChanges = true;
// Set up group element
this.setGroupElement(
$( '<div>' )
.addClass( 'mw-widgets-tableWidget-rows' )
);
// Set up static rows
columnProps = this.model.getAllColumnProperties();
if ( this.model.getTableProperties().showHeaders ) {
this.headerRow = new mw.widgets.RowWidget( {
deletable: false,
label: null
} );
for ( i = 0, len = columnProps.length; i < len; i++ ) {
prop = columnProps[ i ];
headerRowItems.push( new OO.ui.TextInputWidget( {
value: prop.label ? prop.label : ( prop.key ? prop.key : prop.index ),
// TODO: Allow editing of fields
disabled: true
} ) );
}
this.headerRow.addItems( headerRowItems );
}
if ( this.model.getTableProperties().allowRowInsertion ) {
this.insertionRow = new mw.widgets.RowWidget( {
classes: 'mw-widgets-rowWidget-insertionRow',
deletable: false,
label: null
} );
for ( i = 0, len = columnProps.length; i < len; i++ ) {
insertionRowItems.push( new OO.ui.TextInputWidget( {
data: columnProps[ i ].key ? columnProps[ i ].key : columnProps[ i ].index,
disabled: this.isDisabled()
} ) );
}
this.insertionRow.addItems( insertionRowItems );
}
// Set up initial rows
if ( Array.isArray( config.items ) ) {
this.addItems( config.items );
}
// Events
this.model.connect( this, {
valueChange: 'onValueChange',
insertRow: 'onInsertRow',
insertColumn: 'onInsertColumn',
removeRow: 'onRemoveRow',
removeColumn: 'onRemoveColumn',
clear: 'onClear'
} );
this.aggregate( {
inputChange: 'rowInputChange',
deleteButtonClick: 'rowDeleteButtonClick'
} );
this.connect( this, {
rowInputChange: 'onRowInputChange',
rowDeleteButtonClick: 'onRowDeleteButtonClick'
} );
if ( this.model.getTableProperties().allowRowInsertion ) {
this.insertionRow.connect( this, {
inputChange: 'onInsertionRowInputChange'
} );
}
// Initialization
this.$element.addClass( 'mw-widgets-tableWidget' );
if ( this.model.getTableProperties().showHeaders ) {
this.$element.append( this.headerRow.$element );
}
this.$element.append( this.$group );
if ( this.model.getTableProperties().allowRowInsertion ) {
this.$element.append( this.insertionRow.$element );
}
this.$element.toggleClass(
'mw-widgets-tableWidget-no-labels',
!this.model.getTableProperties().showRowLabels
);
this.model.setupTable();
};
/* Inheritance */
OO.inheritClass( mw.widgets.TableWidget, OO.ui.Widget );
OO.mixinClass( mw.widgets.TableWidget, OO.ui.mixin.GroupElement );
/* Static Properties */
mw.widgets.TableWidget.static.patterns = {
validate: /^[0-9]+(\.[0-9]+)?$/,
filter: /[0-9]+(\.[0-9]+)?/
};
/* Events */
/**
* @event change
*
* Change when the data within the table has been updated.
*
* @param {number} rowIndex The index of the row that changed
* @param {string} rowKey The key of the row that changed, or `undefined` if it doesn't exist
* @param {number} columnIndex The index of the column that changed
* @param {string} columnKey The key of the column that changed, or `undefined` if it doesn't exist
* @param {string} value The new value
*/
/**
* @event removeRow
*
* Fires when a row is removed from the table
*
* @param {number} index The index of the row being deleted
* @param {string} key The key of the row being deleted
*/
/* Methods */
/**
* Set the value of a particular cell
*
* @param {number|string} row The row containing the cell to edit. Can be either
* the row index or string key if one has been set for the row.
* @param {number|string} col The column containing the cell to edit. Can be either
* the column index or string key if one has been set for the column.
* @param {Mixed} value The new value
*/
mw.widgets.TableWidget.prototype.setValue = function ( row, col, value ) {
this.model.setValue( row, col, value );
};
/**
* Set the table data
*
* @param {Array} data The new table data
* @return {boolean} The data has been successfully changed
*/
mw.widgets.TableWidget.prototype.setData = function ( data ) {
if ( !Array.isArray( data ) ) {
return false;
}
this.model.setData( data );
return true;
};
/**
* 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.
*/
mw.widgets.TableWidget.prototype.insertRow = function ( data, index, key, label ) {
this.model.insertRow( data, index, 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.
*/
mw.widgets.TableWidget.prototype.insertColumn = function ( data, index, key, label ) {
this.model.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} key The key or numerical index of the row to remove.
*/
mw.widgets.TableWidget.prototype.removeRow = function ( key ) {
this.model.removeRow( 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} key The key or numerical index of the column to remove.
*/
mw.widgets.TableWidget.prototype.removeColumn = function ( key ) {
this.model.removeColumn( key );
};
/**
* Clears all values from the table, without wiping any row or column properties.
*/
mw.widgets.TableWidget.prototype.clear = function () {
this.model.clear();
};
/**
* Clears the table data, as well as all row and column properties
*/
mw.widgets.TableWidget.prototype.clearWithProperties = function () {
this.model.clearWithProperties();
};
/**
* Filter cell input once it is changed
*
* @param {string} value The input value
* @return {string} The filtered input
*/
mw.widgets.TableWidget.prototype.filterCellInput = function ( value ) {
var matches = value.match( mw.widgets.TableWidget.static.patterns.filter );
return ( Array.isArray( matches ) ) ? matches[ 0 ] : '';
};
/**
* @private
* @inheritdoc
*/
mw.widgets.TableWidget.prototype.addItems = function ( items, index ) {
var i, len;
OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
for ( i = index, len = items.length; i < len; i++ ) {
items[ i ].setIndex( i );
}
};
/**
* @private
* @inheritdoc
*/
mw.widgets.TableWidget.prototype.removeItems = function ( items ) {
var i, len, rows;
OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
rows = this.getItems();
for ( i = 0, len = rows.length; i < len; i++ ) {
rows[ i ].setIndex( i );
}
};
/**
* Handle model value changes
*
* @private
* @param {number} row The row index of the changed cell
* @param {number} col The column index of the changed cell
* @param {Mixed} value The new value
* @fires change
*/
mw.widgets.TableWidget.prototype.onValueChange = function ( row, col, value ) {
var rowProps = this.model.getRowProperties( row ),
colProps = this.model.getColumnProperties( col );
this.getItems()[ row ].setValue( col, value );
this.emit( 'change', row, rowProps.key, col, colProps.key, value );
};
/**
* Handle model row insertions
*
* @private
* @param {Array} data The initial data
* @param {number} index The index in which the new row was inserted
* @param {string} key The row key
* @param {string} label The row label
* @fires change
*/
mw.widgets.TableWidget.prototype.onInsertRow = function ( data, index, key, label ) {
var colProps = this.model.getAllColumnProperties(),
keys = [],
newRow, i, len;
for ( i = 0, len = colProps.length; i < len; i++ ) {
keys.push( ( colProps[ i ].key ) ? colProps[ i ].key : i );
}
newRow = new mw.widgets.RowWidget( {
data: data,
keys: keys,
validate: this.model.getValidationPattern(),
label: label,
showLabel: this.model.getTableProperties().showRowLabels,
deletable: this.model.getTableProperties().allowRowDeletion
} );
newRow.setDisabled( this.isDisabled() );
// TODO: Handle index parameter. Right now all new rows are inserted at the end
this.addItems( [ newRow ] );
// If this is the first data being added, refresh headers and insertion row
if ( this.model.getAllRowProperties().length === 1 ) {
this.refreshTableMarginals();
}
for ( i = 0, len = data.length; i < len; i++ ) {
this.emit( 'change', index, key, i, colProps[ i ].key, data[ i ] );
}
};
/**
* Handle model column insertions
*
* @private
* @param {Array} data The initial data
* @param {number} index The index in which to insert the new column
* @param {string} key The row key
* @param {string} label The row label
*
* @fires change
*/
mw.widgets.TableWidget.prototype.onInsertColumn = function ( data, index, key, label ) {
var tableProps = this.model.getTableProperties(),
items = this.getItems(),
rowProps = this.model.getAllRowProperties(),
i, len;
for ( i = 0, len = items.length; i < len; i++ ) {
items[ i ].insertCell( data[ i ], index, key );
this.emit( 'change', i, rowProps[ i ].key, index, key, data[ i ] );
}
if ( tableProps.showHeaders ) {
this.headerRow.addItems( [
new OO.ui.TextInputWidget( {
value: label || key || index,
// TODO: Allow editing of fields
disabled: true
} )
] );
}
if ( tableProps.handleRowInsertion ) {
this.insertionRow.addItems( [
new OO.ui.TextInputWidget( {
validate: this.model.getValidationPattern(),
disabled: this.isDisabled()
} )
] );
}
};
/**
* Handle model row removals
*
* @private
* @param {number} index The removed row index
* @param {string} key The removed row key
* @fires removeRow
*/
mw.widgets.TableWidget.prototype.onRemoveRow = function ( index, key ) {
this.removeItems( [ this.getItems()[ index ] ] );
this.emit( 'removeRow', index, key );
};
/**
* Handle model column removals
*
* @private
* @param {number} index The removed column index
* @param {string} key The removed column key
* @fires removeColumn
*/
mw.widgets.TableWidget.prototype.onRemoveColumn = function ( index, key ) {
var i, items = this.getItems();
for ( i = 0; i < items.length; i++ ) {
items[ i ].removeCell( index );
}
this.emit( 'removeColumn', index, key );
};
/**
* Handle model table clears
*
* @private
* @param {boolean} withProperties Clear row/column properties
*/
mw.widgets.TableWidget.prototype.onClear = function ( withProperties ) {
var i, len, rows;
if ( withProperties ) {
this.removeItems( this.getItems() );
} else {
rows = this.getItems();
for ( i = 0, len = rows.length; i < len; i++ ) {
rows[ i ].clear();
}
}
};
/**
* React to input changes bubbled up from event aggregation
*
* @private
* @param {mw.widgets.RowWidget} row The row that changed
* @param {number} colIndex The column index of the cell that changed
* @param {string} value The new value of the input
* @fires change
*/
mw.widgets.TableWidget.prototype.onRowInputChange = function ( row, colIndex, value ) {
var items = this.getItems(),
i, len, rowIndex;
for ( i = 0, len = items.length; i < len; i++ ) {
if ( row === items[ i ] ) {
rowIndex = i;
break;
}
}
this.model.setValue( rowIndex, colIndex, value );
};
/**
* React to new row input changes
*
* @private
* @param {number} colIndex The column index of the input that fired the change
* @param {string} value The new row value
*/
mw.widgets.TableWidget.prototype.onInsertionRowInputChange = function ( colIndex, value ) {
var insertionRowItems = this.insertionRow.getItems(),
newRowData = [],
i, len, lastRow;
if ( this.listeningToInsertionRowChanges ) {
for ( i = 0, len = insertionRowItems.length; i < len; i++ ) {
if ( i === colIndex ) {
newRowData.push( value );
} else {
newRowData.push( '' );
}
}
this.insertRow( newRowData );
// Focus newly inserted row
lastRow = this.getItems().slice( -1 )[ 0 ];
lastRow.getItems()[ colIndex ].focus();
// Reset insertion row
this.listeningToInsertionRowChanges = false;
this.insertionRow.clear();
this.listeningToInsertionRowChanges = true;
}
};
/**
* Handle row deletion input
*
* @private
* @param {mw.widgets.RowWidget} row The row that asked for the deletion
*/
mw.widgets.TableWidget.prototype.onRowDeleteButtonClick = function ( row ) {
var items = this.getItems(),
i = -1,
len;
for ( i = 0, len = items.length; i < len; i++ ) {
if ( items[ i ] === row ) {
break;
}
}
this.removeRow( i );
};
/**
* @inheritdoc
*/
mw.widgets.TableWidget.prototype.setDisabled = function ( disabled ) {
// Parent method
mw.widgets.TableWidget.super.prototype.setDisabled.call( this, disabled );
if ( !this.items ) {
return;
}
this.getItems().forEach( function ( row ) {
row.setDisabled( disabled );
} );
this.insertionRow.getItems().forEach( function ( row ) {
row.setDisabled( disabled );
} );
};
/**
* Refresh table header and insertion row
*/
mw.widgets.TableWidget.prototype.refreshTableMarginals = function () {
var tableProps = this.model.getTableProperties(),
columnProps = this.model.getAllColumnProperties(),
rowItems,
i, len;
if ( tableProps.showHeaders ) {
this.headerRow.removeItems( this.headerRow.getItems() );
rowItems = [];
for ( i = 0, len = columnProps.length; i < len; i++ ) {
rowItems.push( new OO.ui.TextInputWidget( {
value: columnProps[ i ].key ? columnProps[ i ].key : columnProps[ i ].index,
// TODO: Allow editing of fields
disabled: true
} ) );
}
this.headerRow.addItems( rowItems );
}
if ( tableProps.allowRowInsertion ) {
this.insertionRow.clear();
this.insertionRow.removeItems( this.insertionRow.getItems() );
for ( i = 0, len = columnProps.length; i < len; i++ ) {
this.insertionRow.insertCell( '', columnProps[ i ].index, columnProps[ i ].key );
}
}
};

View file

@ -0,0 +1,519 @@
/*!
* MediaWiki Widgets TableWidgetModel class.
*
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* TableWidget model.
*
* @class
* @mixins OO.EventEmitter
*
* @constructor
* @param {Object} [config] Configuration options
* @cfg {Array} [rows] An array of objects containing `key` and `label` properties for every row
* @cfg {Array} [cols] An array of objects containing `key` and `label` properties for every column
* @cfg {Array} [data] An array containing all values of the table
* @cfg {RegExp|Function|string} [validate] Validation pattern to apply on every cell
* @cfg {boolean} [showHeaders=true] Show table header row. Defaults to true.
* @cfg {boolean} [showRowLabels=true] Show row labels. Defaults to true.
* @cfg {boolean} [allowRowInsertion=true] Allow row insertion. Defaults to true.
* @cfg {boolean} [allowRowDeletion=true] Allow row deletion. Defaults to true.
*/
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 ) {
var 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 */
/**
* @event valueChange
*
* Fired when a value inside the table has changed.
*
* @param {number} row The row index of the updated cell
* @param {number} column The column index of the updated cell
* @param {Mixed} value The new value
*/
/**
* @event insertRow
*
* Fired when a new row is inserted into the table.
*
* @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
*/
/**
* @event insertColumn
*
* Fired when a new row is inserted into the table.
*
* @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
*/
/**
* @event removeRow
*
* Fired when a row is removed from the table.
*
* @param {number} index The removed row index
* @param {string} key The removed row key
*/
/**
* @event removeColumn
*
* Fired when a column is removed from the table.
*
* @param {number} index The removed column index
* @param {string} key The removed column key
*/
/**
* @event clear
*
* Fired when the table data is wiped.
*
* @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
var 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 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 () {
var 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 insertRow
*/
mw.widgets.TableWidgetModel.prototype.buildTable = function () {
var 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 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 {Mixed} value The new value
* @fires valueChange
*/
mw.widgets.TableWidgetModel.prototype.setValue = function ( row, col, value ) {
var 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 insertRow
*/
mw.widgets.TableWidgetModel.prototype.insertRow = function ( data, index, key, label ) {
var insertIndex = ( typeof index === 'number' ) ? index : this.rows.length,
newRowData = [],
insertData, insertDataCell, i, len;
// Add the new row metadata
this.rows.splice( insertIndex, 0, {
index: insertIndex,
key: key || undefined,
label: label || undefined
} );
// Add the new row data
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 ( 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 ( 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 insertColumn
*/
mw.widgets.TableWidgetModel.prototype.insertColumn = function ( data, index, key, label ) {
var insertIndex = ( typeof index === 'number' ) ? index : this.cols.length,
insertDataCell, insertData, i, len;
// Add the new column metadata
this.cols.splice( insertIndex, 0, {
index: insertIndex,
key: key || undefined,
label: label || undefined
} );
// Add the new column data
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
for ( 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 ( 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 removeRow
*/
mw.widgets.TableWidgetModel.prototype.removeRow = function ( handle ) {
var rowProps = this.getRowProperties( handle ),
i, len;
// 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 ( 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 removeColumn
*/
mw.widgets.TableWidgetModel.prototype.removeColumn = function ( handle ) {
var colProps = this.getColumnProperties( handle ),
i, len;
// Exit early if the column couldn't be found
if ( colProps === null ) {
return;
}
this.cols.splice( colProps.index, 1 );
for ( i = 0, len = this.data.length; i < len; i++ ) {
this.data[ i ].splice( colProps.index, 1 );
}
// Update all indexes in following columns
for ( 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 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 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();
};

View file

@ -99,6 +99,7 @@ return [
'tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js',
'tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js',
'tests/qunit/suites/resources/mediawiki.widgets/MediaSearch/mediawiki.widgets.APIResultsQueue.test.js',
'tests/qunit/suites/resources/mediawiki.widgets/Table/mediawiki.widgets.TableWidget.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js',

View file

@ -0,0 +1,96 @@
/*!
* MediaWiki Widgets TableWidget tests.
*/
QUnit.module( 'mediawiki.widgets.TableWidget' );
( function () {
// The test verifes columns can be inserted and seem valid. However the
// code itself is bugged and columns end up duplicated.
//
// See https://phabricator.wikimedia.org/T151262#4253730
QUnit.skip( 'mw.widgets.TableWidget', function ( assert ) {
var widgetA = new mw.widgets.TableWidget( {
rows: [
{
key: 'foo',
label: 'Foo'
},
{
key: 'bar',
label: 'Bar'
}
],
cols: [
{
label: 'A'
},
{
label: 'B'
}
]
} ),
widgetB = new mw.widgets.TableWidget( {
rows: [
{
key: 'foo'
},
{
key: 'bar'
}
],
cols: [
{}, {}, {}
],
data: [
[ '11', '12', '13' ],
[ '21', '22', '23' ]
]
} ),
widgetAexpectedRowProps = {
index: 1,
key: 'bar',
label: 'Bar'
},
widgetAexpectedInitialData = [
[ '', '' ],
[ '', '' ]
],
widgetAexpectedDataAfterValue = [
[ '', '' ],
[ '3', '' ]
],
widgetAexpectedDataAfterRowColumnInsertions = [
[ '', '', '' ],
[ 'a', 'b', 'c' ],
[ '3', '', '' ]
],
widgetAexpectedDataAfterColumnRemoval = [
[ '', '' ],
[ 'b', 'c' ],
[ '', '' ]
];
assert.deepEqual( widgetA.model.data, widgetAexpectedInitialData, 'Table data is initialized properly' );
assert.deepEqual( widgetA.model.getRowProperties( 'bar' ), widgetAexpectedRowProps, 'Row props are returned successfully' );
widgetA.setValue( 'bar', 0, '3' );
assert.deepEqual( widgetA.model.data, widgetAexpectedDataAfterValue, 'Table data is modified successfully' );
widgetA.insertColumn();
widgetA.insertRow( [ 'a', 'b', 'c' ], 1, 'baz', 'Baz' );
assert.deepEqual( widgetA.model.data, widgetAexpectedDataAfterRowColumnInsertions, 'Row and column are added successfully' );
widgetA.removeColumn( 0 );
assert.deepEqual( widgetA.model.data, widgetAexpectedDataAfterColumnRemoval, 'Columns are removed successfully' );
widgetA.removeRow( -1 );
assert.deepEqual( widgetA.model.data, widgetAexpectedDataAfterColumnRemoval, 'Invalid row removal by index does not change table data' );
widgetA.removeRow( 'qux' );
assert.deepEqual( widgetA.model.data, widgetAexpectedDataAfterColumnRemoval, 'Invalid row removal by key does not change table data' );
assert.deepEqual( widgetB.getItems()[ 0 ].getItems()[ 2 ].getValue(), '13', 'Initial data is populated in text inputs properly' );
} );
}() );