2 * MediaWiki Widgets TableWidgetModel class.
4 * @license The MIT License (MIT); see LICENSE.txt
8 * Configuration options.
10 * @typedef {Object} mw.widgets.TableWidgetModel~Config
11 * @property {Array} [rows] An array of objects containing `key` and `label` properties for every row
12 * @property {Array} [cols] An array of objects containing `key` and `label` properties for every column
13 * @property {Array} [data] An array containing all values of the table
14 * @property {RegExp|Function|string} [validate] Validation pattern to apply on every cell
15 * @property {boolean} [showHeaders=true] Show table header row. Defaults to true.
16 * @property {boolean} [showRowLabels=true] Show row labels. Defaults to true.
17 * @property {boolean} [allowRowInsertion=true] Allow row insertion. Defaults to true.
18 * @property {boolean} [allowRowDeletion=true] Allow row deletion. Defaults to true.
22 * @classdesc TableWidget model.
25 * @mixes OO.EventEmitter
28 * @description Create an instance of `mw.widgets.TableWidgetModel`.
29 * @param {mw.widgets.TableWidgetModel~Config} [config] Configuration options
31 mw.widgets.TableWidgetModel = function MwWidgetsTableWidgetModel( config ) {
32 config = config || {};
35 OO.EventEmitter.call( this, config );
37 this.data = config.data || [];
38 this.validate = config.validate;
39 this.showHeaders = ( config.showHeaders !== undefined ) ? !!config.showHeaders : true;
40 this.showRowLabels = ( config.showRowLabels !== undefined ) ? !!config.showRowLabels : true;
41 this.allowRowInsertion = ( config.allowRowInsertion !== undefined ) ?
42 !!config.allowRowInsertion : true;
43 this.allowRowDeletion = ( config.allowRowDeletion !== undefined ) ?
44 !!config.allowRowDeletion : true;
46 this.initializeProps( config.rows, config.cols );
51 OO.mixinClass( mw.widgets.TableWidgetModel, OO.EventEmitter );
56 * Get an entry from a props table
60 * @param {string|number} handle The key (or numeric index) of the row/column
61 * @param {Array} table Props table
62 * @return {Object|null} An object containing the `key`, `index` and `label`
63 * properties of the row/column. Returns `null` if the row/column can't be found.
65 mw.widgets.TableWidgetModel.static.getEntryFromPropsTable = function ( handle, table ) {
69 if ( typeof handle === 'string' ) {
70 for ( i = 0, len = table.length; i < len; i++ ) {
71 if ( table[ i ].key === handle ) {
76 } else if ( typeof handle === 'number' ) {
77 if ( handle < table.length ) {
78 row = table[ handle ];
88 * Fired when a value inside the table has changed.
90 * @event mw.widgets.TableWidgetModel.valueChange
91 * @param {number} row The row index of the updated cell
92 * @param {number} column The column index of the updated cell
93 * @param {any} value The new value
97 * Fired when a new row is inserted into the table.
99 * @event mw.widgets.TableWidgetModel.insertRow
100 * @param {Array} data The initial data
101 * @param {number} index The index in which to insert the new row
102 * @param {string} key The row key
103 * @param {string} label The row label
107 * Fired when a new row is inserted into the table.
109 * @event mw.widgets.TableWidgetModel.insertColumn
110 * @param {Array} data The initial data
111 * @param {number} index The index in which to insert the new column
112 * @param {string} key The column key
113 * @param {string} label The column label
117 * Fired when a row is removed from the table.
119 * @event mw.widgets.TableWidgetModel.removeRow
120 * @param {number} index The removed row index
121 * @param {string} key The removed row key
125 * Fired when a column is removed from the table.
127 * @event mw.widgets.TableWidgetModel.removeColumn
128 * @param {number} index The removed column index
129 * @param {string} key The removed column key
133 * Fired when the table data is wiped.
135 * @event mw.widgets.TableWidgetModel.clear
136 * @param {boolean} clear Clear row/column properties
142 * Initializes and ensures the proper creation of the rows and cols property arrays.
143 * If data exceeds the number of rows and cols given, new ones will be created.
146 * @param {Array} rowProps The initial row props
147 * @param {Array} colProps The initial column props
149 mw.widgets.TableWidgetModel.prototype.initializeProps = function ( rowProps, colProps ) {
150 // FIXME: Account for extra data with missing row/col metadata
157 if ( Array.isArray( rowProps ) ) {
158 for ( i = 0, len = rowProps.length; i < len; i++ ) {
161 key: rowProps[ i ].key,
162 label: rowProps[ i ].label
167 if ( Array.isArray( colProps ) ) {
168 for ( i = 0, len = colProps.length; i < len; i++ ) {
171 key: colProps[ i ].key,
172 label: colProps[ i ].label
179 * Triggers the initialization process and builds the initial table.
181 * @fires mw.widgets.TableWidgetModel.insertRow
183 mw.widgets.TableWidgetModel.prototype.setupTable = function () {
189 * Verifies if the table data is complete and synced with
190 * row and column properties, and adds empty strings as
191 * cell data if cells are missing
195 mw.widgets.TableWidgetModel.prototype.verifyData = function () {
196 let i, j, rowLen, colLen;
198 for ( i = 0, rowLen = this.rows.length; i < rowLen; i++ ) {
199 if ( this.data[ i ] === undefined ) {
200 this.data.push( [] );
203 for ( j = 0, colLen = this.cols.length; j < colLen; j++ ) {
204 if ( this.data[ i ][ j ] === undefined ) {
205 this.data[ i ].push( '' );
212 * Build initial table
215 * @fires mw.widgets.TableWidgetModel.insertRow
217 mw.widgets.TableWidgetModel.prototype.buildTable = function () {
220 for ( i = 0, len = this.rows.length; i < len; i++ ) {
221 this.emit( 'insertRow', this.data[ i ], i, this.rows[ i ].key, this.rows[ i ].label );
226 * Refresh the entire table with new data
229 * @fires mw.widgets.TableWidgetModel.insertRow
231 mw.widgets.TableWidgetModel.prototype.refreshTable = function () {
232 // TODO: Clear existing table
238 * Set the value of a particular cell.
240 * @param {number|string} row The index or key of the row
241 * @param {number|string} col The index or key of the column
242 * @param {any} value The new value
243 * @fires mw.widgets.TableWidgetModel.valueChange
245 mw.widgets.TableWidgetModel.prototype.setValue = function ( row, col, value ) {
246 let rowIndex, colIndex;
248 if ( typeof row === 'number' ) {
250 } else if ( typeof row === 'string' ) {
251 rowIndex = this.getRowProperties( row ).index;
254 if ( typeof col === 'number' ) {
256 } else if ( typeof col === 'string' ) {
257 colIndex = this.getColumnProperties( col ).index;
260 if ( typeof rowIndex === 'number' && typeof colIndex === 'number' &&
261 this.data[ rowIndex ] !== undefined && this.data[ rowIndex ][ colIndex ] !== undefined &&
262 this.data[ rowIndex ][ colIndex ] !== value ) {
264 this.data[ rowIndex ][ colIndex ] = value;
265 this.emit( 'valueChange', rowIndex, colIndex, value );
270 * Set the table data.
272 * @param {Array} data The new table data
274 mw.widgets.TableWidgetModel.prototype.setData = function ( data ) {
275 if ( Array.isArray( data ) ) {
284 * Inserts a row into the table. If the row isn't added at the end of the table,
285 * all the following data will be shifted back one row.
287 * @param {Array} [data] The data to insert to the row.
288 * @param {number} [index] The index in which to insert the new row.
289 * If unset or set to null, the row will be added at the end of the table.
290 * @param {string} [key] A key to quickly select this row.
291 * If unset or set to null, no key will be set.
292 * @param {string} [label] A label to display next to the row.
293 * If unset or set to null, the key will be used if there is one.
294 * @fires mw.widgets.TableWidgetModel.insertRow
296 mw.widgets.TableWidgetModel.prototype.insertRow = function ( data, index, key, label ) {
297 const insertIndex = ( typeof index === 'number' ) ? index : this.rows.length;
299 // Add the new row metadata
300 this.rows.splice( insertIndex, 0, {
302 key: key || undefined,
303 label: label || undefined
306 const newRowData = [];
309 // Add the new row data
310 const insertData = ( Array.isArray( data ) ) ? data : [];
311 // Ensure that all columns of data for this row have been supplied,
312 // otherwise fill the remaining data with empty strings
314 for ( let i = 0, len = this.cols.length; i < len; i++ ) {
316 if ( typeof insertData[ i ] === 'string' || typeof insertData[ i ] === 'number' ) {
317 insertDataCell = insertData[ i ];
320 newRowData.push( insertDataCell );
322 this.data.splice( insertIndex, 0, newRowData );
324 // Update all indexes in following rows
325 for ( let i = insertIndex + 1, len = this.rows.length; i < len; i++ ) {
326 this.rows[ i ].index++;
329 this.emit( 'insertRow', data, insertIndex, key, label );
333 * Inserts a column into the table. If the column isn't added at the end of the table,
334 * all the following data will be shifted back one column.
336 * @param {Array} [data] The data to insert to the column.
337 * @param {number} [index] The index in which to insert the new column.
338 * If unset or set to null, the column will be added at the end of the table.
339 * @param {string} [key] A key to quickly select this column.
340 * If unset or set to null, no key will be set.
341 * @param {string} [label] A label to display next to the column.
342 * If unset or set to null, the key will be used if there is one.
343 * @fires mw.widgets.TableWidgetModel.insertColumn
345 mw.widgets.TableWidgetModel.prototype.insertColumn = function ( data, index, key, label ) {
346 const insertIndex = ( typeof index === 'number' ) ? index : this.cols.length;
348 // Add the new column metadata
349 this.cols.splice( insertIndex, 0, {
351 key: key || undefined,
352 label: label || undefined
355 // Add the new column data
356 const insertData = ( Array.isArray( data ) ) ? data : [];
357 // Ensure that all rows of data for this column have been supplied,
358 // otherwise fill the remaining data with empty strings
362 for ( let i = 0, len = this.rows.length; i < len; i++ ) {
364 if ( typeof insertData[ i ] === 'string' || typeof insertData[ i ] === 'number' ) {
365 insertDataCell = insertData[ i ];
368 this.data[ i ].splice( insertIndex, 0, insertDataCell );
371 // Update all indexes in following cols
372 for ( let i = insertIndex + 1, len = this.cols.length; i < len; i++ ) {
373 this.cols[ i ].index++;
376 this.emit( 'insertColumn', data, index, key, label );
380 * Removes a row from the table. If the row removed isn't at the end of the table,
381 * all the following rows will be shifted back one row.
383 * @param {number|string} handle The key or numerical index of the row to remove
384 * @fires mw.widgets.TableWidgetModel.removeRow
386 mw.widgets.TableWidgetModel.prototype.removeRow = function ( handle ) {
387 const rowProps = this.getRowProperties( handle );
389 // Exit early if the row couldn't be found
390 if ( rowProps === null ) {
394 this.rows.splice( rowProps.index, 1 );
395 this.data.splice( rowProps.index, 1 );
397 // Update all indexes in following rows
398 for ( let i = rowProps.index, len = this.rows.length; i < len; i++ ) {
399 this.rows[ i ].index--;
402 this.emit( 'removeRow', rowProps.index, rowProps.key );
406 * Removes a column from the table. If the column removed isn't at the end of the table,
407 * all the following columns will be shifted back one column.
409 * @param {number|string} handle The key or numerical index of the column to remove
410 * @fires mw.widgets.TableWidgetModel.removeColumn
412 mw.widgets.TableWidgetModel.prototype.removeColumn = function ( handle ) {
413 const colProps = this.getColumnProperties( handle );
415 // Exit early if the column couldn't be found
416 if ( colProps === null ) {
420 this.cols.splice( colProps.index, 1 );
422 for ( let i = 0, len = this.data.length; i < len; i++ ) {
423 this.data[ i ].splice( colProps.index, 1 );
426 // Update all indexes in following columns
427 for ( let i = colProps.index, len = this.cols.length; i < len; i++ ) {
428 this.cols[ i ].index--;
431 this.emit( 'removeColumn', colProps.index, colProps.key );
435 * Clears the table data.
437 * @fires mw.widgets.TableWidgetModel.clear
439 mw.widgets.TableWidgetModel.prototype.clear = function () {
443 this.emit( 'clear', false );
447 * Clears the table data, as well as all row and column properties.
449 * @fires mw.widgets.TableWidgetModel.clear
451 mw.widgets.TableWidgetModel.prototype.clearWithProperties = function () {
456 this.emit( 'clear', true );
460 * Get all table properties.
464 mw.widgets.TableWidgetModel.prototype.getTableProperties = function () {
466 showHeaders: this.showHeaders,
467 showRowLabels: this.showRowLabels,
468 allowRowInsertion: this.allowRowInsertion,
469 allowRowDeletion: this.allowRowDeletion
474 * Get the validation pattern to test cells against.
476 * @return {RegExp|Function|string}
478 mw.widgets.TableWidgetModel.prototype.getValidationPattern = function () {
479 return this.validate;
483 * Get properties of a given row.
485 * @param {string|number} handle The key (or numeric index) of the row
486 * @return {Object|null} An object containing the `key`, `index` and `label` properties of the row.
487 * Returns `null` if the row can't be found.
489 mw.widgets.TableWidgetModel.prototype.getRowProperties = function ( handle ) {
490 return mw.widgets.TableWidgetModel.static.getEntryFromPropsTable( handle, this.rows );
494 * Get properties of all rows.
496 * @return {Array} An array of objects containing `key`, `index` and `label` properties for each row
498 mw.widgets.TableWidgetModel.prototype.getAllRowProperties = function () {
499 return this.rows.slice();
503 * Get properties of a given column.
505 * @param {string|number} handle The key (or numeric index) of the column
506 * @return {Object|null} An object containing the `key`, `index` and
507 * `label` properties of the column.
508 * Returns `null` if the column can't be found.
510 mw.widgets.TableWidgetModel.prototype.getColumnProperties = function ( handle ) {
511 return mw.widgets.TableWidgetModel.static.getEntryFromPropsTable( handle, this.cols );
515 * Get properties of all columns.
517 * @return {Array} An array of objects containing `key`, `index` and
518 * `label` properties for each column
520 mw.widgets.TableWidgetModel.prototype.getAllColumnProperties = function () {
521 return this.cols.slice();