@stackoverflow/stacks 1.9.0 → 1.9.1

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 (132) hide show
  1. package/README.md +161 -153
  2. package/dist/components/table/table.d.ts +26 -4
  3. package/dist/css/stacks.css +21 -6
  4. package/dist/css/stacks.min.css +1 -1
  5. package/dist/js/stacks.js +93 -64
  6. package/dist/js/stacks.min.js +1 -1
  7. package/lib/atomic/border.less +397 -397
  8. package/lib/atomic/color.less +210 -210
  9. package/lib/atomic/flex.less +426 -426
  10. package/lib/atomic/gap.less +44 -44
  11. package/lib/atomic/grid.less +139 -139
  12. package/lib/atomic/misc.less +343 -343
  13. package/lib/atomic/spacing.less +342 -342
  14. package/lib/atomic/typography.less +267 -267
  15. package/lib/atomic/width-height.less +194 -194
  16. package/lib/base/body.less +44 -44
  17. package/lib/base/configuration-static.less +61 -61
  18. package/lib/base/fieldset.less +5 -5
  19. package/lib/base/icon.less +11 -11
  20. package/lib/base/internal.less +220 -220
  21. package/lib/base/reset-meyer.less +64 -64
  22. package/lib/base/reset-normalize.less +449 -449
  23. package/lib/base/reset.less +20 -20
  24. package/lib/components/activity-indicator/activity-indicator.a11y.test.ts +21 -21
  25. package/lib/components/activity-indicator/activity-indicator.less +40 -40
  26. package/lib/components/activity-indicator/activity-indicator.visual.test.ts +23 -23
  27. package/lib/components/anchor/anchor.less +61 -61
  28. package/lib/components/avatar/avatar.a11y.test.ts +36 -36
  29. package/lib/components/avatar/avatar.less +108 -108
  30. package/lib/components/avatar/avatar.visual.test.ts +54 -54
  31. package/lib/components/award-bling/award-bling.a11y.test.ts +17 -0
  32. package/lib/components/award-bling/award-bling.less +31 -31
  33. package/lib/components/award-bling/award-bling.visual.test.ts +26 -0
  34. package/lib/components/badge/badge.less +251 -251
  35. package/lib/components/banner/banner.a11y.test.ts +37 -0
  36. package/lib/components/banner/banner.less +51 -51
  37. package/lib/components/banner/banner.test.ts +73 -77
  38. package/lib/components/banner/banner.ts +149 -149
  39. package/lib/components/banner/banner.visual.test.ts +37 -36
  40. package/lib/components/block-link/block-link.a11y.test.ts +68 -0
  41. package/lib/components/block-link/block-link.less +80 -80
  42. package/lib/components/block-link/block-link.visual.test.ts +61 -0
  43. package/lib/components/breadcrumbs/breadcrumbs.a11y.test.ts +37 -0
  44. package/lib/components/breadcrumbs/breadcrumbs.less +41 -41
  45. package/lib/components/breadcrumbs/breadcrumbs.visual.test.ts +37 -0
  46. package/lib/components/button/button.a11y.test.ts +32 -32
  47. package/lib/components/button/button.less +502 -502
  48. package/lib/components/button/button.visual.test.ts +52 -52
  49. package/lib/components/button-group/button-group.less +83 -83
  50. package/lib/components/card/card.a11y.test.ts +13 -0
  51. package/lib/components/card/card.less +29 -29
  52. package/lib/components/card/card.visual.test.ts +54 -0
  53. package/lib/components/check-control/check-control.less +17 -17
  54. package/lib/components/check-group/check-group.less +19 -19
  55. package/lib/components/checkbox_radio/checkbox_radio.less +158 -158
  56. package/lib/components/code-block/code-block.less +116 -116
  57. package/lib/components/description/description.less +9 -9
  58. package/lib/components/empty-state/empty-state.less +16 -16
  59. package/lib/components/expandable/expandable.less +118 -118
  60. package/lib/components/expandable/expandable.test.ts +51 -53
  61. package/lib/components/expandable/expandable.ts +238 -238
  62. package/lib/components/input-fill/input-fill.less +35 -35
  63. package/lib/components/input-icon/input-icon.less +45 -45
  64. package/lib/components/input-message/input-message.less +48 -48
  65. package/lib/components/input_textarea/input_textarea.less +166 -166
  66. package/lib/components/label/label.less +111 -111
  67. package/lib/components/link/link.less +119 -119
  68. package/lib/components/link-preview/link-preview.less +139 -139
  69. package/lib/components/menu/menu.less +41 -41
  70. package/lib/components/modal/modal.less +113 -113
  71. package/lib/components/modal/modal.ts +379 -379
  72. package/lib/components/navigation/navigation.less +134 -134
  73. package/lib/components/navigation/navigation.ts +128 -128
  74. package/lib/components/notice/notice.less +203 -203
  75. package/lib/components/page-title/page-title.less +51 -51
  76. package/lib/components/pagination/pagination.less +52 -52
  77. package/lib/components/popover/popover.less +148 -148
  78. package/lib/components/popover/popover.ts +651 -651
  79. package/lib/components/popover/tooltip.test.ts +62 -66
  80. package/lib/components/popover/tooltip.ts +343 -343
  81. package/lib/components/popover/tooltip.visual.test.ts +31 -31
  82. package/lib/components/post-summary/post-summary.less +415 -415
  83. package/lib/components/progress-bar/progress-bar.less +291 -291
  84. package/lib/components/prose/prose.less +452 -452
  85. package/lib/components/select/select.less +148 -148
  86. package/lib/components/sidebar-widget/sidebar-widget.less +257 -258
  87. package/lib/components/spinner/spinner.less +103 -103
  88. package/lib/components/table/table.less +307 -292
  89. package/lib/components/table/table.test.ts +366 -0
  90. package/lib/components/table/table.ts +296 -263
  91. package/lib/components/table-container/table-container.less +4 -4
  92. package/lib/components/tag/tag.less +213 -213
  93. package/lib/components/toast/toast.less +35 -35
  94. package/lib/components/toast/toast.test.ts +63 -67
  95. package/lib/components/toast/toast.ts +357 -357
  96. package/lib/components/toast/toast.visual.test.ts +27 -27
  97. package/lib/components/toggle-switch/toggle-switch.less +110 -110
  98. package/lib/components/topbar/topbar.less +436 -435
  99. package/lib/components/uploader/uploader.less +195 -195
  100. package/lib/components/uploader/uploader.ts +205 -205
  101. package/lib/components/user-card/user-card.less +129 -129
  102. package/lib/controllers.ts +33 -33
  103. package/lib/exports/constants-colors.less +1112 -1111
  104. package/lib/exports/constants-helpers.less +108 -108
  105. package/lib/exports/constants-type.less +153 -153
  106. package/lib/exports/exports.less +15 -15
  107. package/lib/exports/mixins.less +299 -299
  108. package/lib/index.ts +32 -32
  109. package/lib/input-utils.less +44 -44
  110. package/lib/stacks-dynamic.less +24 -24
  111. package/lib/stacks-static.less +93 -93
  112. package/lib/stacks.less +13 -13
  113. package/lib/stacks.ts +113 -113
  114. package/lib/test/open-wc-testing-patch.d.ts +26 -0
  115. package/lib/test/test-utils.ts +466 -444
  116. package/lib/tsconfig.build.json +4 -0
  117. package/lib/tsconfig.json +16 -13
  118. package/package.json +106 -105
  119. package/dist/components/activity-indicator/activity-indicator.a11y.test.d.ts +0 -1
  120. package/dist/components/activity-indicator/activity-indicator.visual.test.d.ts +0 -1
  121. package/dist/components/avatar/avatar.a11y.test.d.ts +0 -1
  122. package/dist/components/avatar/avatar.visual.test.d.ts +0 -1
  123. package/dist/components/banner/banner.test.d.ts +0 -1
  124. package/dist/components/banner/banner.visual.test.d.ts +0 -1
  125. package/dist/components/button/button.a11y.test.d.ts +0 -1
  126. package/dist/components/button/button.visual.test.d.ts +0 -1
  127. package/dist/components/expandable/expandable.test.d.ts +0 -1
  128. package/dist/components/popover/tooltip.test.d.ts +0 -1
  129. package/dist/components/popover/tooltip.visual.test.d.ts +0 -1
  130. package/dist/components/toast/toast.test.d.ts +0 -1
  131. package/dist/components/toast/toast.visual.test.d.ts +0 -1
  132. package/dist/test/test-utils.d.ts +0 -136
