@stackoverflow/stacks 2.7.3 → 2.7.4
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.
- package/LICENSE.MD +9 -9
- package/README.md +158 -180
- package/dist/css/stacks.css +6 -0
- package/dist/js/stacks.min.js +1 -1
- package/lib/atomic/border.less +139 -139
- package/lib/atomic/color.less +36 -36
- package/lib/atomic/flex.less +426 -426
- package/lib/atomic/gap.less +44 -44
- package/lib/atomic/grid.less +139 -139
- package/lib/atomic/misc.less +374 -374
- package/lib/atomic/spacing.less +98 -98
- package/lib/atomic/typography.less +266 -264
- package/lib/atomic/width-height.less +194 -194
- package/lib/base/body.less +44 -44
- package/lib/base/configuration-static.less +61 -61
- package/lib/base/fieldset.less +5 -5
- package/lib/base/icon.less +11 -11
- package/lib/base/internal.less +220 -220
- package/lib/base/reset-meyer.less +64 -64
- package/lib/base/reset-normalize.less +449 -449
- package/lib/base/reset.less +20 -20
- package/lib/components/activity-indicator/activity-indicator.less +53 -53
- package/lib/components/avatar/avatar.less +108 -108
- package/lib/components/award-bling/award-bling.less +31 -31
- package/lib/components/banner/banner.less +44 -44
- package/lib/components/banner/banner.ts +149 -149
- package/lib/components/block-link/block-link.less +82 -82
- package/lib/components/breadcrumbs/breadcrumbs.less +41 -41
- package/lib/components/button-group/button-group.less +82 -82
- package/lib/components/card/card.less +37 -37
- package/lib/components/check-control/check-control.less +17 -17
- package/lib/components/check-group/check-group.less +19 -19
- package/lib/components/checkbox_radio/checkbox_radio.less +159 -159
- package/lib/components/code-block/code-block.fixtures.ts +88 -88
- package/lib/components/code-block/code-block.less +116 -116
- package/lib/components/description/description.less +9 -9
- package/lib/components/empty-state/empty-state.less +16 -16
- package/lib/components/expandable/expandable.less +118 -118
- package/lib/components/input-fill/input-fill.less +35 -35
- package/lib/components/input-icon/input-icon.less +45 -45
- package/lib/components/input-message/input-message.less +49 -49
- package/lib/components/label/label.less +110 -110
- package/lib/components/link-preview/link-preview.less +148 -148
- package/lib/components/menu/menu.less +41 -41
- package/lib/components/modal/modal.less +118 -118
- package/lib/components/modal/modal.ts +383 -383
- package/lib/components/navigation/navigation.less +136 -136
- package/lib/components/navigation/navigation.ts +128 -128
- package/lib/components/page-title/page-title.less +51 -51
- package/lib/components/popover/popover.less +159 -159
- package/lib/components/popover/popover.ts +651 -651
- package/lib/components/post-summary/post-summary.less +457 -457
- package/lib/components/progress-bar/progress-bar.less +291 -291
- package/lib/components/prose/prose.less +452 -452
- package/lib/components/select/select.less +138 -138
- package/lib/components/spinner/spinner.less +103 -103
- package/lib/components/table/table.ts +296 -296
- package/lib/components/table-container/table-container.less +4 -4
- package/lib/components/tag/tag.less +186 -186
- package/lib/components/toast/toast.less +35 -35
- package/lib/components/toast/toast.ts +357 -357
- package/lib/components/toggle-switch/toggle-switch.less +104 -104
- package/lib/components/topbar/topbar.less +553 -553
- package/lib/components/uploader/uploader.less +205 -205
- package/lib/components/user-card/user-card.less +129 -129
- package/lib/controllers.ts +33 -33
- package/lib/exports/color-mixins.less +283 -283
- package/lib/exports/constants-helpers.less +108 -108
- package/lib/exports/constants-type.less +155 -155
- package/lib/exports/exports.less +15 -15
- package/lib/exports/mixins.less +334 -333
- package/lib/exports/spacing-mixins.less +67 -67
- package/lib/index.ts +32 -32
- package/lib/input-utils.less +41 -41
- package/lib/stacks-dynamic.less +24 -24
- package/lib/stacks-static.less +93 -93
- package/lib/stacks.less +13 -13
- package/lib/test/assertions.ts +36 -36
- package/lib/test/less-test-utils.ts +28 -28
- package/lib/test/open-wc-testing-patch.d.ts +26 -26
- package/lib/tsconfig.build.json +4 -4
- package/lib/tsconfig.json +17 -17
- package/package.json +26 -22
|
@@ -1,296 +1,296 @@
|
|
|
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 readonly columnTarget: HTMLTableCellElement;
|
|
16
|
-
declare readonly 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
|
+
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 readonly columnTarget: HTMLTableCellElement;
|
|
16
|
+
declare readonly 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
|
+
}
|