cx 24.4.3 → 24.4.5

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.
@@ -1,141 +1,141 @@
1
- import { ArrayAdapter } from "./ArrayAdapter";
2
- import { ReadOnlyDataView } from "../../data/ReadOnlyDataView";
3
- import { Grouper } from "../../data/Grouper";
4
- import { isArray } from "../../util/isArray";
5
- import { isDefined } from "../../util/isDefined";
6
- import { getComparer } from "../../data/comparer";
7
- import { Culture } from "../Culture";
8
- import { isObject } from "../../util/isObject";
9
-
10
- export class GroupAdapter extends ArrayAdapter {
11
- init() {
12
- super.init();
13
-
14
- if (this.groupRecordsAlias) this.groupRecordsName = this.groupRecordsAlias;
15
-
16
- if (this.groupings) this.groupBy(this.groupings);
17
- }
18
-
19
- getRecords(context, instance, records, parentStore) {
20
- let result = super.getRecords(context, instance, records, parentStore);
21
-
22
- if (this.groupings) {
23
- let groupedResults = [];
24
- this.processLevel([], result, groupedResults, parentStore);
25
- result = groupedResults;
26
- }
27
-
28
- return result;
29
- }
30
-
31
- processLevel(keys, records, result, parentStore) {
32
- let level = keys.length;
33
- let inverseLevel = this.groupings.length - level;
34
-
35
- if (inverseLevel == 0) {
36
- for (let i = 0; i < records.length; i++) {
37
- records[i].store.setStore(parentStore);
38
- result.push(records[i]);
39
- }
40
- return;
41
- }
42
-
43
- let grouping = this.groupings[level];
44
- let { grouper } = grouping;
45
- grouper.reset();
46
- grouper.processAll(records);
47
- let results = grouper.getResults();
48
- if (grouping.comparer) results.sort(grouping.comparer);
49
-
50
- results.forEach((gr) => {
51
- keys.push(gr.key);
52
-
53
- let key = keys.map(serializeKey).join("|");
54
-
55
- let $group = {
56
- ...gr.key,
57
- ...gr.aggregates,
58
- $name: gr.name,
59
- $level: inverseLevel,
60
- $records: gr.records || [],
61
- $key: key,
62
- };
63
-
64
- let data = {
65
- [this.recordName]: gr.records.length > 0 ? gr.records[0].data : null,
66
- [this.groupName]: $group,
67
- };
68
-
69
- let groupStore = new ReadOnlyDataView({
70
- store: parentStore,
71
- data,
72
- immutable: this.immutable,
73
- });
74
-
75
- let group = {
76
- key,
77
- data,
78
- group: $group,
79
- grouping,
80
- store: groupStore,
81
- level: inverseLevel,
82
- };
83
-
84
- if (grouping.includeHeader !== false)
85
- result.push({
86
- ...group,
87
- type: "group-header",
88
- key: "header:" + group.key,
89
- });
90
-
91
- this.processLevel(keys, gr.records, result, groupStore);
92
-
93
- if (grouping.includeFooter !== false)
94
- result.push({
95
- ...group,
96
- type: "group-footer",
97
- key: "footer:" + group.key,
98
- });
99
-
100
- keys.pop();
101
- });
102
- }
103
-
104
- groupBy(groupings) {
105
- if (!groupings) this.groupings = null;
106
- else if (isArray(groupings)) {
107
- this.groupings = groupings;
108
- this.groupings.forEach((g) => {
109
- let groupSorters = [];
110
- let key = {};
111
- for (let name in g.key) {
112
- if (!g.key[name] || !isDefined(g.key[name].direction) || !isDefined(g.key[name].value))
113
- g.key[name] = { value: g.key[name], direction: "ASC" };
114
- key[name] = g.key[name].value;
115
- groupSorters.push({
116
- field: name,
117
- direction: g.key[name].direction,
118
- });
119
- }
120
- g.grouper = new Grouper(key, { ...this.aggregates, ...g.aggregates }, (r) => r.store.getData(), g.text);
121
- g.comparer = null;
122
- if (groupSorters.length > 0)
123
- g.comparer = getComparer(
124
- groupSorters,
125
- (x) => x.key,
126
- this.sortOptions ? Culture.getComparer(this.sortOptions) : null,
127
- );
128
- });
129
- } else throw new Error("Invalid grouping provided.");
130
- }
131
- }
132
-
133
- GroupAdapter.prototype.groupName = "$group";
134
-
135
- function serializeKey(data) {
136
- if (isObject(data))
137
- return Object.keys(data)
138
- .map((k) => serializeKey(data[k]))
139
- .join(":");
140
- return data?.toString() ?? "";
141
- }
1
+ import { ArrayAdapter } from "./ArrayAdapter";
2
+ import { ReadOnlyDataView } from "../../data/ReadOnlyDataView";
3
+ import { Grouper } from "../../data/Grouper";
4
+ import { isArray } from "../../util/isArray";
5
+ import { isDefined } from "../../util/isDefined";
6
+ import { getComparer } from "../../data/comparer";
7
+ import { Culture } from "../Culture";
8
+ import { isObject } from "../../util/isObject";
9
+
10
+ export class GroupAdapter extends ArrayAdapter {
11
+ init() {
12
+ super.init();
13
+
14
+ if (this.groupRecordsAlias) this.groupRecordsName = this.groupRecordsAlias;
15
+
16
+ if (this.groupings) this.groupBy(this.groupings);
17
+ }
18
+
19
+ getRecords(context, instance, records, parentStore) {
20
+ let result = super.getRecords(context, instance, records, parentStore);
21
+
22
+ if (this.groupings) {
23
+ let groupedResults = [];
24
+ this.processLevel([], result, groupedResults, parentStore);
25
+ result = groupedResults;
26
+ }
27
+
28
+ return result;
29
+ }
30
+
31
+ processLevel(keys, records, result, parentStore) {
32
+ let level = keys.length;
33
+ let inverseLevel = this.groupings.length - level;
34
+
35
+ if (inverseLevel == 0) {
36
+ for (let i = 0; i < records.length; i++) {
37
+ records[i].store.setStore(parentStore);
38
+ result.push(records[i]);
39
+ }
40
+ return;
41
+ }
42
+
43
+ let grouping = this.groupings[level];
44
+ let { grouper } = grouping;
45
+ grouper.reset();
46
+ grouper.processAll(records);
47
+ let results = grouper.getResults();
48
+ if (grouping.comparer) results.sort(grouping.comparer);
49
+
50
+ results.forEach((gr) => {
51
+ keys.push(gr.key);
52
+
53
+ let key = keys.map(serializeKey).join("|");
54
+
55
+ let $group = {
56
+ ...gr.key,
57
+ ...gr.aggregates,
58
+ $name: gr.name,
59
+ $level: inverseLevel,
60
+ $records: gr.records || [],
61
+ $key: key,
62
+ };
63
+
64
+ let data = {
65
+ [this.recordName]: gr.records.length > 0 ? gr.records[0].data : null,
66
+ [this.groupName]: $group,
67
+ };
68
+
69
+ let groupStore = new ReadOnlyDataView({
70
+ store: parentStore,
71
+ data,
72
+ immutable: this.immutable,
73
+ });
74
+
75
+ let group = {
76
+ key,
77
+ data,
78
+ group: $group,
79
+ grouping,
80
+ store: groupStore,
81
+ level: inverseLevel,
82
+ };
83
+
84
+ if (grouping.includeHeader !== false)
85
+ result.push({
86
+ ...group,
87
+ type: "group-header",
88
+ key: "header:" + group.key,
89
+ });
90
+
91
+ this.processLevel(keys, gr.records, result, groupStore);
92
+
93
+ if (grouping.includeFooter !== false)
94
+ result.push({
95
+ ...group,
96
+ type: "group-footer",
97
+ key: "footer:" + group.key,
98
+ });
99
+
100
+ keys.pop();
101
+ });
102
+ }
103
+
104
+ groupBy(groupings) {
105
+ if (!groupings) this.groupings = null;
106
+ else if (isArray(groupings)) {
107
+ this.groupings = groupings;
108
+ this.groupings.forEach((g) => {
109
+ let groupSorters = [];
110
+ let key = {};
111
+ for (let name in g.key) {
112
+ if (!g.key[name] || !isDefined(g.key[name].direction) || !isDefined(g.key[name].value))
113
+ g.key[name] = { value: g.key[name], direction: "ASC" };
114
+ key[name] = g.key[name].value;
115
+ groupSorters.push({
116
+ field: name,
117
+ direction: g.key[name].direction,
118
+ });
119
+ }
120
+ g.grouper = new Grouper(key, { ...this.aggregates, ...g.aggregates }, (r) => r.store.getData(), g.text);
121
+ g.comparer = null;
122
+ if (groupSorters.length > 0)
123
+ g.comparer = getComparer(
124
+ groupSorters,
125
+ (x) => x.key,
126
+ this.sortOptions ? Culture.getComparer(this.sortOptions) : null,
127
+ );
128
+ });
129
+ } else throw new Error("Invalid grouping provided.");
130
+ }
131
+ }
132
+
133
+ GroupAdapter.prototype.groupName = "$group";
134
+
135
+ function serializeKey(data) {
136
+ if (isObject(data))
137
+ return Object.keys(data)
138
+ .map((k) => serializeKey(data[k]))
139
+ .join(":");
140
+ return data?.toString() ?? "";
141
+ }
@@ -124,6 +124,13 @@ let formatFactory = {
124
124
  };
