@stackoverflow/stacks 1.8.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 (137) hide show
  1. package/README.md +161 -153
  2. package/dist/{controllers/s-banner.d.ts → components/banner/banner.d.ts} +1 -1
  3. package/dist/{controllers/s-expandable-control.d.ts → components/expandable/expandable.d.ts} +1 -1
  4. package/dist/{controllers/s-modal.d.ts → components/modal/modal.d.ts} +1 -1
  5. package/dist/{controllers/s-navigation-tablist.d.ts → components/navigation/navigation.d.ts} +1 -1
  6. package/dist/{controllers/s-popover.d.ts → components/popover/popover.d.ts} +1 -1
  7. package/dist/{controllers/s-tooltip.d.ts → components/popover/tooltip.d.ts} +1 -1
  8. package/dist/components/table/table.d.ts +30 -0
  9. package/dist/{controllers/s-toast.d.ts → components/toast/toast.d.ts} +1 -1
  10. package/dist/{controllers/s-uploader.d.ts → components/uploader/uploader.d.ts} +1 -1
  11. package/dist/controllers.d.ts +9 -0
  12. package/dist/css/stacks.css +2063 -1994
  13. package/dist/css/stacks.min.css +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/js/stacks.js +1465 -1436
  16. package/dist/js/stacks.min.js +1 -1
  17. package/lib/{css/atomic/borders.less → atomic/border.less} +397 -379
  18. package/lib/{css/atomic/colors.less → atomic/color.less} +210 -210
  19. package/lib/{css/atomic → atomic}/flex.less +426 -426
  20. package/lib/{css/atomic → atomic}/gap.less +44 -44
  21. package/lib/{css/atomic → atomic}/grid.less +139 -139
  22. package/lib/{css/atomic → atomic}/misc.less +343 -343
  23. package/lib/{css/atomic → atomic}/spacing.less +342 -342
  24. package/lib/{css/atomic → atomic}/typography.less +267 -267
  25. package/lib/{css/atomic → atomic}/width-height.less +194 -194
  26. package/lib/{css/base → base}/body.less +44 -44
  27. package/lib/{css/base → base}/configuration-static.less +61 -61
  28. package/lib/{css/base → base}/fieldset.less +5 -5
  29. package/lib/{css/base/icons.less → base/icon.less} +11 -20
  30. package/lib/{css/base/internals.less → base/internal.less} +220 -220
  31. package/lib/{css/base → base}/reset-meyer.less +64 -64
  32. package/lib/{css/base → base}/reset-normalize.less +449 -449
  33. package/lib/{css/base → base}/reset.less +20 -20
  34. package/lib/components/activity-indicator/activity-indicator.a11y.test.ts +21 -0
  35. package/lib/{css/components → components/activity-indicator}/activity-indicator.less +40 -40
  36. package/lib/components/activity-indicator/activity-indicator.visual.test.ts +23 -0
  37. package/lib/{css/components/anchors.less → components/anchor/anchor.less} +61 -61
  38. package/lib/components/avatar/avatar.a11y.test.ts +36 -0
  39. package/lib/{css/components/avatars.less → components/avatar/avatar.less} +108 -108
  40. package/lib/components/avatar/avatar.visual.test.ts +54 -0
  41. package/lib/components/award-bling/award-bling.a11y.test.ts +17 -0
  42. package/lib/{css/components → components/award-bling}/award-bling.less +31 -31
  43. package/lib/components/award-bling/award-bling.visual.test.ts +26 -0
  44. package/lib/{css/components/badges.less → components/badge/badge.less} +251 -251
  45. package/lib/components/banner/banner.a11y.test.ts +37 -0
  46. package/lib/components/banner/banner.less +51 -0
  47. package/lib/{test/s-banner.test.ts → components/banner/banner.test.ts} +73 -73
  48. package/lib/{ts/controllers/s-banner.ts → components/banner/banner.ts} +149 -149
  49. package/lib/components/banner/banner.visual.test.ts +37 -0
  50. package/lib/components/block-link/block-link.a11y.test.ts +68 -0
  51. package/lib/{css/components → components/block-link}/block-link.less +80 -80
  52. package/lib/components/block-link/block-link.visual.test.ts +61 -0
  53. package/lib/components/breadcrumbs/breadcrumbs.a11y.test.ts +37 -0
  54. package/lib/{css/components → components/breadcrumbs}/breadcrumbs.less +41 -41
  55. package/lib/components/breadcrumbs/breadcrumbs.visual.test.ts +37 -0
  56. package/lib/components/button/button.a11y.test.ts +32 -0
  57. package/lib/{css/components/buttons.less → components/button/button.less} +502 -501
  58. package/lib/components/button/button.visual.test.ts +52 -0
  59. package/lib/{css/components/button-groups.less → components/button-group/button-group.less} +83 -83
  60. package/lib/components/card/card.a11y.test.ts +13 -0
  61. package/lib/{css/components/cards.less → components/card/card.less} +29 -29
  62. package/lib/components/card/card.visual.test.ts +54 -0
  63. package/lib/components/check-control/check-control.less +17 -0
  64. package/lib/components/check-group/check-group.less +19 -0
  65. package/lib/{css/components/checkboxes-radios.less → components/checkbox_radio/checkbox_radio.less} +158 -158
  66. package/lib/{css/components/code-blocks.less → components/code-block/code-block.less} +116 -116
  67. package/lib/{css/components → components/description}/description.less +9 -9
  68. package/lib/{css/components/empty-states.less → components/empty-state/empty-state.less} +16 -16
  69. package/lib/{css/components → components/expandable}/expandable.less +118 -115
  70. package/lib/components/expandable/expandable.test.ts +51 -0
  71. package/lib/{ts/controllers/s-expandable-control.ts → components/expandable/expandable.ts} +238 -238
  72. package/lib/components/input-fill/input-fill.less +35 -0
  73. package/lib/components/input-icon/input-icon.less +45 -0
  74. package/lib/components/input-message/input-message.less +48 -0
  75. package/lib/{css/components/inputs.less → components/input_textarea/input_textarea.less} +166 -297
  76. package/lib/{css/components/labels.less → components/label/label.less} +111 -111
  77. package/lib/{css/components → components/link}/link.less +119 -119
  78. package/lib/{css/components/link-previews.less → components/link-preview/link-preview.less} +139 -139
  79. package/lib/{css/components → components/menu}/menu.less +41 -41
  80. package/lib/{css/components/modals.less → components/modal/modal.less} +113 -113
  81. package/lib/{ts/controllers/s-modal.ts → components/modal/modal.ts} +379 -379
  82. package/lib/{css/components → components/navigation}/navigation.less +134 -134
  83. package/lib/{ts/controllers/s-navigation-tablist.ts → components/navigation/navigation.ts} +128 -128
  84. package/lib/{css/components/notices.less → components/notice/notice.less} +203 -292
  85. package/lib/{css/components/page-titles.less → components/page-title/page-title.less} +51 -51
  86. package/lib/{css/components → components/pagination}/pagination.less +52 -52
  87. package/lib/{css/components/popovers.less → components/popover/popover.less} +148 -147
  88. package/lib/{ts/controllers/s-popover.ts → components/popover/popover.ts} +651 -651
  89. package/lib/{test/s-tooltip.test.ts → components/popover/tooltip.test.ts} +62 -62
  90. package/lib/{ts/controllers/s-tooltip.ts → components/popover/tooltip.ts} +343 -343
  91. package/lib/{test/s-tooltip.visual.test.ts → components/popover/tooltip.visual.test.ts} +31 -31
  92. package/lib/{css/components → components/post-summary}/post-summary.less +415 -415
  93. package/lib/{css/components/progress-bars.less → components/progress-bar/progress-bar.less} +291 -291
  94. package/lib/{css/components → components/prose}/prose.less +452 -452
  95. package/lib/{css/components → components/select}/select.less +148 -148
  96. package/lib/{css/components/sidebar-widgets.less → components/sidebar-widget/sidebar-widget.less} +257 -259
  97. package/lib/{css/components → components/spinner}/spinner.less +103 -103
  98. package/lib/{css/components → components/table}/table.less +307 -297
  99. package/lib/components/table/table.test.ts +366 -0
  100. package/lib/{ts/controllers/s-table.ts → components/table/table.ts} +296 -263
  101. package/lib/components/table-container/table-container.less +4 -0
  102. package/lib/{css/components/tags.less → components/tag/tag.less} +213 -213
  103. package/lib/components/toast/toast.less +35 -0
  104. package/lib/{test/s-toast.test.ts → components/toast/toast.test.ts} +63 -63
  105. package/lib/{ts/controllers/s-toast.ts → components/toast/toast.ts} +357 -357
  106. package/lib/components/toast/toast.visual.test.ts +27 -0
  107. package/lib/{css/components/toggle-switches.less → components/toggle-switch/toggle-switch.less} +110 -110
  108. package/lib/{css/components → components/topbar}/topbar.less +436 -435
  109. package/lib/{css/components → components/uploader}/uploader.less +195 -195
  110. package/lib/{ts/controllers/s-uploader.ts → components/uploader/uploader.ts} +205 -205
  111. package/lib/{css/components/user-cards.less → components/user-card/user-card.less} +129 -129
  112. package/lib/controllers.ts +33 -0
  113. package/lib/{css/exports → exports}/constants-colors.less +1112 -1111
  114. package/lib/{css/exports → exports}/constants-helpers.less +108 -108
  115. package/lib/{css/exports → exports}/constants-type.less +153 -153
  116. package/lib/{css/exports → exports}/exports.less +15 -15
  117. package/lib/{css/exports → exports}/mixins.less +299 -299
  118. package/lib/{ts/index.ts → index.ts} +32 -32
  119. package/lib/{css/input-utils.less → input-utils.less} +44 -44
  120. package/lib/{css/stacks-dynamic.less → stacks-dynamic.less} +24 -25
  121. package/lib/stacks-static.less +93 -0
  122. package/lib/{css/stacks.less → stacks.less} +13 -13
  123. package/lib/{ts/stacks.ts → stacks.ts} +113 -113
  124. package/lib/test/open-wc-testing-patch.d.ts +26 -0
  125. package/lib/test/test-utils.ts +466 -0
  126. package/lib/tsconfig.build.json +4 -0
  127. package/lib/tsconfig.json +16 -13
  128. package/package.json +106 -105
  129. package/dist/controllers/index.d.ts +0 -9
  130. package/dist/controllers/s-table.d.ts +0 -8
  131. package/lib/css/stacks-static.less +0 -87
  132. package/lib/test/s-avatar.a11y.test.ts +0 -77
  133. package/lib/test/s-banner.visual.test.ts +0 -61
  134. package/lib/test/s-btn.a11y.test.ts +0 -123
  135. package/lib/test/s-btn.visual.test.ts +0 -16
  136. package/lib/test/s-toast.visual.test.ts +0 -48
  137. package/lib/ts/controllers/index.ts +0 -17
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ .s-table-container {
2
+ overflow-x: auto;
3
+ @scrollbar-styles();
4
+ }