Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.widgets / Table / mw.widgets.TableWidgetModel.js
blob72a3d90fef70fd090e80dd9505840583ff42eae6
1 /*!
2  * MediaWiki Widgets TableWidgetModel class.
3  *
4  * @license The MIT License (MIT); see LICENSE.txt
5  */
7 /**
8  * Configuration options.
9  *
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.
19  */
21 /**
22  * @classdesc TableWidget model.
23  *
24  * @class
25  * @mixes OO.EventEmitter
26  *
27  * @constructor
28  * @description Create an instance of `mw.widgets.TableWidgetModel`.
29  * @param {mw.widgets.TableWidgetModel~Config} [config] Configuration options
30  */
31 mw.widgets.TableWidgetModel = function MwWidgetsTableWidgetModel( config ) {
32         config = config || {};
34         // Mixin constructors
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 );
49 /* Inheritance */
51 OO.mixinClass( mw.widgets.TableWidgetModel, OO.EventEmitter );
53 /* Static Methods */
55 /**
56  * Get an entry from a props table
57  *
58  * @static
59  * @private
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.
64  */
65 mw.widgets.TableWidgetModel.static.getEntryFromPropsTable = function ( handle, table ) {
66         let row = null,
67                 i, len;
69         if ( typeof handle === 'string' ) {
70                 for ( i = 0, len = table.length; i < len; i++ ) {
71                         if ( table[ i ].key === handle ) {
72                                 row = table[ i ];
73                                 break;
74                         }
75                 }
76         } else if ( typeof handle === 'number' ) {
77                 if ( handle < table.length ) {
78                         row = table[ handle ];
79                 }
80         }
82         return row;
85 /* Events */
87 /**
88  * Fired when a value inside the table has changed.
89  *
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
94  */
96 /**
97  * Fired when a new row is inserted into the table.
98  *
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
104  */
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
114  */
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
122  */
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
130  */
133  * Fired when the table data is wiped.
135  * @event mw.widgets.TableWidgetModel.clear
136  * @param {boolean} clear Clear row/column properties
137  */
139 /* Methods */
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.
145  * @private
146  * @param {Array} rowProps The initial row props
147  * @param {Array} colProps The initial column props
148  */
149 mw.widgets.TableWidgetModel.prototype.initializeProps = function ( rowProps, colProps ) {
150         // FIXME: Account for extra data with missing row/col metadata
152         let i, len;
154         this.rows = [];
155         this.cols = [];
157         if ( Array.isArray( rowProps ) ) {
158                 for ( i = 0, len = rowProps.length; i < len; i++ ) {
159                         this.rows.push( {
160                                 index: i,
161                                 key: rowProps[ i ].key,
162                                 label: rowProps[ i ].label
163                         } );
164                 }
165         }
167         if ( Array.isArray( colProps ) ) {
168                 for ( i = 0, len = colProps.length; i < len; i++ ) {
169                         this.cols.push( {
170                                 index: i,
171                                 key: colProps[ i ].key,
172                                 label: colProps[ i ].label
173                         } );
174                 }
175         }
179  * Triggers the initialization process and builds the initial table.
181  * @fires mw.widgets.TableWidgetModel.insertRow
182  */
183 mw.widgets.TableWidgetModel.prototype.setupTable = function () {
184         this.verifyData();
185         this.buildTable();
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
193  * @private
194  */
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( [] );
201                 }
203                 for ( j = 0, colLen = this.cols.length; j < colLen; j++ ) {
204                         if ( this.data[ i ][ j ] === undefined ) {
205                                 this.data[ i ].push( '' );
206                         }
207                 }
208         }
212  * Build initial table
214  * @private
215  * @fires mw.widgets.TableWidgetModel.insertRow
216  */
217 mw.widgets.TableWidgetModel.prototype.buildTable = function () {
218         let i, len;
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 );
222         }
226  * Refresh the entire table with new data
228  * @private
229  * @fires mw.widgets.TableWidgetModel.insertRow
230  */
231 mw.widgets.TableWidgetModel.prototype.refreshTable = function () {
232         // TODO: Clear existing table
234         this.buildTable();
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
244  */
245 mw.widgets.TableWidgetModel.prototype.setValue = function ( row, col, value ) {
246         let rowIndex, colIndex;
248         if ( typeof row === 'number' ) {
249                 rowIndex = row;
250         } else if ( typeof row === 'string' ) {
251                 rowIndex = this.getRowProperties( row ).index;
252         }
254         if ( typeof col === 'number' ) {
255                 colIndex = col;
256         } else if ( typeof col === 'string' ) {
257                 colIndex = this.getColumnProperties( col ).index;
258         }
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 );
266         }
270  * Set the table data.
272  * @param {Array} data The new table data
273  */
274 mw.widgets.TableWidgetModel.prototype.setData = function ( data ) {
275         if ( Array.isArray( data ) ) {
276                 this.data = data;
278                 this.verifyData();
279                 this.refreshTable();
280         }
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
295  */
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, {
301                 index: insertIndex,
302                 key: key || undefined,
303                 label: label || undefined
304         } );
306         const newRowData = [];
307         let insertDataCell;
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++ ) {
315                 insertDataCell = '';
316                 if ( typeof insertData[ i ] === 'string' || typeof insertData[ i ] === 'number' ) {
317                         insertDataCell = insertData[ i ];
318                 }
320                 newRowData.push( insertDataCell );
321         }
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++;
327         }
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
344  */
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, {
350                 index: insertIndex,
351                 key: key || undefined,
352                 label: label || undefined
353         } );
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
360         let insertDataCell;
362         for ( let i = 0, len = this.rows.length; i < len; i++ ) {
363                 insertDataCell = '';
364                 if ( typeof insertData[ i ] === 'string' || typeof insertData[ i ] === 'number' ) {
365                         insertDataCell = insertData[ i ];
366                 }
368                 this.data[ i ].splice( insertIndex, 0, insertDataCell );
369         }
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++;
374         }
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
385  */
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 ) {
391                 return;
392         }
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--;
400         }
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
411  */
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 ) {
417                 return;
418         }
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 );
424         }
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--;
429         }
431         this.emit( 'removeColumn', colProps.index, colProps.key );
435  * Clears the table data.
437  * @fires mw.widgets.TableWidgetModel.clear
438  */
439 mw.widgets.TableWidgetModel.prototype.clear = function () {
440         this.data = [];
441         this.verifyData();
443         this.emit( 'clear', false );
447  * Clears the table data, as well as all row and column properties.
449  * @fires mw.widgets.TableWidgetModel.clear
450  */
451 mw.widgets.TableWidgetModel.prototype.clearWithProperties = function () {
452         this.data = [];
453         this.rows = [];
454         this.cols = [];
456         this.emit( 'clear', true );
460  * Get all table properties.
462  * @return {Object}
463  */
464 mw.widgets.TableWidgetModel.prototype.getTableProperties = function () {
465         return {
466                 showHeaders: this.showHeaders,
467                 showRowLabels: this.showRowLabels,
468                 allowRowInsertion: this.allowRowInsertion,
469                 allowRowDeletion: this.allowRowDeletion
470         };
474  * Get the validation pattern to test cells against.
476  * @return {RegExp|Function|string}
477  */
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.
488  */
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
497  */
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.
509  */
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
519  */
520 mw.widgets.TableWidgetModel.prototype.getAllColumnProperties = function () {
521         return this.cols.slice();