125
125
  }
126
126
  },
127
+
128
+ zeroPad: function (part0, length) {
129
+ return (value) => {
130
+ let s = String(value);
131
+ return s.padStart(length, "0");
132
+ };
133
+ },
127
134
  };
128
135
 
129
136
  formatFactory.s = formatFactory.str = formatFactory.string;
@@ -134,6 +141,7 @@ formatFactory.ps = formatFactory.percentageSign;
134
141
  formatFactory.d = formatFactory.date;
135
142
  formatFactory.t = formatFactory.time;
136
143
  formatFactory.dt = formatFactory.datetime;
144
+ formatFactory.zeropad = formatFactory.zeroPad;
137
145
 
138
146
  function buildFormatter(format) {
139
147
  let formatter = defaultFormatter,
@@ -139,6 +139,8 @@ interface GridColumnConfig {
139
139
  /** Options for data sorting. See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator */
140
140
  sortOptions?: CollatorOptions;
141
141
  colSpan?: NumberProp;
142
+
143
+ mergeCells?: Prop<null | false | "same-value" | "always">;
142
144
  }
143
145
 
144
146
  interface GridRowLineConfig {
@@ -321,7 +323,7 @@ interface GridProps<T = unknown> extends StyledContainerProps {
321
323
  sortField?: string;
322
324
  sortDirection?: string;
323
325
  },
324
- instance?: Instance
326
+ instance?: Instance,
325
327
  ) => FetchRecordsResult | Promise<FetchRecordsResult>;
326
328
 
327
329
  /** Callback function to be executed when a row is double-clicked. */
@@ -354,7 +356,7 @@ interface GridProps<T = unknown> extends StyledContainerProps {
354
356
  /** Callback to create a function that can be used to check whether a record is selectable. */
355
357
  onCreateIsRecordSelectable?: (
356
358
  params: any,
357
- instance: Instance
359
+ instance: Instance,
358
360
  ) => (record: T, options?: { range?: boolean; toggle?: boolean }) => boolean;
359
361
 
360
362
  /** Parameters whose change will cause scroll to be reset. */
@@ -128,6 +128,9 @@ export class Grid extends Container {
128
128
 
129
129
  row.hasSortableColumns = false;
130
130
  row.hasResizableColumns = false;
131
+ row.hasMergedCells = false;
132
+ row.mergedColumns = [];
133
+
131
134
  let aggregates = {};
132
135
  let showFooter = false;
133
136
  let lines = [];
@@ -146,9 +149,18 @@ export class Grid extends Container {
146
149
  });
147
150
 
148
151
  row.header.items.forEach((line) => {
149
- line.items.forEach((c) => {
152
+ line.items.forEach((c, index) => {
150
153
  if (c.sortable) row.hasSortableColumns = true;
151
154
 
155
+ if (c.mergeCells) {
156
+ if (row.header.items.length > 1)
157
+ Console.warn("Merged columns are only supported in grids in which rows have only one line of cells.");
158
+ else {
159
+ row.hasMergedCells = true;
160
+ row.mergedColumns.push({ uniqueColumnId: c.uniqueColumnId, index, mode: c.mergeCells });
161
+ }
162
+ }
163
+
152
164
  if (
153
165
  c.resizable ||
154
166
  (c.header && c.header.resizable) ||
@@ -609,15 +621,16 @@ export class Grid extends Container {
609
621
  if (!header) return null;
610
622
 
611
623
  let skip = {};
624
+ let lineIndex = 0;
612
625
 
613
- header.children.forEach((line, lineIndex) => {
626
+ header.children.forEach((line) => {
614
627
  let empty = [true, true, true];
615
628
  let result = [[], [], []];
616
629
 
617
630
  line.children.forEach((hdinst, colIndex) => {
618
631
  let hdwidget = hdinst.widget;
619
632
  for (let l = 0; l < 3; l++) {
620
- let colKey = `${lineIndex}-${colIndex}-${l}`;
633
+ let colKey = `${lineIndex + l}-${colIndex}`;
621
634
 
622
635
  if (skip[colKey]) continue;
623
636
 
@@ -690,7 +703,7 @@ export class Grid extends Container {
690
703
 
691
704
  for (let r = 0; r < header.data.rowSpan; r++)
692
705
  for (let c = 0; c < header.data.colSpan; c++)
693
- skip[`${lineIndex}-${colIndex + c}-${l + r}`] = true;
706
+ skip[`${lineIndex + l + r}-${colIndex + c}`] = true;
694
707
  }
695
708
 
696
709
  if ((hdwidget.resizable || header.data.resizable) && header.data.colSpan < 2) {
@@ -745,6 +758,7 @@ export class Grid extends Container {
745
758
  });
746
759
 
747
760
  result = result.filter((_, i) => !empty[i]);
761
+ lineIndex += result.length;
748
762
 
749
763
  if (result[0]) {
750
764
  if (fixed && !fixedColumns) {
@@ -1054,9 +1068,7 @@ export class Grid extends Container {
1054
1068
 
1055
1069
  for (let i = 0; i < records.length; i++) {
1056
1070
  record = records[i];
1057
- if (record.type == "data") {
1058
- record.vdom = record.row.render(context, record.key);
1059
- }
1071
+ if (record.type == "data") record.vdom = record.row.render(context, record.key);
1060
1072
 
1061
1073
  if (record.type == "group-header") {
1062
1074
  record.vdom = [];
@@ -1311,11 +1323,12 @@ class GridComponent extends VDOM.Component {
1311
1323
 
1312
1324
  createRowRenderer(cellWrap) {
1313
1325
  let { instance, data } = this.props;
1314
- let { widget, isRecordSelectable, visibleColumns, isRecordDraggable } = instance;
1326
+ let { widget, isRecordSelectable, visibleColumns, isRecordDraggable, row } = instance;
1315
1327
  let { CSS, baseClass } = widget;
1316
1328
  let { dragSource } = data;
1317
1329
  let { dragged, cursor, cursorCellIndex, cellEdit, dropInsertionIndex, dropTarget } = this.state;
1318
1330
  let { colWidth, dimensionsVersion } = instance.state;
1331
+ let { hasMergedCells } = row;
1319
1332
 
1320
1333
  return (record, index, standalone, fixed) => {
1321
1334
  let { store, key, row } = record;
@@ -1341,49 +1354,58 @@ class GridComponent extends VDOM.Component {
1341
1354
  mod["draggable"] = draggable;
1342
1355
  mod["non-draggable"] = !draggable;
1343
1356
 
1344
- let wrap = (children) => (
1345
- <GridRowComponent
1346
- key={key}
1347
- className={CSS.state(mod)}
1348
- store={store}
1349
- dragSource={dragSource}
1350
- instance={row}
1351
- grid={instance}
1352
- record={record}
1353
- parent={this}
1354
- cursorIndex={index}
1355
- selected={row.selected}
1356
- isBeingDragged={dragged}
1357
- isDraggedOver={mod.over}
1358
- cursor={mod.cursor}
1359
- cursorCellIndex={index == cursor && cursorCellIndex}
1360
- cellEdit={index == cursor && cursorCellIndex != null && cellEdit}
1361
- shouldUpdate={row.shouldUpdate}
1362
- dimensionsVersion={dimensionsVersion}
1363
- fixed={fixed}
1364
- >
1365
- {children.content.map(({ key, data, content }, line) => (
1366
- <tr key={key} className={data.classNames} style={data.style}>
1367
- {content.map(({ key, data, content, uniqueColumnId }, cellIndex) => {
1368
- if (Boolean(data.fixed) !== fixed) return null;
1369
- let cellected =
1370
- index == cursor && cellIndex == cursorCellIndex && widget.cellEditable && line == 0;
1371
- let className = cellected
1372
- ? CSS.expand(data.classNames, CSS.state("cellected"))
1373
- : data.classNames;
1374
- if (cellected && cellEdit) {
1375
- let column = visibleColumns[cursorCellIndex];
1376
- if (column && column.editor && data.editable)
1377
- return this.renderCellEditor(key, CSS, baseClass, row, column);
1378
- }
1379
- let width = colWidth[uniqueColumnId];
1380
- let style = data.style;
1381
- if (width) {
1382
- style = {
1383
- ...style,
1384
- maxWidth: `${width}px`,
1385
- };
1386
- }
1357
+ let wrap = (children) => {
1358
+ let skipCells = {};
1359
+ return (
1360
+ <GridRowComponent
1361
+ key={key}
1362
+ className={CSS.state(mod)}
1363
+ store={store}
1364
+ dragSource={dragSource}
1365
+ instance={row}
1366
+ grid={instance}
1367
+ record={record}
1368
+ parent={this}
1369
+ cursorIndex={index}
1370
+ selected={row.selected}
1371
+ isBeingDragged={dragged}
1372
+ isDraggedOver={mod.over}
1373
+ cursor={mod.cursor}
1374
+ cursorCellIndex={index == cursor && cursorCellIndex}
1375
+ cellEdit={index == cursor && cursorCellIndex != null && cellEdit}
1376
+ shouldUpdate={row.shouldUpdate}
1377
+ dimensionsVersion={dimensionsVersion}
1378
+ fixed={fixed}
1379
+ useTrTag={hasMergedCells}
1380
+ >
1381
+ {children.content.map(({ key, data, content }, line) => {
1382
+ var cells = content.map(({ key, data, content, uniqueColumnId, merged, mergeRowSpan }, cellIndex) => {
1383
+ if (Boolean(data.fixed) !== fixed) return null;
1384
+ if (merged) return null;
1385
+ let cellected =
1386
+ index == cursor && cellIndex == cursorCellIndex && widget.cellEditable && line == 0;
1387
+ let className = cellected ? CSS.expand(data.classNames, CSS.state("cellected")) : data.classNames;
1388
+ if (cellected && cellEdit) {
1389
+ let column = visibleColumns[cursorCellIndex];
1390
+ if (column && column.editor && data.editable)
1391
+ return this.renderCellEditor(key, CSS, baseClass, row, column);
1392
+ }
1393
+ let width = colWidth[uniqueColumnId];
1394
+ let style = data.style;
1395
+ if (width) {
1396
+ style = {
1397
+ ...style,
1398
+ maxWidth: `${width}px`,
1399
+ };
1400
+ }
1401
+
1402
+ if (skipCells[`${line}-${cellIndex}`]) return null;
1403
+
1404
+ if (data.colSpan > 1 || data.rowSpan > 1) {
1405
+ for (let r = line; r < line + (data.rowSpan ?? 1); r++)
1406
+ for (let c = cellIndex; c < cellIndex + (data.colSpan ?? 1); c++)
1407
+ skipCells[`${r}-${c}`] = true;
1408
+ }
1387
1409
 
1388
1410
  if (cellWrap) content = cellWrap(content);
1389
1411
 
@@ -1393,16 +1415,22 @@ class GridComponent extends VDOM.Component {
1393
1415
  className={className}
1394
1416
  style={style}
1395
1417
  colSpan={data.colSpan}
1396
- rowSpan={data.rowSpan}
1418
+ rowSpan={mergeRowSpan ?? data.rowSpan}
1397
1419
  >
1398
1420
  {content}
1399
1421
  </td>
1400
1422
  );
1401
- })}
1402
- </tr>
1403
- ))}
1404
- </GridRowComponent>
1405
- );
1423
+ });
1424
+ if (hasMergedCells) return cells;
1425
+ return (
1426
+ <tr key={key} className={data.classNames} style={data.style}>
1427
+ {cells}
1428
+ </tr>
1429
+ );
1430
+ })}
1431
+ </GridRowComponent>
1432
+ );
1433
+ };
1406
1434
 
1407
1435
  if (!standalone) return wrap(record.vdom);
1408
1436
 
@@ -1570,10 +1598,53 @@ class GridComponent extends VDOM.Component {
1570
1598
  });
1571
1599
  instance.recordInstanceCache.sweep();
1572
1600
  } else {
1601
+ let { row } = instance;
1602
+ let { hasMergedCells, mergedColumns } = row;
1603
+ if (hasMergedCells) {
1604
+ // merge adjacent cells with the same value in columns that are marked as merged
1605
+ let rowSpan = {};
1606
+ let getCellRenderInfo = (vdom, cellIndex) => vdom.content[0]?.content[cellIndex];
1607
+ for (let index = instance.records.length - 1; index >= 0; index--) {
1608
+ let row = instance.records[index];
1609
+ let prevRow = instance.records[index - 1];
1610
+ if (row.type == "data") {
1611
+ let stopMerge = false;
1612
+ for (let mi = 0; mi < mergedColumns.length; mi++) {
1613
+ let mergedCol = mergedColumns[mi];
1614
+ let cellInfo = getCellRenderInfo(row.vdom, mergedCol.index);
1615
+ cellInfo.merged = false;
1616
+ delete cellInfo.mergeRowSpan;
1617
+ if (prevRow?.type == "data") {
1618
+ let shouldMerge = false;
1619
+ switch (mergedCol.mode) {
1620
+ case "always":
1621
+ shouldMerge = true;
1622
+ break;
1623
+ case "same-value":
1624
+ let prevCellInfo = getCellRenderInfo(prevRow.vdom, mergedCol.index);
1625
+ shouldMerge = !stopMerge && cellInfo.data.value === prevCellInfo.data.value;
1626
+ break;
1627
+ }
1628
+
1629
+ if (shouldMerge) {
1630
+ cellInfo.merged = true;
1631
+ rowSpan[mergedCol.uniqueColumnId] = (rowSpan[mergedCol.uniqueColumnId] ?? 1) + 1;
1632
+ } else {
1633
+ if (mergedCol.mode == "same-value") stopMerge = true;
1634
+ }
1635
+ }
1636
+ if (!cellInfo.merged && rowSpan[mergedCol.uniqueColumnId] > 1) {
1637
+ cellInfo.mergeRowSpan = rowSpan[mergedCol.uniqueColumnId];
1638
+ rowSpan[mergedCol.uniqueColumnId] = 1;
1639
+ }
1640
+ }
1641
+ } else rowSpan = {};
1642
+ }
1643
+ }
1644
+
1573
1645
  instance.records.forEach((record, i) => {
1574
- if (record.type == "data") {
1575
- addRow(record, i);
1576
- } else {
1646
+ if (record.type == "data") addRow(record, i, false);
1647
+ else {
1577
1648
  children.push(record.vdom);
1578
1649
  if (hasFixedColumns) fixedChildren.push(record.fixedVdom);
1579
1650
  }