@@ -1,263 +1,296 @@
1
- import * as Stacks from "../../stacks";
2
-
3
- export class TableController extends Stacks.StacksController {
4
- static targets = ["column"];
5
- readonly columnTarget!: Element;
6
- readonly columnTargets!: Element[];
7
-
8
- setCurrentSort(headElem: Element, direction: "asc" | "desc" | "none") {
9
- if (["asc", "desc", "none"].indexOf(direction) < 0) {
10
- throw "direction must be one of asc, desc, or none";
11
- }
12
- // eslint-disable-next-line @typescript-eslint/no-this-alias
13
- const controller = this;
14
- this.columnTargets.forEach(function (target) {
15
- const isCurrrent = target === headElem;
16
-
17
- target.classList.toggle(
18
- "is-sorted",
19
- isCurrrent && direction !== "none"
20
- );
21
-
22
- target
23
- .querySelectorAll(".js-sorting-indicator")
24
- .forEach(function (icon) {
25
- const visible = isCurrrent ? direction : "none";
26
- icon.classList.toggle(
27
- "d-none",
28
- !icon.classList.contains(
29
- "js-sorting-indicator-" + visible
30
- )
31
- );
32
- });
33
-
34
- if (!isCurrrent || direction === "none") {
35
- controller.removeElementData(target, "sort-direction");
36
- } else {
37
- controller.setElementData(target, "sort-direction", direction);
38
- }
39
- });
40
- }
41
-
42
- sort(evt: Event) {
43
- // eslint-disable-next-line @typescript-eslint/no-this-alias
44
- const controller = this;
45
- const colHead = evt.currentTarget;
46
- if (!(colHead instanceof HTMLTableCellElement)) {
47
- throw "invalid event target";
48
- }
49
- const table = this.element as HTMLTableElement;
50
- const tbody = table.tBodies[0];
51
-
52
- // the column slot number of the clicked header
53
- const colno = getCellSlot(colHead);
54
-
55
- if (colno < 0) {
56
- // this shouldn't happen if the clicked element is actually a column head
57
- return;
58
- }
59
-
60
- // an index of the <tbody>, so we can find out for each row which <td> element is
61
- // in the same column slot as the header
62
- const slotIndex = buildIndex(tbody);
63
-
64
- // the default behavior when clicking a header is to sort by this column in ascending
65
- // direction, *unless* it is already sorted that way
66
- const direction =
67
- this.getElementData(colHead, "sort-direction") === "asc" ? -1 : 1;
68
-
69
- const rows = Array.from(table.tBodies[0].rows);
70
-
71
- // if this is still false after traversing the data, that means all values are integers (or empty)
72
- // and thus we'll sort numerically.
73
- let anyNonInt = false;
74
-
75
- // data will be a list of tuples [value, rowNum], where value is what we're sorting by
76
- const data: [string | number, number][] = [];
77
- let firstBottomRow: HTMLTableRowElement;
78
- rows.forEach(function (row, index) {
79
- const force = controller.getElementData(row, "sort-to");
80
- if (force === "top") {
81
- return; // rows not added to the list will automatically end up at the top
82
- } else if (force === "bottom") {
83
- if (!firstBottomRow) {
84
- firstBottomRow = row;
85
- }
86
- return;
87
- }
88
- const cell = slotIndex[index][colno];
89
- if (!cell) {
90
- data.push(["", index]);
91
- return;
92
- }
93
-
94
- // unless the to-be-sorted-by value is explicitly provided on the element via this attribute,
95
- // the value we're using is the cell's text, trimmed of any whitespace
96
- const explicit = controller.getElementData(cell, "sort-val");
97
- const d =
98
- typeof explicit === "string"
99
- ? explicit
100
- : cell.textContent?.trim() ?? "";
101
-
102
- if (d !== "" && `${parseInt(d, 10)}` !== d) {
103
- anyNonInt = true;
104
- }
105
- data.push([d, index]);
106
- });
107
-
108
- // If all values were integers (or empty cells), sort numerically, with empty cells treated as
109
- // having the lowest possible value (i.e. sorted to the top if ascending, bottom if descending)
110
- if (!anyNonInt) {
111
- data.forEach(function (tuple) {
112
- tuple[0] =
113
- tuple[0] === ""
114
- ? Number.MIN_VALUE
115
- : parseInt(tuple[0] as string, 10);
116
- });
117
- }
118
-
119
- // We don't sort an array of <tr>, but instead an arrays of row *numbers*, because this way we
120
- // can enforce stable sorting, i.e. rows that compare equal are guaranteed to remain in the same
121
- // order (the JS standard does not gurantee this for sort()).
122
- data.sort(function (a, b) {
123
- // first compare the values (a[0])
124
- if (a[0] > b[0]) {
125
- return 1 * direction;
126
- } else if (a[0] < b[0]) {
127
- return -1 * direction;
128
- } else {
129
- // if the values are equal, compare the row numbers (a[1]) to guarantee stable sorting
130
- // (note that this comparison is independent of the sorting direction)
131
- return a[1] > b[1] ? 1 : -1;
132
- }
133
- });
134
-
135
- // this is the actual reordering of the table rows
136
- data.forEach(function (tup) {
137
- const row = rows[tup[1]];
138
- row.parentElement?.removeChild(row);
139
- if (firstBottomRow) {
140
- tbody.insertBefore(row, firstBottomRow);
141
- } else {
142
- tbody.appendChild(row);
143
- }
144
- });
145
-
146
- // update the UI and set the `data-sort-direction` attribute if appropriate, so that the next click
147
- // will cause sorting in descending direction
148
- this.setCurrentSort(colHead, direction === 1 ? "asc" : "desc");
149
- }
150
- }
151
-
152
- function buildIndex(
153
- section: HTMLTableSectionElement
154
- ): HTMLTableCellElement[][] {
155
- const result = buildIndexOrGetCellSlot(section);
156
- if (!(result instanceof Array)) {
157
- throw "shouldn't happen";
158
- }
159
- return result;
160
- }
161
-
162
- function getCellSlot(cell: HTMLTableCellElement): number {
163
- if (
164
- !(
165
- cell.parentElement &&
166
- cell.parentElement.parentElement instanceof HTMLTableSectionElement
167
- )
168
- ) {
169
- throw "invalid table";
170
- }
171
- const result = buildIndexOrGetCellSlot(
172
- cell.parentElement.parentElement,
173
- cell
174
- );
175
- if (typeof result !== "number") {
176
- throw "shouldn't happen";
177
- }
178
- return result;
179
- }
180
-
181
- // Just because a <td> is the 4th *child* of its <tr> doesn't mean it belongs to the 4th *column*
182
- // of the table. Previous cells may have a colspan; cells in previous rows may have a rowspan.
183
- // Because we need to know which header cells and data cells belong together, we have to 1) find out
184
- // which column number (or "slot" as we call it here) the header cell has, and 2) for each row find
185
- // out which <td> cell corresponds to this slot (because those are the rows we're sorting by).
186
- //
187
- // That's what the following function does. If the second argument is not given, it returns an index
188
- // of the table, which is an array of arrays. Each of the sub-arrays corresponds to a table row. The
189
- // indices of the sub-array correspond to column slots; the values are the actual table cell elements.
190
- // For example index[4][3] is the <td> or <th> in row 4, column 3 of the table section (<tbody> or <thead>).
191
- // Note that this element is not necessarily even in the 4th (zero-based) <tr> -- if it has a rowSpan > 1,
192
- // it may also be in a previous <tr>.
193
- //
194
- // If the second argument is given, it's a <td> or <th> that we're trying to find, and the algorithm
195
- // stops as soon as it has found it and the function returns its slot number.
196
- function buildIndexOrGetCellSlot(
197
- section: HTMLTableSectionElement,
198
- findCell?: HTMLTableCellElement
199
- ) {
200
- const index = [];
201
- let curRow: Element | null = section.children[0];
202
-
203
- // the elements of these two arrays are synchronized; the first array contains table cell elements,
204
- // the second one contains a number that indicates for how many more rows this elements will
205
- // exist (i.e. the value is initially one less than the cell's rowspan, and will be decreased for each row)
206
- const growing: HTMLTableCellElement[] = [];
207
- const growingRowsLeft: number[] = [];
208
-
209
- // continue while we have actual <tr>'s left *or* we still have rowspan'ed elements that aren't done
210
- while (
211
- curRow ||
212
- growingRowsLeft.some(function (e) {
213
- return e !== 0;
214
- })
215
- ) {
216
- const curIndexRow: HTMLTableCellElement[] = [];
217
- index.push(curIndexRow);
218
-
219
- let curSlot = 0;
220
- if (curRow) {
221
- for (
222
- let curCellInd = 0;
223
- curCellInd < curRow.children.length;
224
- curCellInd++
225
- ) {
226
- while (growingRowsLeft[curSlot]) {
227
- growingRowsLeft[curSlot]--;
228
- curIndexRow[curSlot] = growing[curSlot];
229
- curSlot++;
230
- }
231
- const cell = curRow.children[curCellInd];
232
- if (!(cell instanceof HTMLTableCellElement)) {
233
- throw "invalid table";
234
- }
235
- if (getComputedStyle(cell).display === "none") {
236
- continue;
237
- }
238
- if (cell === findCell) {
239
- return curSlot;
240
- }
241
- const nextFreeSlot = curSlot + cell.colSpan;
242
- for (; curSlot < nextFreeSlot; curSlot++) {
243
- growingRowsLeft[curSlot] = cell.rowSpan - 1; // if any of these is already growing, the table is broken -- no guarantees of anything
244
- growing[curSlot] = cell;
245
- curIndexRow[curSlot] = cell;
246
- }
247
- }
248
- }
249
- while (curSlot < growing.length) {
250
- if (growingRowsLeft[curSlot]) {
251
- growingRowsLeft[curSlot]--;
252
- curIndexRow[curSlot] = growing[curSlot];
253
- }
254
- curSlot++;
255
- }
256
- if (curRow) {
257
- curRow = curRow.nextElementSibling;
258
- }
259
- }
260
- return findCell
261
- ? -1
262
- : index; /* if findCell was given but we end up here, that means it isn't in this section */
263
- }
1
+ import * as Stacks from "../../stacks";
2
+
3
+ /**
4
+ * The string values of these enumerations should correspond with `aria-sort` valid values.
5
+ *
6
+ * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-sort#values
7
+ */
8
+ export enum SortOrder {
9
+ Ascending = "ascending",
10
+ Descending = "descending",
11
+ None = "none",
12
+ }
13
+
14
+ export class TableController extends Stacks.StacksController {
15
+ declare columnTarget: HTMLTableCellElement;
16
+ declare columnTargets: HTMLTableCellElement[];
17
+
18
+ static targets = ["column"];
19
+
20
+ sort(evt: PointerEvent) {
21
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
22
+ const controller = this;
23
+ const sortTriggerEl = evt.currentTarget;
24
+ // TODO: support *only* button as trigger in next major release
25
+ const triggerIsButton = sortTriggerEl instanceof HTMLButtonElement;
26
+ // the below conditional is here for backward compatibility with the old API
27
+ // where we did not advise buttons as sortable column head triggers
28
+ const colHead = (
29
+ triggerIsButton ? sortTriggerEl.parentElement : sortTriggerEl
30
+ ) as HTMLTableCellElement;
31
+ const table = this.element as HTMLTableElement;
32
+ const tbody = table.tBodies[0];
33
+
34
+ // the column slot number of the clicked header
35
+ const colno = getCellSlot(colHead);
36
+
37
+ if (colno < 0) {
38
+ // this shouldn't happen if the clicked element is actually a column head
39
+ return;
40
+ }
41
+
42
+ // an index of the <tbody>, so we can find out for each row which <td> element is
43
+ // in the same column slot as the header
44
+ const slotIndex = buildIndex(tbody);
45
+
46
+ // the default behavior when clicking a header is to sort by this column in ascending
47
+ // direction, *unless* it is already sorted that way
48
+ const direction =
49
+ colHead.getAttribute("aria-sort") === SortOrder.Ascending ? -1 : 1;
50
+
51
+ const rows = Array.from(table.tBodies[0].rows);
52
+
53
+ // if this is still false after traversing the data, that means all values are integers (or empty)
54
+ // and thus we'll sort numerically.
55
+ let anyNonInt = false;
56
+
57
+ // data will be a list of tuples [value, rowNum], where value is what we're sorting by
58
+ const data: [string | number, number][] = [];
59
+ let firstBottomRow: HTMLTableRowElement;
60
+ rows.forEach(function (row, index) {
61
+ const force = controller.getElementData(row, "sort-to");
62
+ if (force === "top") {
63
+ return; // rows not added to the list will automatically end up at the top
64
+ } else if (force === "bottom") {
65
+ if (!firstBottomRow) {
66
+ firstBottomRow = row;
67
+ }
68
+ return;
69
+ }
70
+ const cell = slotIndex[index][colno];
71
+ if (!cell) {
72
+ data.push(["", index]);
73
+ return;
74
+ }
75
+
76
+ // unless the to-be-sorted-by value is explicitly provided on the element via this attribute,
77
+ // the value we're using is the cell's text, trimmed of any whitespace
78
+ const explicit = controller.getElementData(cell, "sort-val");
79
+ const d = explicit ?? cell.textContent?.trim() ?? "";
80
+
81
+ if (d !== "" && `${parseInt(d, 10)}` !== d) {
82
+ anyNonInt = true;
83
+ }
84
+ data.push([d, index]);
85
+ });
86
+
87
+ // If all values were integers (or empty cells), sort numerically, with empty cells treated as
88
+ // having the lowest possible value (i.e. sorted to the top if ascending, bottom if descending)
89
+ if (!anyNonInt) {
90
+ data.forEach(function (tuple) {
91
+ tuple[0] =
92
+ tuple[0] === ""
93
+ ? Number.MIN_VALUE
94
+ : parseInt(tuple[0] as string, 10);
95
+ });
96
+ }
97
+
98
+ // We don't sort an array of <tr>, but instead an arrays of row *numbers*, because this way we
99
+ // can enforce stable sorting, i.e. rows that compare equal are guaranteed to remain in the same
100
+ // order (the JS standard does not gurantee this for sort()).
101
+ data.sort(function (a, b) {
102
+ // first compare the values (a[0])
103
+ if (a[0] > b[0]) {
104
+ return 1 * direction;
105
+ } else if (a[0] < b[0]) {
106
+ return -1 * direction;
107
+ } else {
108
+ // if the values are equal, compare the row numbers (a[1]) to guarantee stable sorting
109
+ // (note that this comparison is independent of the sorting direction)
110
+ return a[1] > b[1] ? 1 : -1;
111
+ }
112
+ });
113
+
114
+ // this is the actual reordering of the table rows
115
+ data.forEach(([_, rowIndex]) => {
116
+ const row = rows[rowIndex];
117
+ row.parentElement?.removeChild(row);
118
+
119
+ if (firstBottomRow) {
120
+ tbody.insertBefore(row, firstBottomRow);
121
+ } else {
122
+ tbody.appendChild(row);
123
+ }
124
+ });
125
+
126
+ // update the UI and set the `data-sort-direction` attribute if appropriate, so that the next click
127
+ // will cause sorting in descending direction
128
+ this.updateSortedColumnStyles(
129
+ colHead,
130
+ direction === 1 ? SortOrder.Ascending : SortOrder.Descending
131
+ );
132
+ }
133
+
134
+ private updateSortedColumnStyles = (
135
+ targetColumnHeader: Element,
136
+ direction: SortOrder
137
+ ): void => {
138
+ // Loop through all sortable columns and remove their sorting direction
139
+ // (if any), and only leave/set a sorting on `targetColumnHeader`.
140
+ this.columnTargets.forEach((header: HTMLTableCellElement) => {
141
+ const isCurrent = header === targetColumnHeader;
142
+ const classSuffix = isCurrent
143
+ ? direction === SortOrder.Ascending
144
+ ? "asc"
145
+ : "desc"
146
+ : SortOrder.None;
147
+
148
+ header.classList.toggle(
149
+ "is-sorted",
150
+ isCurrent && direction !== SortOrder.None
151
+ );
152
+ header.querySelectorAll(".js-sorting-indicator").forEach((icon) => {
153
+ icon.classList.toggle(
154
+ "d-none",
155
+ !icon.classList.contains(
156
+ "js-sorting-indicator-" + classSuffix
157
+ )
158
+ );
159
+ });
160
+
161
+ if (isCurrent) {
162
+ header.setAttribute("aria-sort", direction);
163
+ } else {
164
+ header.removeAttribute("aria-sort");
165
+ }
166
+ });
167
+ };
168
+ }
169
+
170
+ /**
171
+ * @internal This function is exported for testing purposes but is not a part of our public API
172
+ *
173
+ * @param section
174
+ */
175
+ export function buildIndex(
176
+ section: HTMLTableSectionElement
177
+ ): HTMLTableCellElement[][] {
178
+ const result = buildIndexOrGetCellSlot(section);
179
+
180
+ if (!Array.isArray(result)) {
181
+ throw "shouldn't happen";
182
+ }
183
+
184
+ return result;
185
+ }
186
+
187
+ /**
188
+ * @internal This function is exported for testing purposes but is not a part of our public API
189
+ *
190
+ * @param cell
191
+ */
192
+ export function getCellSlot(cell: HTMLTableCellElement): number {
193
+ const tableElement = cell.parentElement?.parentElement;
194
+
195
+ if (!(tableElement instanceof HTMLTableSectionElement)) {
196
+ throw "invalid table";
197
+ }
198
+
199
+ const result = buildIndexOrGetCellSlot(tableElement, cell);
200
+
201
+ if (typeof result !== "number") {
202
+ throw "shouldn't happen";
203
+ }
204
+
205
+ return result;
206
+ }
207
+
208
+ /**
209
+ * Just because a <td> is the 4th *child* of its <tr> doesn't mean it belongs to the 4th *column*
210
+ * of the table. Previous cells may have a colspan; cells in previous rows may have a rowspan.
211
+ * Because we need to know which header cells and data cells belong together, we have to 1) find out
212
+ * which column number (or "slot" as we call it here) the header cell has, and 2) for each row find
213
+ * out which <td> cell corresponds to this slot (because those are the rows we're sorting by).
214
+ *
215
+ * That's what the following function does. If the second argument is not given, it returns an index
216
+ * of the table, which is an array of arrays. Each of the sub-arrays corresponds to a table row. The
217
+ * indices of the sub-array correspond to column slots; the values are the actual table cell elements.
218
+ * For example index[4][3] is the <td> or <th> in row 4, column 3 of the table section (<tbody> or <thead>).
219
+ * Note that this element is not necessarily even in the 4th (zero-based) <tr> -- if it has a rowSpan > 1,
220
+ * it may also be in a previous <tr>.
221
+ *
222
+ * If the second argument is given, it's a <td> or <th> that we're trying to find, and the algorithm
223
+ * stops as soon as it has found it and the function returns its slot number.
224
+ */
225
+ function buildIndexOrGetCellSlot(
226
+ section: HTMLTableSectionElement,
227
+ findCell?: HTMLTableCellElement
228
+ ) {
229
+ const index = [];
230
+ let curRow: Element | null = section.children[0];
231
+
232
+ // the elements of these two arrays are synchronized; the first array contains table cell elements,
233
+ // the second one contains a number that indicates for how many more rows this elements will
234
+ // exist (i.e. the value is initially one less than the cell's rowspan, and will be decreased for each row)
235
+ const growing: HTMLTableCellElement[] = [];
236
+ const growingRowsLeft: number[] = [];
237
+
238
+ // continue while we have actual <tr>'s left *or* we still have rowspan'ed elements that aren't done
239
+ while (curRow || growingRowsLeft.some((e) => e !== 0)) {
240
+ const curIndexRow: HTMLTableCellElement[] = [];
241
+ index.push(curIndexRow);
242
+
243
+ let curSlot = 0;
244
+ if (curRow) {
245
+ for (
246
+ let curCellIdx = 0;
247
+ curCellIdx < curRow.children.length;
248
+ curCellIdx++
249
+ ) {
250
+ while (growingRowsLeft[curSlot]) {
251
+ growingRowsLeft[curSlot]--;
252
+ curIndexRow[curSlot] = growing[curSlot];
253
+ curSlot++;
254
+ }
255
+
256
+ const cell = curRow.children[curCellIdx];
257
+
258
+ if (!(cell instanceof HTMLTableCellElement)) {
259
+ throw "invalid table";
260
+ }
261
+
262
+ if (getComputedStyle(cell).display === "none") {
263
+ continue;
264
+ }
265
+
266
+ if (cell === findCell) {
267
+ return curSlot;
268
+ }
269
+
270
+ const nextFreeSlot = curSlot + cell.colSpan;
271
+
272
+ for (; curSlot < nextFreeSlot; curSlot++) {
273
+ growingRowsLeft[curSlot] = cell.rowSpan - 1; // if any of these is already growing, the table is broken -- no guarantees of anything
274
+ growing[curSlot] = cell;
275
+ curIndexRow[curSlot] = cell;
276
+ }
277
+ }
278
+ }
279
+
280
+ while (curSlot < growing.length) {
281
+ if (growingRowsLeft[curSlot]) {
282
+ growingRowsLeft[curSlot]--;
283
+ curIndexRow[curSlot] = growing[curSlot];
284
+ }
285
+
286
+ curSlot++;
287
+ }
288
+
289
+ if (curRow) {
290
+ curRow = curRow.nextElementSibling;
291
+ }
292
+ }
293
+
294
+ // if findCell was given, but we end up here, that means it isn't in this section
295
+ return findCell ? -1 : index;
296
+ }
@@ -1,4 +1,4 @@
1
- .s-table-container {
2
- overflow-x: auto;
3
- @scrollbar-styles();
4
- }
1
+ .s-table-container {
2
+ overflow-x: auto;
3
+ @scrollbar-styles();
4
+ }