lightning-base-components 1.13.10-alpha → 1.14.4-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/metadata/raptor.json +24 -0
  2. package/package.json +20 -4
  3. package/scopedImports/@salesforce-internal-core.appVersion.js +1 -1
  4. package/scopedImports/@salesforce-label-LightningDualListbox.movedOptionsPlural.js +1 -0
  5. package/scopedImports/@salesforce-label-LightningDualListbox.movedOptionsSingular.js +1 -0
  6. package/scopedImports/@salesforce-label-LightningErrorMessage.validitySelectAtleastOne.js +1 -0
  7. package/scopedImports/@salesforce-label-LightningMap.titleWithAddress.js +1 -0
  8. package/scopedImports/@salesforce-label-LightningModalBase.cancelandclose.js +1 -0
  9. package/src/lightning/ariaObserver/__component__/ariaObserver.spec.js +112 -0
  10. package/src/lightning/ariaObserver/__docs__/ariaObserver.md +142 -0
  11. package/src/lightning/{utilsPrivate/contentMutation.js → ariaObserver/ariaObserver.js} +60 -98
  12. package/src/lightning/buttonMenu/keyboard.js +0 -10
  13. package/src/lightning/card/card.html +6 -0
  14. package/src/lightning/checkboxGroup/checkboxGroup.html +2 -2
  15. package/src/lightning/checkboxGroup/checkboxGroup.js +6 -1
  16. package/src/lightning/colorPickerCustom/colorPickerCustom.js +20 -1
  17. package/src/lightning/datatable/__docs__/datatable.md +55 -0
  18. package/src/lightning/datatable/__examples__/basic/basic.html +1 -1
  19. package/src/lightning/datatable/columns-shared.js +1 -1
  20. package/src/lightning/datatable/datatable.js +98 -30
  21. package/src/lightning/datatable/errors.js +20 -9
  22. package/src/lightning/datatable/headerActions.js +77 -49
  23. package/src/lightning/datatable/infiniteLoading.js +100 -28
  24. package/src/lightning/datatable/inlineEdit.js +505 -379
  25. package/src/lightning/datatable/inlineEditShared.js +24 -0
  26. package/src/lightning/datatable/keyboard.js +162 -127
  27. package/src/lightning/datatable/renderManager.js +201 -133
  28. package/src/lightning/datatable/rowLevelActions.js +17 -13
  29. package/src/lightning/datatable/rowNumber.js +54 -20
  30. package/src/lightning/datatable/rowSelection.js +760 -0
  31. package/src/lightning/datatable/rowSelectionShared.js +79 -0
  32. package/src/lightning/datatable/rows.js +17 -6
  33. package/src/lightning/datatable/state.js +16 -2
  34. package/src/lightning/datatable/templates/div/div.css +4 -0
  35. package/src/lightning/datatable/templates/div/div.html +6 -0
  36. package/src/lightning/datatable/templates/table/table.html +5 -0
  37. package/src/lightning/datatable/utils.js +14 -0
  38. package/src/lightning/datatable/wrapText.js +77 -47
  39. package/src/lightning/dualListbox/dualListbox.html +1 -1
  40. package/src/lightning/dualListbox/dualListbox.js +42 -0
  41. package/src/lightning/formattedDateTime/__docs__/formattedDateTime.md +36 -3
  42. package/src/lightning/formattedDateTime/__examples__/datetime/datetime.html +2 -2
  43. package/src/lightning/formattedDateTime/__examples__/datetime/datetime.js +3 -1
  44. package/src/lightning/formattedDateTime/__examples__/time/time.html +1 -1
  45. package/src/lightning/formattedDateTime/__examples__/time/time.js +3 -1
  46. package/src/lightning/formattedDateTime/formattedDateTime.js +1 -0
  47. package/src/lightning/input/input.html +1 -5
  48. package/src/lightning/input/input.js +69 -48
  49. package/src/lightning/inputUtils/validity.js +12 -1
  50. package/src/lightning/pillContainer/__docs__/pillContainer.md +45 -1
  51. package/src/lightning/primitiveCellActions/primitiveCellActions.js +69 -12
  52. package/src/lightning/primitiveCellFactory/cellWithStandardLayout.html +13 -11
  53. package/src/lightning/primitiveCellFactory/primitiveCellFactory.js +13 -8
  54. package/src/lightning/primitiveDatatableIeditPanel/primitiveDatatableIeditPanel.html +17 -14
  55. package/src/lightning/primitiveDatatableIeditPanel/primitiveDatatableIeditPanel.js +167 -98
  56. package/src/lightning/primitiveDatatableIeditTypeFactory/primitiveDatatableIeditTypeFactory.js +94 -69
  57. package/src/lightning/primitiveDatatableStatusBar/primitiveDatatableStatusBar.html +4 -4
  58. package/src/lightning/primitiveDatatableStatusBar/primitiveDatatableStatusBar.js +4 -4
  59. package/src/lightning/primitiveHeaderActions/primitiveHeaderActions.js +99 -37
  60. package/src/lightning/progressIndicator/progressIndicator.js +1 -1
  61. package/src/lightning/progressStep/progressStep.js +30 -22
  62. package/src/lightning/staticMap/staticMap.html +1 -0
  63. package/src/lightning/staticMap/staticMap.js +39 -2
  64. package/src/lightning/utils/classSet.js +4 -1
  65. package/src/lightning/utilsPrivate/utilsPrivate.js +12 -1
  66. package/scopedImports/@salesforce-label-LightningModalBase.close.js +0 -1
  67. package/src/lightning/datatable/inlineEdit-shared.js +0 -14
  68. package/src/lightning/datatable/selector-shared.js +0 -38
  69. package/src/lightning/datatable/selector.js +0 -527
@@ -0,0 +1,760 @@
1
+ import {
2
+ resolveRowClassNames,
3
+ getRows,
4
+ getRowByKey,
5
+ getRowsTotal,
6
+ getRowIndexByKey,
7
+ rowKeyExists,
8
+ } from './rows';
9
+ import {
10
+ getColumns,
11
+ getStateColumnIndex,
12
+ SELECTABLE_ROW_CHECKBOX,
13
+ } from './columns';
14
+ import { isNonNegativeInteger } from './utils';
15
+ import {
16
+ getCurrentSelectionLength,
17
+ isSelectedRow,
18
+ isDisabledRow,
19
+ getRowSelectionInputType,
20
+ getMaxRowSelection,
21
+ getSelectedRowsKeys,
22
+ } from './rowSelectionShared';
23
+
24
+ export {
25
+ getCurrentSelectionLength,
26
+ isSelectedRow,
27
+ isDisabledRow,
28
+ getRowSelectionInputType,
29
+ getMaxRowSelection,
30
+ getSelectedRowsKeys,
31
+ } from './rowSelectionShared';
32
+
33
+ const MAX_ROW_SELECTION_DEFAULT = undefined;
34
+
35
+ /************************** EVENT HANDLERS **************************/
36
+
37
+ /**
38
+ * Marks all possible rows as selected depending on the max-row-selection value.
39
+ * Fires the `rowselection` event with the new set of selected rows.
40
+ *
41
+ * @param {CustomEvent} event - `selectallrows`
42
+ */
43
+ export function handleSelectAllRows(event) {
44
+ event.stopPropagation();
45
+ markAllRowsSelected(this.state);
46
+ this.fireSelectedRowsChange(this.getSelectedRows());
47
+ }
48
+
49
+ /**
50
+ * Marks all rows as de-selected. Does not need to account for max-row-selection.
51
+ * Fires the `rowselection` event with the new set of selected rows.
52
+ * @param {CustomEvent} event - `deselectallrows`
53
+ */
54
+ export function handleDeselectAllRows(event) {
55
+ event.stopPropagation();
56
+ markAllRowsDeselected(this.state);
57
+ this.fireSelectedRowsChange(this.getSelectedRows());
58
+ }
59
+
60
+ /**
61
+ * Handles selection of row(s)
62
+ * 1. Marks the relevant rows as selected
63
+ * Depends on whether a single row or multiple rows (interval) were selected
64
+ * 2. Records the last selected row's row key value in the state
65
+ * 3. Fires the `rowselection` event with the new selected rows in the details object
66
+ *
67
+ * @param {CustomEvent} event - `selectrow` event
68
+ */
69
+ export function handleSelectRow(event) {
70
+ event.stopPropagation();
71
+ const { rowKeyValue, isMultiple } = event.detail;
72
+ let fromRowKey = rowKeyValue;
73
+
74
+ if (isMultiple) {
75
+ fromRowKey = getLastRowSelection(this.state) || rowKeyValue;
76
+ }
77
+
78
+ markSelectedRowsInterval(this.state, fromRowKey, rowKeyValue);
79
+ setLastRowSelection(this.state, rowKeyValue);
80
+ this.fireSelectedRowsChange(this.getSelectedRows());
81
+ }
82
+
83
+ /**
84
+ * Handles the de-selection of row(s)
85
+ * 1. Marks the relevant rows as de-selected
86
+ * Depends on whether a single row or multiple rows (interval) was de-selected
87
+ * 2. Records the last selected row's row key value in the state
88
+ * 3. Fires the `rowselection` event with the new selected rows in the details object
89
+ *
90
+ * @param {CustomEvent} event - `deselectrow` event
91
+ */
92
+ export function handleDeselectRow(event) {
93
+ event.stopPropagation();
94
+ const { rowKeyValue, isMultiple } = event.detail;
95
+ let fromRowKey = rowKeyValue;
96
+
97
+ if (isMultiple) {
98
+ fromRowKey = getLastRowSelection(this.state) || rowKeyValue;
99
+ }
100
+
101
+ markDeselectedRowsInterval(this.state, fromRowKey, rowKeyValue);
102
+ setLastRowSelection(this.state, rowKeyValue);
103
+ this.fireSelectedRowsChange(this.getSelectedRows());
104
+ }
105
+
106
+ /**
107
+ * Handles the `rowselection` event
108
+ */
109
+ export function handleRowSelectionChange() {
110
+ updateBulkSelectionState(this.state);
111
+ }
112
+
113
+ /************************** ROW SELECTION **************************/
114
+
115
+ /**
116
+ * Marks all rows as selected.
117
+ * Retrieve all rows from the state object. Iterate over these rows; for each:
118
+ * 1. If the index is less than the specified max-row-selection value or
119
+ * if max-row-selection is not specified at all, set `isSelected` and
120
+ * `ariaSelected` to true and resolve `classnames` to reflect that the
121
+ * row is selected on each row object.
122
+ * 2. If max-row-selection has been reached, mark the remaining rows
123
+ * to reflect that they are not selected and disable them.
124
+ *
125
+ * @param {Object} state - datatable's state object
126
+ */
127
+ export function markAllRowsSelected(state) {
128
+ const rows = getRows(state);
129
+ const maxRowSelection = getMaxRowSelection(state);
130
+
131
+ resetSelectedRowsKeys(state);
132
+ rows.forEach((row, index) => {
133
+ if (index < maxRowSelection || maxRowSelection === undefined) {
134
+ setRowSelectedAttributes(true, row);
135
+ addKeyToSelectedRowKeys(state, row.key);
136
+ } else {
137
+ row.isDisabled = true;
138
+ setRowSelectedAttributes(false, row);
139
+ }
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Marks all rows as de-selected.
145
+ * Retrieve all rows from the state object. Iterate over these rows; for each row
146
+ * set `isSelected` and `ariaSelected` to false and enable the row. Also resolve
147
+ * the `classnames` for the row to reflect that it is not selected.
148
+ *
149
+ * @param {Object} state - datatable's state object
150
+ * @returns
151
+ */
152
+ export function markAllRowsDeselected(state) {
153
+ const rows = getRows(state);
154
+
155
+ resetSelectedRowsKeys(state);
156
+ rows.forEach((row) => {
157
+ row.isDisabled = false;
158
+ setRowSelectedAttributes(false, row);
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Marks rows as selected for all rows within the interval.
164
+ * An interval is created when the user had initially selected a cell,
165
+ * then does a Shift + click on a different row selection checkbox.
166
+ * By doing so, all the rows (and checkboxes) between those two rows
167
+ * will all be selected.
168
+ *
169
+ * Note that this function is also used not only for intervals but also
170
+ * for single row selections. In that case, the startRowKey and
171
+ * endRowKey will both have the same value.
172
+ *
173
+ * This does not handle the case when the header checkbox that selects
174
+ * all rows of the table is clicked. That is handled by - `handleSelectAllRows`
175
+ *
176
+ * @param {Object} state - datatable's state object
177
+ * @param {String} startRowKey - row key value of the first row that was selected (start of the interval)
178
+ * @param {String} endRowKey - row key value of the last row that was selected (end of the interval)
179
+ */
180
+ function markSelectedRowsInterval(state, startRowKey, endRowKey) {
181
+ const rows = getRows(state);
182
+ const { start, end } = getRowIntervalIndexes(state, startRowKey, endRowKey);
183
+ const maxRowSelection = getMaxRowSelection(state) || getRowsTotal(state);
184
+ let i = start,
185
+ maxSelectionReached;
186
+
187
+ while (i <= end && !maxSelectionReached) {
188
+ markRowSelected(state, rows[i].key);
189
+ maxSelectionReached =
190
+ getCurrentSelectionLength(state) >= maxRowSelection;
191
+ i++;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Marks rows as de-selected for all rows within the interval.
197
+ * An interval for de-selection is created when the user ended selection or
198
+ * de-selects a row and then does a Shift + click on a row that was previously selected.
199
+ * By doing so, all the rows (and checkboxes) between the two rows will be de-selected
200
+ *
201
+ * Note that this function is also used not only for intervals but also
202
+ * for single row de-selections. In that case, the startRowKey and
203
+ * endRowKey will both have the same value.
204
+ *
205
+ * This does not handle the case when the header checkbox is clicked to de-select all rows
206
+ * That is handledd by - `handleDeselectAllRows`
207
+ *
208
+ * @param {Object} state - datatable's state object
209
+ * @param {String} startRowKey - row key value of the first row that was selected (start of the interval)
210
+ * @param {String} endRowKey - row key value of the last row that was selected (end of the interval)
211
+ */
212
+ function markDeselectedRowsInterval(state, startRowKey, endRowKey) {
213
+ const rows = getRows(state);
214
+ const { start, end } = getRowIntervalIndexes(state, startRowKey, endRowKey);
215
+
216
+ for (let i = start; i <= end; i++) {
217
+ markRowDeselected(state, rows[i].key);
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Marks a row with the specified row key value as selected. This is done by:
223
+ * 1. Sets `isSelected`, `ariaSelected` to true and resolves `classnames`
224
+ * to that which reflect that the row is selected, on the row object.
225
+ * These are used by the template to render the appropriate values.
226
+ * 2. If max-row-selection > 1 (checkbox/multi-selection),
227
+ * a. If with this selection, the max-row-selection value is reached,
228
+ * disable all the other un-selected rows
229
+ * b. Add the row key value of that row to the state
230
+ * 3. If max-row-selection = 1 (radio button selector),
231
+ * a. If another row was previously selected before, de-select that row
232
+ * b. Add the row key value of that row to the state
233
+ *
234
+ * @param {Object} state - datatable's state object
235
+ * @param {String} rowKeyValue - row key value of row to mark selected
236
+ */
237
+ export function markRowSelected(state, rowKeyValue) {
238
+ const row = getRowByKey(state, rowKeyValue);
239
+ const maxRowSelection = getMaxRowSelection(state) || getRowsTotal(state);
240
+ const previousSelectionLength = getCurrentSelectionLength(state);
241
+
242
+ setRowSelectedAttributes(true, row);
243
+
244
+ if (maxRowSelection > 1) {
245
+ addKeyToSelectedRowKeys(state, row.key);
246
+ if (previousSelectionLength + 1 === maxRowSelection) {
247
+ markDeselectedRowDisabled(state);
248
+ }
249
+ } else {
250
+ if (previousSelectionLength === 1) {
251
+ const previousSelectedRow = getRowByKey(
252
+ state,
253
+ Object.keys(state.selectedRowsKeys)[0]
254
+ );
255
+ setRowSelectedAttributes(false, previousSelectedRow);
256
+ resetSelectedRowsKeys(state);
257
+ }
258
+ addKeyToSelectedRowKeys(state, row.key);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Marks a row with the specified row key value as de-selected. This is done by:
264
+ * 1. Sets `isSelected`, `ariaSelected` to false and resolves `classnames`
265
+ * to that which reflect that the row is not selected, on the row object.
266
+ * These are used by the template to render the appropriate values.
267
+ * 2. The row key value of this row is removed from list of selected row keys
268
+ * 3. Before the current de-selection, if the remaining unselected rows were
269
+ * disabled (max-row-selection was reached), enable all unselected rows
270
+ * so that they can be selected.
271
+ *
272
+ * @param {Object} state - datatable's state object
273
+ * @param {String} rowKeyValue - row key value of row to mark selected
274
+ */
275
+ export function markRowDeselected(state, rowKeyValue) {
276
+ const row = getRowByKey(state, rowKeyValue);
277
+ const maxRowSelection = getMaxRowSelection(state);
278
+
279
+ setRowSelectedAttributes(false, row);
280
+ removeKeyFromSelectedRowKeys(state, row.key);
281
+
282
+ if (getCurrentSelectionLength(state) === maxRowSelection - 1) {
283
+ markDeselectedRowEnabled(state);
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Iterates over the row key values passed in and sets the relevant
289
+ * values on the row object to reflect that it is selected.
290
+ * Sets `isSelected`, `ariaSelected` and `classnames` on the row object
291
+ * which are used by the template to render the appropriate values.
292
+ *
293
+ * @param {Object} state - datatable's state object
294
+ * @param {Array} keys - a list of row key values to be marked selected
295
+ */
296
+ function markRowsSelectedByKeys(state, keys) {
297
+ keys.forEach((rowKeyValue) => {
298
+ const row = getRowByKey(state, rowKeyValue);
299
+ setRowSelectedAttributes(true, row);
300
+ });
301
+ }
302
+
303
+ /**
304
+ * Iterates over the row key values passed in and un-sets the relevant
305
+ * values on the row object to reflect that it is not selected.
306
+ * Sets `isSelected`, `ariaSelected` to false and resolves `classnames`
307
+ * to one which reflects that the row is not selected on the row object.
308
+ * These are used by the template to render the appropriate values.
309
+ *
310
+ * @param {Object} state - datatable's state object
311
+ * @param {Array} keys - a list of row key values to be marked selected
312
+ */
313
+ function markRowsDeselectedByKeys(state, keys) {
314
+ keys.forEach((rowKeyValue) => {
315
+ const row = getRowByKey(state, rowKeyValue);
316
+ setRowSelectedAttributes(false, row);
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Marks all deselected rows as disabled. This prevents the user
322
+ * from selecting any more rows.
323
+ * This is typically called once the selection has reached the
324
+ * max-row-selection value and no more rows are allowed to be selected.
325
+ *
326
+ * @param {Object} state - datatable's state object
327
+ */
328
+ export function markDeselectedRowDisabled(state) {
329
+ const rows = getRows(state);
330
+ rows.forEach((row) => {
331
+ if (!isSelectedRow(state, row.key)) {
332
+ row.isDisabled = true;
333
+ }
334
+ });
335
+ }
336
+
337
+ /**
338
+ * Marks all the deselected rows as enabled. This allows the user
339
+ * to select more rows.
340
+ * This is typically called when the maximum number of rows were
341
+ * previously selected but a row was deselected, now allowing
342
+ * any other row to be selected - for this, all rows should be enabled
343
+ *
344
+ * @param {Object} state - datatable's state object
345
+ */
346
+ export function markDeselectedRowEnabled(state) {
347
+ const rows = getRows(state);
348
+ rows.forEach((row) => {
349
+ if (!isSelectedRow(state, row.key)) {
350
+ row.isDisabled = false;
351
+ }
352
+ });
353
+ }
354
+
355
+ /************************** SELECTED ROW KEYS **************************/
356
+
357
+ /**
358
+ * This is called when the `selected-rows` attribute is set on the datatable
359
+ * The 'value' parameter should be an Array of key-field values. If it is not an array,
360
+ * we throw an error stating so and all rows are de-selected.
361
+ *
362
+ * If the 'value' param is valid, we filter the 'value' array for only the valid keys.
363
+ * If the number of valid keys exceeds the max-row-selection value, we use only the
364
+ * number of keys from the valid list as that of the max-row-selection value.
365
+ *
366
+ * Compute the differences between the currently selected rows vs
367
+ * the newly selected rows to find out which rows need to be additionally
368
+ * selected and de-selected.
369
+ * Based on the above computation, mark rows as selected or de-selected and
370
+ * set the new `selectedRowsKeys` in the state object.
371
+ *
372
+ * If we select the max number of rows allowed and if max-row-selection > 1 (multi-select),
373
+ * disable all the other rows.
374
+ * If the previous selection had reached the max limit and the new selection
375
+ * is less than the limit, re-enable the de-selected rows to allow for new selection.
376
+ *
377
+ * @param {Object} state - datatable's state object
378
+ * @param {Array} value - key-field values of rows to set as selected
379
+ */
380
+ export function setSelectedRowsKeys(state, value) {
381
+ if (Array.isArray(value)) {
382
+ const maxRowSelection = getMaxRowSelection(state);
383
+ const previousSelectionLength = getCurrentSelectionLength(state);
384
+ let selectedRows = filterValidKeys(state, value);
385
+ if (selectedRows.length > maxRowSelection) {
386
+ // eslint-disable-next-line no-console
387
+ console.warn(`The number of keys in selectedRows for lightning:datatable
388
+ exceeds the limit defined by maxRowSelection.`);
389
+ selectedRows = selectedRows.slice(0, maxRowSelection);
390
+ }
391
+
392
+ // Convert the selectedRows Array to an Object that state.selectedRowKeys expects
393
+ // ['a', 'b'] -> { a : true, b : true}
394
+ const normalizedSelectedRowsKeys =
395
+ normalizeSelectedRowsKey(selectedRows);
396
+
397
+ // Compute differences between currently selected rows and
398
+ // newly selected row keys. The diff will tell which new rows
399
+ // need to be selected and which already selected rows need to
400
+ // be deselected
401
+ const selectionOperations = getSelectedDiff(state, selectedRows);
402
+ const deselectionOperations = getDeselectedDiff(
403
+ state,
404
+ normalizedSelectedRowsKeys
405
+ );
406
+ markRowsSelectedByKeys(state, selectionOperations);
407
+ markRowsDeselectedByKeys(state, deselectionOperations);
408
+ state.selectedRowsKeys = normalizedSelectedRowsKeys;
409
+
410
+ // If we select the max number of rows allowed and if max-row-selection > 1 (multi-select),
411
+ // disable all the other rows to prevent further selection
412
+ if (selectedRows.length === maxRowSelection && maxRowSelection > 1) {
413
+ markDeselectedRowDisabled(state);
414
+ } else if (
415
+ selectedRows.length < maxRowSelection &&
416
+ previousSelectionLength === maxRowSelection
417
+ ) {
418
+ // If the previous selection had reached the max limit and the new selection
419
+ // is less than the limit, re-enable the de-selected rows to allow for new selection.
420
+ markDeselectedRowEnabled(state);
421
+ }
422
+ } else {
423
+ // eslint-disable-next-line no-console
424
+ console.error(`The "selectedRows" passed into "lightning:datatable"
425
+ must be an Array with the keys of the selected rows. We receive instead ${value}`);
426
+ markAllRowsDeselected(state);
427
+ }
428
+ }
429
+
430
+ export function syncSelectedRowsKeys(state, selectedRows) {
431
+ let changed = false;
432
+ const { selectedRowsKeys, keyField } = state;
433
+
434
+ if (Object.keys(selectedRowsKeys).length !== selectedRows.length) {
435
+ changed = true;
436
+ state.selectedRowsKeys = updateSelectedRowsKeysFromSelectedRows(
437
+ selectedRows,
438
+ keyField
439
+ );
440
+ } else {
441
+ changed = selectedRows.some((row) => !selectedRowsKeys[row[keyField]]);
442
+ if (changed) {
443
+ state.selectedRowsKeys = updateSelectedRowsKeysFromSelectedRows(
444
+ selectedRows,
445
+ keyField
446
+ );
447
+ }
448
+ }
449
+
450
+ updateBulkSelectionState(state);
451
+
452
+ return {
453
+ ifChanged: (callback) => {
454
+ if (changed && typeof callback === 'function') {
455
+ callback(selectedRows);
456
+ }
457
+ },
458
+ };
459
+ }
460
+
461
+ function updateSelectedRowsKeysFromSelectedRows(selectedRows, keyField) {
462
+ return selectedRows.reduce((selectedRowsKeys, row) => {
463
+ selectedRowsKeys[row[keyField]] = true;
464
+ return selectedRowsKeys;
465
+ }, {});
466
+ }
467
+
468
+ function addKeyToSelectedRowKeys(state, key) {
469
+ state.selectedRowsKeys[key] = true;
470
+ }
471
+
472
+ function removeKeyFromSelectedRowKeys(state, key) {
473
+ // not using delete this.state.selectedRowsKeys[key]
474
+ // because that causes perf issues
475
+ state.selectedRowsKeys[key] = false;
476
+ }
477
+
478
+ function normalizeSelectedRowsKey(value) {
479
+ return value.reduce((map, key) => {
480
+ map[key] = true;
481
+ return map;
482
+ }, {});
483
+ }
484
+
485
+ function filterValidKeys(state, keys) {
486
+ return keys.filter((key) => rowKeyExists(state, key));
487
+ }
488
+
489
+ export function resetSelectedRowsKeys(state) {
490
+ state.selectedRowsKeys = {};
491
+ }
492
+
493
+ /************************** LAST SELECTED ROW KEYS **************************/
494
+
495
+ /**
496
+ * Returns the row key value of the row that was last selected
497
+ * Returns undefined if the row key value is invalid
498
+ *
499
+ * @param {Object} state - the datatable state.
500
+ * @return {String | undefined } the row key or undefined.
501
+ */
502
+ function getLastRowSelection(state) {
503
+ const lastSelectedRowKey = state.lastSelectedRowKey;
504
+ const keyIsValid =
505
+ lastSelectedRowKey !== undefined &&
506
+ getRowIndexByKey(state, lastSelectedRowKey) !== undefined;
507
+
508
+ return keyIsValid ? lastSelectedRowKey : undefined;
509
+ }
510
+
511
+ function setLastRowSelection(state, rowKeyValue) {
512
+ state.lastSelectedRowKey = rowKeyValue;
513
+ }
514
+
515
+ /************************** INPUT TYPES **************************/
516
+
517
+ /**
518
+ * Computes whether or not the input type rendered for row selection needs to change
519
+ * The input type may need to change if:
520
+ * 1. The max-row-selection value was previously 1 and is now either
521
+ * greater than 1 or is undefined OR
522
+ * 2. The max-row-selection value was previously greater than 1 or undefined
523
+ * and is now 1 OR
524
+ * 3. Previous max-row-selection value was 0 OR
525
+ * 4. New max-row-selection value is 0
526
+ *
527
+ * @param {Number} previousMaxRowSelection
528
+ * @param {Number} newMaxRowSelection
529
+ * @returns
530
+ */
531
+ export function inputTypeNeedsToChange(
532
+ previousMaxRowSelection,
533
+ newMaxRowSelection
534
+ ) {
535
+ return (
536
+ (previousMaxRowSelection === 1 &&
537
+ isMultiSelection(newMaxRowSelection)) ||
538
+ (isMultiSelection(previousMaxRowSelection) &&
539
+ newMaxRowSelection === 1) ||
540
+ previousMaxRowSelection === 0 ||
541
+ newMaxRowSelection === 0
542
+ );
543
+ }
544
+
545
+ export function updateRowSelectionInputType(state) {
546
+ const type = getRowSelectionInputType(state);
547
+ const rows = getRows(state);
548
+
549
+ rows.forEach((row) => {
550
+ row.inputType = type;
551
+ row.isDisabled = isDisabledRow(state, row.key);
552
+ });
553
+ }
554
+
555
+ /************************** MAX ROW SELECTION **************************/
556
+
557
+ /**
558
+ * Sets maxRowSelection to the provided value,
559
+ * only keeping up to maxRowSelection values selected.
560
+ *
561
+ * Use input type checkbox if maxRowSelection > 1
562
+ * and input type is radio if maxRowSelection = 1.
563
+ * Invalid values are set to default and an error is logged
564
+ *
565
+ * @param {Object} state - the datatable state.
566
+ * @param {Number | String} - value to set for maxRowSelection
567
+ */
568
+ export function setMaxRowSelection(state, value) {
569
+ const previousSelectedRowsKeys = getSelectedRowsKeys(state);
570
+ markAllRowsDeselected(state);
571
+ if (isNonNegativeInteger(value)) {
572
+ const previousMaxRowSelection = getMaxRowSelection(state);
573
+ state.maxRowSelection = Number(value);
574
+ const newMaxRowSelection = getMaxRowSelection(state);
575
+ // reselect up to maxRowSelection rows
576
+ const numberOfRows = Math.min(
577
+ previousSelectedRowsKeys.length,
578
+ newMaxRowSelection
579
+ );
580
+ for (let i = 0; i < numberOfRows; i++) {
581
+ markRowSelected(state, previousSelectedRowsKeys[i]);
582
+ }
583
+ if (
584
+ inputTypeNeedsToChange(
585
+ previousMaxRowSelection,
586
+ getMaxRowSelection(state)
587
+ )
588
+ ) {
589
+ updateRowSelectionInputType(state);
590
+ updateBulkSelectionState(state);
591
+ }
592
+ } else {
593
+ state.maxRowSelection = MAX_ROW_SELECTION_DEFAULT;
594
+ // eslint-disable-next-line no-console
595
+ console.error(
596
+ `The maxRowSelection value passed into lightning:datatable
597
+ should be a positive integer. We receive instead (${value}).`
598
+ );
599
+ }
600
+ }
601
+
602
+ /************************** BULK SELECTION STATE **************************/
603
+
604
+ export function updateBulkSelectionState(state) {
605
+ const selectBoxesColumnIndex = getSelectBoxesColumnIndex(state);
606
+ if (selectBoxesColumnIndex >= 0) {
607
+ state.columns[selectBoxesColumnIndex] = Object.assign(
608
+ {},
609
+ state.columns[selectBoxesColumnIndex],
610
+ {
611
+ bulkSelection: getBulkSelectionState(state),
612
+ isBulkSelectionDisabled: isBulkSelectionDisabled(state),
613
+ }
614
+ );
615
+ }
616
+ }
617
+
618
+ function getBulkSelectionState(state) {
619
+ const selected = getCurrentSelectionLength(state);
620
+ const total = getMaxRowSelection(state) || getRowsTotal(state);
621
+ if (selected === 0) {
622
+ return 'none';
623
+ } else if (selected === total) {
624
+ return 'all';
625
+ }
626
+ return 'some';
627
+ }
628
+
629
+ function isBulkSelectionDisabled(state) {
630
+ return getRowsTotal(state) === 0 || getMaxRowSelection(state) === 0;
631
+ }
632
+
633
+ /************************** HELPER FUNCTIONS **************************/
634
+
635
+ /**
636
+ * Gets the interval of row indexes based on the start and end row key values
637
+ * Retrieves the index of the row with startRowKey and the same with endRowKey
638
+ * Returns an object that contains the start index which is the lower index value of the two
639
+ * and the end index which is the higher value of the two.
640
+ *
641
+ * @param {Object} state - datatable's state object
642
+ * @param {String} startRowKey - row key value of the first row that was selected (start of the interval)
643
+ * @param {String} endRowKey - row key value of the last row that was selected (end of the interval)
644
+ * @returns {Object} - object with start index and end index
645
+ */
646
+ function getRowIntervalIndexes(state, startRowKey, endRowKey) {
647
+ const start =
648
+ startRowKey === 'HEADER' ? 0 : getRowIndexByKey(state, startRowKey);
649
+ const end = getRowIndexByKey(state, endRowKey);
650
+
651
+ return {
652
+ start: Math.min(start, end),
653
+ end: Math.max(start, end),
654
+ };
655
+ }
656
+
657
+ /**
658
+ * Sets aria-selected to 'true' on the cell identified by the rowKeyValue and colKeyValue.
659
+ * This will reflect on the td or th or corresponding element in the role-based table.
660
+ *
661
+ * Note: This change is volatile, and will be reset (lost) in the next index regeneration.
662
+ *
663
+ * @param {Object} state - the state of the datatable
664
+ * @param {String} rowKeyValue - the row key of the cell to mark selected
665
+ * @param {String} colKeyValue - the col key of the cell to mark selected
666
+ */
667
+ export function setAriaSelectedOnCell(state, rowKeyValue, colKeyValue) {
668
+ const row = getRowByKey(state, rowKeyValue);
669
+ const colIndex = getStateColumnIndex(state, colKeyValue);
670
+
671
+ if (row && colIndex) {
672
+ row.cells[colIndex].ariaSelected = 'true';
673
+ }
674
+ }
675
+
676
+ /**
677
+ * Sets aria-selected to 'false' on the cell identified by the rowKeyValue and colKeyValue.
678
+ * This aria-selected attribute will be removed from the cell (if it was previously added))
679
+ * on the td or th or the corresponding element in the role-based table.
680
+ *
681
+ * Note: This change is volatile, and will be reset (lost) in the next index regeneration.
682
+ *
683
+ * @param {Object} state - the state of the datatable
684
+ * @param {String} rowKeyValue - the row key of the cell to select
685
+ * @param {String} colKeyValue - the col key of the cell to select
686
+ */
687
+ export function unsetAriaSelectedOnCell(state, rowKeyValue, colKeyValue) {
688
+ const row = getRowByKey(state, rowKeyValue);
689
+ const colIndex = getStateColumnIndex(state, colKeyValue);
690
+
691
+ if (row && colIndex) {
692
+ row.cells[colIndex].ariaSelected = false;
693
+ }
694
+ }
695
+
696
+ /**
697
+ * Sets `isSelected`, `ariaSelected` to true | false and resolves `classnames`
698
+ * to one which reflects the selected value of the row on the row object.
699
+ * These are used by the template to render the appropriate values.
700
+ *
701
+ * @param {Boolean} selectedValue - is the row selected or not
702
+ * @param {Object} row - the row on which to set the selected attributes
703
+ */
704
+ function setRowSelectedAttributes(selectedValue, row) {
705
+ row.isSelected = selectedValue;
706
+ row.ariaSelected = selectedValue;
707
+ row.classnames = resolveRowClassNames(row);
708
+ }
709
+
710
+ function getSelectedDiff(state, value) {
711
+ const selectedRowsKeys = state.selectedRowsKeys;
712
+ return value.filter((key) => !selectedRowsKeys[key]);
713
+ }
714
+
715
+ function getDeselectedDiff(state, value) {
716
+ const currentSelectedRowsKeys = state.selectedRowsKeys;
717
+ return Object.keys(currentSelectedRowsKeys).filter(
718
+ (key) => currentSelectedRowsKeys[key] && !value[key]
719
+ );
720
+ }
721
+
722
+ function getSelectBoxesColumnIndex(state) {
723
+ const columns = getColumns(state) || [];
724
+ let selectBoxColumnIndex = -1;
725
+
726
+ columns.some((column, index) => {
727
+ if (column.type === SELECTABLE_ROW_CHECKBOX) {
728
+ selectBoxColumnIndex = index;
729
+ return true;
730
+ }
731
+
732
+ return false;
733
+ });
734
+
735
+ return selectBoxColumnIndex;
736
+ }
737
+
738
+ /**
739
+ * Represents whether the select all rows checkbox on the header
740
+ * should be visible or not.
741
+ * If max-row-selection = 1, then the select all rows checkbox
742
+ * should not be visible/rendered.
743
+ *
744
+ * @param {Object} state - datatable's state object
745
+ * @returns
746
+ */
747
+ export function getHideSelectAllCheckbox(state) {
748
+ return getMaxRowSelection(state) === 1;
749
+ }
750
+
751
+ /**
752
+ * Represents whether the datatable should enable multiselection or not
753
+ * depending on the max-row-selection value passed in
754
+ *
755
+ * @param {Number} value - max-row-selection value
756
+ * @returns
757
+ */
758
+ function isMultiSelection(value) {
759
+ return value > 1 || value === undefined;
760
+ }