@fusion.dev/helix-headless 0.2.0 → 0.3.0
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/custom-elements.json +234 -234
- package/dist/accordion/item/accordion-item.js +0 -108
- package/dist/accordion/root/accordion-root.js +0 -33
- package/dist/announcement/root/announcement-root.js +1 -76
- package/dist/app-header/root/app-header-root.js +0 -84
- package/dist/avatar/group/avatar-group.js +0 -77
- package/dist/avatar/root/avatar-root.js +0 -72
- package/dist/badge/root/badge-root.js +0 -67
- package/dist/bar-chart/root/bar-chart-root.js +3 -263
- package/dist/blockquote/root/blockquote-root.js +0 -46
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
- package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
- package/dist/button/root/button-root.js +0 -215
- package/dist/button-group/root/button-group-root.js +0 -30
- package/dist/callout/root/callout-root.js +0 -52
- package/dist/card/root/card-root.js +0 -41
- package/dist/carousel/root/carousel-root.d.ts +2 -2
- package/dist/carousel/root/carousel-root.js +2 -362
- package/dist/chat/composer/chat-composer.js +0 -131
- package/dist/chat/message/chat-message.js +0 -137
- package/dist/chat/root/chat-root.js +0 -193
- package/dist/checkbox/root/checkbox-root.js +0 -113
- package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
- package/dist/code/root/code-root.js +0 -26
- package/dist/code-block/root/code-block-root.js +0 -100
- package/dist/color-input/root/color-input-root.js +0 -217
- package/dist/color-picker/color.js +0 -69
- package/dist/color-picker/root/color-picker-root.js +0 -272
- package/dist/combobox/root/combobox-root.js +0 -428
- package/dist/command/root/command-root.js +0 -177
- package/dist/copy-button/root/copy-button-root.js +0 -134
- package/dist/data-table/root/data-table-root.js +0 -336
- package/dist/data-table/root/order.js +0 -100
- package/dist/date-input/root/date-input-root.js +1 -398
- package/dist/date-picker/root/date-picker-root.js +0 -358
- package/dist/dialog/root/dialog-root.js +0 -189
- package/dist/divider/root/divider-root.js +0 -71
- package/dist/donut-chart/root/donut-chart-root.js +0 -214
- package/dist/drawer/root/drawer-root.js +0 -116
- package/dist/drilldown/item/drilldown-item.js +0 -82
- package/dist/drilldown/root/drilldown-root.js +0 -91
- package/dist/element-events.generated.js +0 -5
- package/dist/empty/root/empty-root.js +0 -35
- package/dist/file-input/root/file-input-root.js +0 -232
- package/dist/format-date/root/format-date-root.js +0 -95
- package/dist/heatmap/root/buckets.js +0 -57
- package/dist/heatmap/root/heatmap-root.js +0 -313
- package/dist/icon/root/icon-root.js +0 -48
- package/dist/icon-button/root/icon-button-root.js +0 -30
- package/dist/image/root/image-root.js +0 -174
- package/dist/index.js +0 -9
- package/dist/input/root/input-root.js +0 -228
- package/dist/kbd/root/kbd-root.js +0 -117
- package/dist/legend/root/legend-root.js +0 -73
- package/dist/line-chart/root/line-chart-root.js +0 -172
- package/dist/list/item/list-item.js +0 -92
- package/dist/list/root/list-root.js +0 -26
- package/dist/listbox/root/listbox-root.js +2 -429
- package/dist/loader/root/loader-root.js +0 -40
- package/dist/megamenu/root/megamenu-root.js +0 -284
- package/dist/menu/item/menu-item.js +1 -221
- package/dist/menu/root/menu-root.js +1 -171
- package/dist/meter/root/meter-root.js +1 -152
- package/dist/navigation/root/navigation-root.js +0 -56
- package/dist/notifications/notification/notification.js +0 -159
- package/dist/notifications/root/notifications-root.js +0 -148
- package/dist/number-input/root/number-input-root.js +1 -263
- package/dist/option/group/option-group.js +0 -26
- package/dist/option/root/option-root.js +0 -157
- package/dist/outline/root/outline-root.js +0 -292
- package/dist/pagination/root/pagination-root.js +0 -361
- package/dist/pagination/size/pagination-size.js +0 -104
- package/dist/pagination/summary/pagination-summary.js +0 -90
- package/dist/pin-input/root/pin-input-root.js +0 -276
- package/dist/popover/root/popover-root.js +0 -103
- package/dist/progress/root/progress-root.js +1 -133
- package/dist/qr-code/root/qr-code-root.js +0 -96
- package/dist/quote/root/quote-root.js +0 -25
- package/dist/radio/group/radio-group.js +0 -81
- package/dist/radio/root/radio-root.js +0 -85
- package/dist/rating/root/rating-root.js +0 -220
- package/dist/scroll-area/root/scroll-area-root.js +0 -100
- package/dist/select/root/select-root.js +2 -218
- package/dist/skeleton/root/skeleton-root.js +0 -20
- package/dist/slider/root/slider-root.js +1 -267
- package/dist/sparkline/root/sparkline-root.js +3 -164
- package/dist/speed-dial/item/speed-dial-item.js +0 -52
- package/dist/speed-dial/root/speed-dial-root.js +1 -116
- package/dist/splitter/root/splitter-root.js +0 -489
- package/dist/stat/group/stat-group.js +0 -18
- package/dist/stat/root/stat-root.js +0 -23
- package/dist/steps/root/steps-root.js +0 -62
- package/dist/steps/step/step.js +0 -131
- package/dist/switch/root/switch-root.js +0 -80
- package/dist/table/cell/table-cell.js +0 -23
- package/dist/table/column/table-column.js +0 -158
- package/dist/table/root/table-root.js +0 -82
- package/dist/table/row/table-row.js +0 -21
- package/dist/tabs/panel/tab-panel.js +0 -49
- package/dist/tabs/root/tabs-root.js +0 -98
- package/dist/tabs/tab/tab.js +0 -59
- package/dist/tag/root/tag-root.js +0 -31
- package/dist/textarea/root/textarea-root.js +0 -162
- package/dist/time-input/root/time-input-root.js +0 -283
- package/dist/time-picker/root/time-picker-root.js +0 -326
- package/dist/timeline/item/timeline-item.js +0 -66
- package/dist/timeline/root/timeline-root.js +0 -41
- package/dist/toast/item/toast-item.js +0 -165
- package/dist/toast/root/toast-root.js +0 -51
- package/dist/toggle/group/toggle-group.js +0 -150
- package/dist/toggle/root/toggle-root.js +0 -78
- package/dist/toolbar/root/toolbar-root.js +0 -116
- package/dist/tooltip/root/tooltip-root.js +0 -427
- package/dist/tour/root/tour-root.js +0 -130
- package/dist/tour/stop/tour-stop.js +1 -127
- package/dist/tree/item/tree-item.js +0 -198
- package/dist/tree/root/tree-root.js +0 -97
- package/dist/trend/root/trend-root.js +0 -53
- package/dist/video/root/video-root.d.ts +8 -12
- package/dist/video/root/video-root.js +5 -756
- package/dist/video-playlist/item/video-playlist-item.js +1 -90
- package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
- package/dist/video-playlist/root/video-playlist-root.js +0 -113
- package/dist/virtual-list/root/virtual-list-root.js +0 -105
- package/jsx.d.ts +1408 -2576
- package/package.json +6 -6
- package/vue.d.ts +1 -1
|
@@ -9,176 +9,36 @@ import { customElement, HelixElement, HelixFilterEvent, HelixNavigateEvent, Heli
|
|
|
9
9
|
import { nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
11
11
|
import { repeat } from 'lit/directives/repeat.js';
|
|
12
|
-
/* `static-html`'s `html`, because the pagination's tag is interpolated: a
|
|
13
|
-
template naming a literal `helix-pagination` renders the styled element from
|
|
14
|
-
the headless tier, and `lit`'s own `html` treats `${tag}` in tag position as
|
|
15
|
-
text, which renders an element called `lit$…$` that lays out as nothing.
|
|
16
|
-
Measured: the styled spec read `localName` as `lit$640177303$`. */
|
|
17
12
|
import { html } from 'lit/static-html.js';
|
|
18
13
|
import '../../pagination/index.js';
|
|
19
14
|
import { cycle, format, ordered, parse } from './order.js';
|
|
20
|
-
/**
|
|
21
|
-
* @summary A table that owns a result: rows as data, with sort, filter and paging.
|
|
22
|
-
*
|
|
23
|
-
* @description
|
|
24
|
-
* **The schema is markup and the data is a property.** Columns are declared as
|
|
25
|
-
* `helix-table-column` children, because a schema is small, declarative, and
|
|
26
|
-
* something a reader should be able to see in the HTML; rows are handed over as
|
|
27
|
-
* an array, because data is large and a table that draws ten thousand elements
|
|
28
|
-
* cannot window them.
|
|
29
|
-
*
|
|
30
|
-
* ```html
|
|
31
|
-
* <helix-data-table .rows=${rows} sort="name:asc" page-size="20">
|
|
32
|
-
* <helix-table-column field="name" sortable>Name</helix-table-column>
|
|
33
|
-
* <helix-table-column field="size" align="end" sortable .cell=${bytes}>Size</helix-table-column>
|
|
34
|
-
* </helix-data-table>
|
|
35
|
-
* ```
|
|
36
|
-
*
|
|
37
|
-
* `helix-table` takes rows as markup and is the right component for a table of
|
|
38
|
-
* twelve things a person wrote out.
|
|
39
|
-
*
|
|
40
|
-
* **Sort and filter apply to the whole set, never the page.** Sorting the drawn
|
|
41
|
-
* window is correct on the first screen and wrong on every other one, which is
|
|
42
|
-
* how it survives being looked at. The arithmetic lives in `order.ts` where
|
|
43
|
-
* nothing can see a window, and its spec asserts the composition rather than the
|
|
44
|
-
* three steps, because each step alone passes either way.
|
|
45
|
-
*
|
|
46
|
-
* **What it does not do, stated where a consumer will read it: the cell values
|
|
47
|
-
* do not server-render.** The headings do, because a column renders its own, and
|
|
48
|
-
* the row elements do, because their count comes from `rows`. Reading a cell
|
|
49
|
-
* needs the columns, and the columns are markup children that Lit SSR does not
|
|
50
|
-
* hand this element: `children` is undefined there. So a crawler and a reader on
|
|
51
|
-
* a slow connection see a table of the right shape with nothing in it, which is
|
|
52
|
-
* strictly less than `helix-table` serves for a table whose rows are markup.
|
|
53
|
-
*
|
|
54
|
-
* That is the cost of "the schema is markup and the data is a property". Use
|
|
55
|
-
* `helix-table` where the content has to be in the served HTML and the result is
|
|
56
|
-
* small enough to write out; use this where the result is large, and accept that
|
|
57
|
-
* the first paint is a shell. The obvious workaround, a `columns` property
|
|
58
|
-
* beside the markup columns, is deliberately not offered: it splits one source
|
|
59
|
-
* of truth for the columns into two that nothing checks against each other, and
|
|
60
|
-
* a table rendering one way on the server and another in the browser is that
|
|
61
|
-
* failure with a hydration mismatch on top.
|
|
62
|
-
*
|
|
63
|
-
* **Every axis is controlled or uncontrolled, and that is what makes a server
|
|
64
|
-
* mode fall out rather than be bolted on.** `sort`, `filter`, `page` and
|
|
65
|
-
* `page-size` each defer to the consumer when set and seed themselves from a
|
|
66
|
-
* `default-*` when not. A consumer who answers all four holds the data flow:
|
|
67
|
-
* they hear the reader's request, fetch, and hand back a new `rows` with a
|
|
68
|
-
* `total`. Retrofitting that inverts the flow and is a rewrite, so it is here
|
|
69
|
-
* from the start.
|
|
70
|
-
*
|
|
71
|
-
* @since 0.1.0
|
|
72
|
-
* @status experimental
|
|
73
|
-
* @category data
|
|
74
|
-
* @aliases Grid, Result table, Data grid
|
|
75
|
-
* @documentation https://helix.fusion.dev/components/data-table
|
|
76
|
-
*
|
|
77
|
-
* @slot - The columns, declared as `helix-table-column` children.
|
|
78
|
-
* @slot footer - Below the grid, replacing the pagination this would otherwise draw.
|
|
79
|
-
* @slot toolbar - Above the grid, for a search field or actions.
|
|
80
|
-
*
|
|
81
|
-
* @event {HelixFilterEvent} helix-filter - Fired when the query changes, and cancelable.
|
|
82
|
-
* @event {HelixNavigateEvent} helix-navigate - Fired when a reader asks for another page, and cancelable.
|
|
83
|
-
* @event {HelixSortEvent} helix-sort - Fired when a reader asks for a different order, and cancelable.
|
|
84
|
-
*
|
|
85
|
-
* @csspart cell - One cell.
|
|
86
|
-
* @csspart empty - Shown when no rows match.
|
|
87
|
-
* @csspart footer - The region below the grid.
|
|
88
|
-
* @csspart head - The row the column headings sit in. `display: contents`, so it takes no space.
|
|
89
|
-
* @csspart loading - Shown while the rows are still on their way.
|
|
90
|
-
* @csspart pagination - The pagination, where this draws one.
|
|
91
|
-
* @csspart row - One row of data.
|
|
92
|
-
* @csspart table - The grid.
|
|
93
|
-
* @csspart toolbar - The region above the grid.
|
|
94
|
-
*
|
|
95
|
-
* @cssproperty --data-table-template - The column track list this element worked out from the columns declared in it. Written by the component, read by the sheet that paints: a stylesheet takes it to lay the grid out, and setting it does nothing, because the component writes it inline on the very element that reads it and an inline declaration beats any sheet. `--data-table-columns` is the one to set.
|
|
96
|
-
*/
|
|
97
15
|
let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollection(HelixElement) {
|
|
98
16
|
constructor() {
|
|
99
17
|
super(...arguments);
|
|
100
|
-
/**
|
|
101
|
-
* Query to open with, where nothing declared one.
|
|
102
|
-
*/
|
|
103
18
|
this.defaultFilter = '';
|
|
104
|
-
/**
|
|
105
|
-
* Page to open on, where nothing declared one.
|
|
106
|
-
*/
|
|
107
19
|
this.defaultPage = 1;
|
|
108
|
-
/**
|
|
109
|
-
* Page size to open with, where nothing declared one.
|
|
110
|
-
*/
|
|
111
20
|
this.defaultPageSize = 0;
|
|
112
|
-
/**
|
|
113
|
-
* Order to open on, where nothing declared one.
|
|
114
|
-
*/
|
|
115
21
|
this.defaultSort = '';
|
|
116
|
-
/**
|
|
117
|
-
* The field naming a row's stable identity.
|
|
118
|
-
*
|
|
119
|
-
* **Needed before selection exists, not after.** Lit keys the rendered rows by
|
|
120
|
-
* it, so a sort moves a row's element rather than rewriting every cell in
|
|
121
|
-
* place, which is what keeps focus inside a cell and keeps a CSS transition
|
|
122
|
-
* attached to the row it belongs to. Left unset the index is the key, which is
|
|
123
|
-
* correct only while nothing reorders: it is why this defaults to something
|
|
124
|
-
* that works and is worth setting anyway.
|
|
125
|
-
*/
|
|
126
22
|
this.key = '';
|
|
127
|
-
/**
|
|
128
|
-
* Accessible name for the grid.
|
|
129
|
-
*/
|
|
130
23
|
this.label = '';
|
|
131
|
-
/**
|
|
132
|
-
* Whether this element does the work or asks the consumer to.
|
|
133
|
-
*
|
|
134
|
-
* `client` filters, sorts and pages `rows` itself. `server` applies none of
|
|
135
|
-
* them: `rows` is the page, `total` is the size of the whole result, and the
|
|
136
|
-
* three events are requests the consumer answers by fetching. Past some size
|
|
137
|
-
* the client cannot hold the data, so this is not an optimization, it is the
|
|
138
|
-
* only mode that works, and it is here from the start because adding it later
|
|
139
|
-
* inverts the data flow.
|
|
140
|
-
*/
|
|
141
24
|
this.mode = 'client';
|
|
142
|
-
/**
|
|
143
|
-
* The whole result, in the order it arrived.
|
|
144
|
-
*
|
|
145
|
-
* A property and not an attribute: it is an array. Left alone this element
|
|
146
|
-
* filters, sorts and pages it; in `server` mode it is the page the consumer
|
|
147
|
-
* already fetched and none of the three is applied.
|
|
148
|
-
*
|
|
149
|
-
* **Read, never written.** Sorting produces a new array rather than reordering
|
|
150
|
-
* this one, because a consumer's array may be the same object their own state
|
|
151
|
-
* holds and reordering it under them is a mutation they did not ask for.
|
|
152
|
-
*/
|
|
153
25
|
this.rows = [];
|
|
154
|
-
/**
|
|
155
|
-
* How many rows the whole result has, where this element cannot count them.
|
|
156
|
-
*
|
|
157
|
-
* Only read in `server` mode, where `rows` is one page and its length says
|
|
158
|
-
* nothing about the result. Ignored in `client` mode rather than trusted,
|
|
159
|
-
* because two sources for one count is the mistake, and here the array is the
|
|
160
|
-
* one that cannot be stale.
|
|
161
|
-
*/
|
|
162
26
|
this.total = 0;
|
|
163
27
|
this.sortState = undefined;
|
|
164
28
|
this.filterState = undefined;
|
|
165
29
|
this.pageState = undefined;
|
|
166
30
|
this.pageSizeState = undefined;
|
|
167
31
|
this.paginatedState = undefined;
|
|
168
|
-
/** Said once per element, so a table that re-renders does not repeat it. */
|
|
169
32
|
this.warnedAboutSize = false;
|
|
170
|
-
/** Columns arrive from the light DOM, which changes no property here. */
|
|
171
33
|
this.handleSlotChange = () => {
|
|
172
34
|
this.requestUpdate();
|
|
173
35
|
};
|
|
174
|
-
/** A click on a sortable heading. */
|
|
175
36
|
this.handleHeadClick = (event) => {
|
|
176
37
|
const column = this.columnFrom(event);
|
|
177
38
|
if (column?.canSort) {
|
|
178
39
|
this.toggleSort(column.field);
|
|
179
40
|
}
|
|
180
41
|
};
|
|
181
|
-
/** The same on Enter and Space, since a heading that sorts is a control. */
|
|
182
42
|
this.handleHeadKey = (event) => {
|
|
183
43
|
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
184
44
|
return;
|
|
@@ -189,14 +49,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
189
49
|
this.toggleSort(column.field);
|
|
190
50
|
}
|
|
191
51
|
};
|
|
192
|
-
/**
|
|
193
|
-
* The composed pagination's request, relayed as this element's own.
|
|
194
|
-
*
|
|
195
|
-
* Stopped rather than allowed to bubble, because two `helix-navigate` events
|
|
196
|
-
* for one click would reach a consumer's handler twice: the pagination's, and
|
|
197
|
-
* this element's after it. The one that leaves carries this element's page
|
|
198
|
-
* bounds rather than the inner component's.
|
|
199
|
-
*/
|
|
200
52
|
this.handleNavigate = (event) => {
|
|
201
53
|
event.stopPropagation();
|
|
202
54
|
const wanted = Number(event.detail.value);
|
|
@@ -206,26 +58,10 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
206
58
|
};
|
|
207
59
|
}
|
|
208
60
|
static { HelixHeadlessDataTable_1 = this; }
|
|
209
|
-
/**
|
|
210
|
-
* Nothing is assigned by name, so the columns can be put in a header row.
|
|
211
|
-
*
|
|
212
|
-
* `role="columnheader"` under `role="grid"` with no owning row is invalid, and
|
|
213
|
-
* the columns are ordinary children a consumer writes with no `slot`. Manual
|
|
214
|
-
* assignment is the only way to wrap them without editing the consumer's
|
|
215
|
-
* markup, which is a change a React consumer's hydration reports and does not
|
|
216
|
-
* patch up. The same arrangement `helix-table` arrived at, for the same reason.
|
|
217
|
-
*/
|
|
218
61
|
static { this.shadowRootOptions = {
|
|
219
62
|
...HelixElement.shadowRootOptions,
|
|
220
63
|
slotAssignment: 'manual',
|
|
221
64
|
}; }
|
|
222
|
-
/**
|
|
223
|
-
* The order, as `field:direction`.
|
|
224
|
-
*
|
|
225
|
-
* Set it and this element defers: a reader's click fires `helix-sort` and
|
|
226
|
-
* changes nothing until a new value arrives. `default-sort` seeds an order this
|
|
227
|
-
* element then owns.
|
|
228
|
-
*/
|
|
229
65
|
get sort() {
|
|
230
66
|
return this.sortState ?? this.defaultSort;
|
|
231
67
|
}
|
|
@@ -234,13 +70,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
234
70
|
this.sortState = next;
|
|
235
71
|
this.requestUpdate('sort', previous);
|
|
236
72
|
}
|
|
237
|
-
/**
|
|
238
|
-
* What the rows are narrowed to, as one query across every column.
|
|
239
|
-
*
|
|
240
|
-
* One query rather than a predicate per column, which is what a reader means
|
|
241
|
-
* by a search box above a table. It matches on each column's ordering value,
|
|
242
|
-
* so search and sort cannot disagree about what a column holds.
|
|
243
|
-
*/
|
|
244
73
|
get filter() {
|
|
245
74
|
return this.filterState ?? this.defaultFilter;
|
|
246
75
|
}
|
|
@@ -249,13 +78,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
249
78
|
this.filterState = next;
|
|
250
79
|
this.requestUpdate('filter', previous);
|
|
251
80
|
}
|
|
252
|
-
/**
|
|
253
|
-
* The page on show, from 1.
|
|
254
|
-
*
|
|
255
|
-
* Clamped on the way out, so what this reports is the page it drew: a
|
|
256
|
-
* bookmarked link to page 99 of a result that has since shrunk reads back as
|
|
257
|
-
* the last page rather than as 99.
|
|
258
|
-
*/
|
|
259
81
|
get page() {
|
|
260
82
|
const asked = this.pageState ?? this.defaultPage;
|
|
261
83
|
return Math.min(this.pages, Math.max(1, Math.floor(asked) || 1));
|
|
@@ -265,15 +87,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
265
87
|
this.pageState = next;
|
|
266
88
|
this.requestUpdate('page', previous);
|
|
267
89
|
}
|
|
268
|
-
/**
|
|
269
|
-
* How many rows a page holds. Zero is every row, which is what "no paging" means.
|
|
270
|
-
*
|
|
271
|
-
* **It announces nothing, and that is not an omission.** Nothing in this
|
|
272
|
-
* element ever asks for a different page size: a size selector is
|
|
273
|
-
* `helix-pagination-size`, which a consumer puts in the `footer` slot and
|
|
274
|
-
* answers themselves. Routing this through `requestChange` would announce an
|
|
275
|
-
* event the element never has cause to fire.
|
|
276
|
-
*/
|
|
277
90
|
get pageSize() {
|
|
278
91
|
return Math.max(0, Math.floor(this.pageSizeState ?? this.defaultPageSize) || 0);
|
|
279
92
|
}
|
|
@@ -282,14 +95,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
282
95
|
this.pageSizeState = next;
|
|
283
96
|
this.requestUpdate('pageSize', previous);
|
|
284
97
|
}
|
|
285
|
-
/**
|
|
286
|
-
* Whether to draw a pagination under the grid.
|
|
287
|
-
*
|
|
288
|
-
* On by default once a page size is set, because a paged table with no way to
|
|
289
|
-
* change pages shows a reader the first twenty rows and no way to know there
|
|
290
|
-
* are more. Off for a consumer supplying their own through the `footer` slot,
|
|
291
|
-
* which is why the slot replaces this rather than sitting beside it.
|
|
292
|
-
*/
|
|
293
98
|
get paginated() {
|
|
294
99
|
return this.paginatedState ?? this.pageSize > 0;
|
|
295
100
|
}
|
|
@@ -298,56 +103,19 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
298
103
|
this.paginatedState = next;
|
|
299
104
|
this.requestUpdate('paginated', previous);
|
|
300
105
|
}
|
|
301
|
-
/** The columns declared as children, in the order they are written. */
|
|
302
106
|
get tableColumns() {
|
|
303
|
-
/* `?? []` is the server. Lit's element shim carries attributes and not a
|
|
304
|
-
tree, so `children` is undefined there, and this getter is read during
|
|
305
|
-
`render`: spreading it throws and takes the whole SSR suite with it. */
|
|
306
107
|
return [...(this.children ?? [])].filter((child) => 'isTableColumn' in child && child.isTableColumn === true);
|
|
307
108
|
}
|
|
308
|
-
/** The columns reduced to what the ordering needs. */
|
|
309
109
|
get axes() {
|
|
310
110
|
return this.tableColumns
|
|
311
111
|
.filter((column) => column.field !== '')
|
|
312
112
|
.map((column) => ({ field: column.field, order: (row) => column.order(row) }));
|
|
313
113
|
}
|
|
314
|
-
/** The track list, from the columns if there are any. */
|
|
315
114
|
get template() {
|
|
316
115
|
const declared = this.tableColumns;
|
|
317
116
|
return declared.length > 0 ? declared.map((column) => column.track).join(' ') : '';
|
|
318
117
|
}
|
|
319
|
-
/**
|
|
320
|
-
* How many rows this draws before it is worth saying something, and why 2,000.
|
|
321
|
-
*
|
|
322
|
-
* Measured 28 Aug 2026 on a four-column grid, mounting and painting: 100 rows
|
|
323
|
-
* in 10.9ms, 500 in 30.2ms, 2,000 in 83.2ms and **5,000 rows as 20,000 cells
|
|
324
|
-
* in 186ms**. It is linear at roughly 37 microseconds a row, so the 100ms an
|
|
325
|
-
* interaction has to stay inside lands near 2,700 and this sits under it.
|
|
326
|
-
* Sorting stays cheap throughout, 23.7ms at five thousand, so the cost is the
|
|
327
|
-
* cells rather than the work over them.
|
|
328
|
-
*/
|
|
329
118
|
static { this.COMFORTABLE = 2_000; }
|
|
330
|
-
/**
|
|
331
|
-
* Says so when a table is asked to draw more than it comfortably can.
|
|
332
|
-
*
|
|
333
|
-
* **The component renders every row it is given and defaults to doing so.**
|
|
334
|
-
* `defaultPageSize` is `0`, so `paginated` is false unless a consumer asks,
|
|
335
|
-
* which means the out-of-the-box arrangement is the unbounded one and nothing
|
|
336
|
-
* about it says that a result of any size will simply be drawn.
|
|
337
|
-
*
|
|
338
|
-
* **Windowing was refused**, which is worth recording because it is the
|
|
339
|
-
* obvious answer. This element already has two mechanisms: `paginated` with a
|
|
340
|
-
* `page-size`, and `mode="server"` where `rows` is one page, `total` is the
|
|
341
|
-
* whole result and `aria-rowcount` already reports the result rather than the
|
|
342
|
-
* window, which is the hard half of virtualization done correctly. A third
|
|
343
|
-
* would fight the first two, and virtualizing a grid means variable row
|
|
344
|
-
* heights, a sticky header and keeping `aria-rowindex` honest against a result
|
|
345
|
-
* the element is no longer holding. `VirtualizeController` exists for the case
|
|
346
|
-
* where the rows are a flat list and this is not that.
|
|
347
|
-
*
|
|
348
|
-
* Warned once per element, in a browser only: a warning during a server render
|
|
349
|
-
* is one nobody can act on and one that would print per request.
|
|
350
|
-
*/
|
|
351
119
|
warnIfCrowded(drawn) {
|
|
352
120
|
if (this.warnedAboutSize ||
|
|
353
121
|
drawn <= HelixHeadlessDataTable_1.COMFORTABLE ||
|
|
@@ -360,14 +128,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
360
128
|
'crosses an interaction budget around 2,700. Set `page-size` to paginate, or `mode="server"` ' +
|
|
361
129
|
'to hand it one page at a time with `total` describing the whole result.');
|
|
362
130
|
}
|
|
363
|
-
/**
|
|
364
|
-
* The rows to draw, and the counts around them.
|
|
365
|
-
*
|
|
366
|
-
* In `server` mode this element applies nothing: `rows` is already the page,
|
|
367
|
-
* `total` is the result, and both counts come from the consumer. Anything else
|
|
368
|
-
* would be this element filtering a page it was handed, which narrows one
|
|
369
|
-
* screen and calls it a search.
|
|
370
|
-
*/
|
|
371
131
|
get view() {
|
|
372
132
|
if (this.mode === 'server') {
|
|
373
133
|
const total = Math.max(this.total, this.rows.length);
|
|
@@ -382,7 +142,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
382
142
|
this.warnIfCrowded(view.rows.length);
|
|
383
143
|
return view;
|
|
384
144
|
}
|
|
385
|
-
/** How many pages the result makes, which is at least one even when empty. */
|
|
386
145
|
get pages() {
|
|
387
146
|
const size = this.pageSize;
|
|
388
147
|
if (size <= 0) {
|
|
@@ -391,12 +150,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
391
150
|
const count = this.mode === 'server' ? Math.max(this.total, this.rows.length) : this.matched;
|
|
392
151
|
return Math.max(1, Math.ceil(count / size));
|
|
393
152
|
}
|
|
394
|
-
/**
|
|
395
|
-
* Rows after filtering, computed without paging.
|
|
396
|
-
*
|
|
397
|
-
* Separate from `view` because `pages` needs it and `page` needs `pages`:
|
|
398
|
-
* asking `view` for it would recurse through the clamp in `page`.
|
|
399
|
-
*/
|
|
400
153
|
get matched() {
|
|
401
154
|
const query = this.filter.trim();
|
|
402
155
|
if (query === '') {
|
|
@@ -411,17 +164,9 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
411
164
|
: String(value).toLocaleLowerCase().includes(needle);
|
|
412
165
|
})).length;
|
|
413
166
|
}
|
|
414
|
-
/** Emptiness is a fact about the data here, not about slotted children. */
|
|
415
167
|
get hasRows() {
|
|
416
168
|
return this.view.rows.length > 0;
|
|
417
169
|
}
|
|
418
|
-
/**
|
|
419
|
-
* Asks for a different order, and reports whether the request stood.
|
|
420
|
-
*
|
|
421
|
-
* Announced before it is applied, so `preventDefault()` is a refusal and the
|
|
422
|
-
* order does not move, which is what makes a server-side sort expressible: the
|
|
423
|
-
* consumer refuses the local reorder, fetches, and hands back a new set.
|
|
424
|
-
*/
|
|
425
170
|
requestSort(next) {
|
|
426
171
|
return requestChange({
|
|
427
172
|
apply: (value) => {
|
|
@@ -434,7 +179,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
434
179
|
next,
|
|
435
180
|
});
|
|
436
181
|
}
|
|
437
|
-
/** Asks for a different query. */
|
|
438
182
|
requestFilter(next) {
|
|
439
183
|
return requestChange({
|
|
440
184
|
apply: (value) => {
|
|
@@ -447,7 +191,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
447
191
|
next,
|
|
448
192
|
});
|
|
449
193
|
}
|
|
450
|
-
/** Asks for a different page. */
|
|
451
194
|
requestPage(next) {
|
|
452
195
|
if (next === this.page || next < 1 || next > this.pages) {
|
|
453
196
|
return false;
|
|
@@ -463,30 +206,12 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
463
206
|
next,
|
|
464
207
|
});
|
|
465
208
|
}
|
|
466
|
-
/**
|
|
467
|
-
* Narrows the rows, from a consumer's own search field.
|
|
468
|
-
*
|
|
469
|
-
* Public because the query lives above the table in every design that has one,
|
|
470
|
-
* and a consumer wiring their own input needs somewhere to send it. Returns
|
|
471
|
-
* whether the change stood, the same as every other request here.
|
|
472
|
-
*/
|
|
473
209
|
search(query) {
|
|
474
210
|
return this.requestFilter(query);
|
|
475
211
|
}
|
|
476
|
-
/**
|
|
477
|
-
* Cycles the order by one column, which is what a click on its heading means.
|
|
478
|
-
*
|
|
479
|
-
* Ascending, descending, then the order the rows arrived in. Public for the
|
|
480
|
-
* same reason `search` is: a consumer's own heading UI needs a way to ask.
|
|
481
|
-
*/
|
|
482
212
|
toggleSort(field) {
|
|
483
213
|
return this.requestSort(format(cycle(parse(this.sort), field)));
|
|
484
214
|
}
|
|
485
|
-
/**
|
|
486
|
-
* The columns into the header row, everything else into the toolbar and footer.
|
|
487
|
-
*
|
|
488
|
-
* Manual assignment, so a consumer's own markup is never written to.
|
|
489
|
-
*/
|
|
490
215
|
assign() {
|
|
491
216
|
const root = this.renderRoot;
|
|
492
217
|
if (!root?.querySelector) {
|
|
@@ -513,30 +238,10 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
513
238
|
super.willUpdate(changed);
|
|
514
239
|
hostAria(this.internals, { label: this.label || 'Table', role: 'grid' });
|
|
515
240
|
}
|
|
516
|
-
/**
|
|
517
|
-
* Says on the toolbar and the footer whether anything is in them.
|
|
518
|
-
*
|
|
519
|
-
* **A stylesheet cannot ask.** `[part="toolbar"]:has(> slot:not(:has(*)))` is
|
|
520
|
-
* two faults at once: `:has()` may not be nested inside `:has()`, so the
|
|
521
|
-
* selector fails to parse and the whole rule is discarded; and
|
|
522
|
-
* `slot:not(:has(*))` is as true of a filled slot as of an empty one, because
|
|
523
|
-
* assigned nodes are not a slot's DOM children. Both verified in a browser.
|
|
524
|
-
*/
|
|
525
241
|
markEmpty() {
|
|
526
242
|
this.markRegion('toolbar', ['toolbar']);
|
|
527
|
-
/* The footer is not empty while this element is drawing its own pagination
|
|
528
|
-
into it. */
|
|
529
243
|
this.markRegion('footer', ['footer'], '[part="pagination"]');
|
|
530
244
|
}
|
|
531
|
-
/**
|
|
532
|
-
* One region, hidden when its slot holds nothing and nothing of ours fills it.
|
|
533
|
-
*
|
|
534
|
-
* `hidden` beside the `data-empty` mirror: `data-empty` is the hook the styled
|
|
535
|
-
* sheet reads, and this tier ships no sheet, so an empty region left in the box
|
|
536
|
-
* tree would be a gap a headless consumer cannot close without knowing to look
|
|
537
|
-
* for it. The styled rule stays too, because that sheet sets `display` on these
|
|
538
|
-
* parts and therefore outranks the UA's `[hidden]` one.
|
|
539
|
-
*/
|
|
540
245
|
markRegion(part, slots, occupied) {
|
|
541
246
|
const region = this.renderRoot?.querySelector(`[part="${part}"]`);
|
|
542
247
|
const empty = !slots.some((name) => slotFilled(this.renderRoot?.querySelector(`slot[name="${name}"]`))) && !(occupied && this.renderRoot?.querySelector(occupied));
|
|
@@ -549,20 +254,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
549
254
|
this.assign();
|
|
550
255
|
this.announceSort();
|
|
551
256
|
}
|
|
552
|
-
/**
|
|
553
|
-
* Tells each column which way it is ordered, if it is the one.
|
|
554
|
-
*
|
|
555
|
-
* **The table owns the sort and the column carries the semantics**, which is
|
|
556
|
-
* the only arrangement that can be right: `role="columnheader"` is on the
|
|
557
|
-
* column, so `aria-sort` has to be there too, and the value is a fact about
|
|
558
|
-
* the table's `sort` rather than about any column. Pushed down here rather
|
|
559
|
-
* than pulled up there, because a column asking its parent would have to know
|
|
560
|
-
* what kind of parent it has, and it is slotted into two.
|
|
561
|
-
*
|
|
562
|
-
* In `updated` and after `assign`, so the order announced is the order the
|
|
563
|
-
* rows are already in. Announced before the rows moved is the sort a reader is
|
|
564
|
-
* told about being one they cannot see yet.
|
|
565
|
-
*/
|
|
566
257
|
announceSort() {
|
|
567
258
|
const current = parse(this.sort);
|
|
568
259
|
for (const column of this.tableColumns) {
|
|
@@ -585,10 +276,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
585
276
|
renderRows(rows) {
|
|
586
277
|
const columns = this.tableColumns;
|
|
587
278
|
const field = this.key;
|
|
588
|
-
/* Keyed, so a sort moves a row's element rather than rewriting every cell
|
|
589
|
-
in place: focus inside a cell survives and a transition stays attached to
|
|
590
|
-
the row it belongs to. The index is the fallback and is correct only
|
|
591
|
-
while nothing reorders, which is why `key` is worth setting. */
|
|
592
279
|
return html `${repeat(rows, (row, index) => (field ? String(row[field] ?? index) : index), (row, index) => html `<div
|
|
593
280
|
part="row"
|
|
594
281
|
role="row"
|
|
@@ -597,14 +284,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
597
284
|
${columns.map((column) => this.renderCell(column, row))}
|
|
598
285
|
</div>`)}`;
|
|
599
286
|
}
|
|
600
|
-
/**
|
|
601
|
-
* A row's place in the whole result, counting the header as row 1.
|
|
602
|
-
*
|
|
603
|
-
* **The number describes the result and not the window**, which is the whole
|
|
604
|
-
* reason a virtualized or paged grid needs `aria-rowindex` at all: a screen
|
|
605
|
-
* reader on page three should hear row 41, and a grid whose indices restart at
|
|
606
|
-
* every page tells a reader the result is twenty rows long however many it has.
|
|
607
|
-
*/
|
|
608
287
|
rowIndex(within) {
|
|
609
288
|
const size = this.pageSize;
|
|
610
289
|
const offset = size > 0 ? (this.page - 1) * size : 0;
|
|
@@ -642,21 +321,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
642
321
|
: nothing}
|
|
643
322
|
</div>`;
|
|
644
323
|
}
|
|
645
|
-
/**
|
|
646
|
-
* Listens on the host, not on the slot the columns are assigned to.
|
|
647
|
-
*
|
|
648
|
-
* The columns are light-DOM children of this element, so their events bubble
|
|
649
|
-
* to it directly and the shadow tree is not involved at all. Bound on the
|
|
650
|
-
* `<slot>` instead, this works in a browser, because an event from a slotted
|
|
651
|
-
* node passes through the slot in the flattened tree, and does not work in
|
|
652
|
-
* happy-dom, which does not implement that propagation: measured, the host
|
|
653
|
-
* hears the click and the slot does not. A spec written against the slot fails
|
|
654
|
-
* in the suite while the component is correct.
|
|
655
|
-
*
|
|
656
|
-
* Added here rather than in the template because there is nothing in the
|
|
657
|
-
* shadow tree to attach it to, and removed on disconnect because this element
|
|
658
|
-
* is the listener's owner rather than its subject.
|
|
659
|
-
*/
|
|
660
324
|
connectedCallback() {
|
|
661
325
|
super.connectedCallback();
|
|
662
326
|
this.addEventListener('click', this.handleHeadClick);
|
|
@@ -1,29 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Filter, sort, then window. In that order, over the whole set, every time.
|
|
3
|
-
*
|
|
4
|
-
* **This is the file that stops the classic virtualization bug.** Sorting the
|
|
5
|
-
* rendered window looks completely correct on the first screen and is wrong
|
|
6
|
-
* everywhere else, and it is the natural thing to write once rows are data and
|
|
7
|
-
* only some of them are drawn. Keeping the three operations here, as functions
|
|
8
|
-
* over an array with no knowledge of what is on screen, is what makes that
|
|
9
|
-
* mistake hard to make: nothing in this module can see a window, and `window()`
|
|
10
|
-
* is the last thing applied rather than the first.
|
|
11
|
-
*
|
|
12
|
-
* The order is not a preference. Filtering after sorting sorts rows that are
|
|
13
|
-
* about to be thrown away, which is only slower; paging before either is the bug.
|
|
14
|
-
* Filter narrows, sort arranges what is left, the page is a slice of that, and
|
|
15
|
-
* the count a reader is shown is the length after filtering rather than the
|
|
16
|
-
* length of the data, or the pagination promises pages that are empty.
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* `field:asc` and `field:desc`, and nothing else.
|
|
20
|
-
*
|
|
21
|
-
* A bare `field` parses as ascending, because that is what a URL query usually
|
|
22
|
-
* carries and refusing it would make the common case the verbose one. Anything
|
|
23
|
-
* unparseable is no sort rather than an error: this value arrives from a query
|
|
24
|
-
* string as often as from code, and a bad one should leave the table in the
|
|
25
|
-
* order the data came in rather than break the page.
|
|
26
|
-
*/
|
|
27
1
|
export const parse = (spec) => {
|
|
28
2
|
const trimmed = spec.trim();
|
|
29
3
|
if (trimmed === '') {
|
|
@@ -35,36 +9,13 @@ export const parse = (spec) => {
|
|
|
35
9
|
}
|
|
36
10
|
return { direction, field };
|
|
37
11
|
};
|
|
38
|
-
/** A `Sort` written back out, so the property and the attribute agree. */
|
|
39
12
|
export const format = (sort) => sort ? `${sort.field}:${sort.direction}` : '';
|
|
40
|
-
/**
|
|
41
|
-
* What a reader gets on the next click of a sortable heading.
|
|
42
|
-
*
|
|
43
|
-
* Ascending, descending, then none. **The third state is not an omission to be
|
|
44
|
-
* tidied away**: the order rows arrived in is information the data came with,
|
|
45
|
-
* often the server's own relevance ranking, and a two-state cycle makes it
|
|
46
|
-
* unreachable once a reader has sorted once.
|
|
47
|
-
*/
|
|
48
13
|
export const cycle = (current, field) => {
|
|
49
14
|
if (current?.field !== field) {
|
|
50
15
|
return { direction: 'asc', field };
|
|
51
16
|
}
|
|
52
17
|
return current.direction === 'asc' ? { direction: 'desc', field } : undefined;
|
|
53
18
|
};
|
|
54
|
-
/**
|
|
55
|
-
* Rows matching a query, across every axis.
|
|
56
|
-
*
|
|
57
|
-
* One query over all columns rather than a predicate per column, which is what a
|
|
58
|
-
* reader means by a search box above a table. Per-column predicates are a
|
|
59
|
-
* different feature and compose with this rather than replacing it.
|
|
60
|
-
*
|
|
61
|
-
* Case-insensitive and substring, on the ordering value rather than the rendered
|
|
62
|
-
* one: a cell showing a formatted date should be findable by the text a reader
|
|
63
|
-
* sees, and a cell showing an icon should be findable by the value behind it.
|
|
64
|
-
* Where those differ, `compare` on the column is what decides, which is the same
|
|
65
|
-
* function sorting uses, so search and sort cannot disagree about what a column
|
|
66
|
-
* holds.
|
|
67
|
-
*/
|
|
68
19
|
export const filter = (rows, query, axes) => {
|
|
69
20
|
const needle = query.trim().toLocaleLowerCase();
|
|
70
21
|
if (needle === '') {
|
|
@@ -72,16 +23,6 @@ export const filter = (rows, query, axes) => {
|
|
|
72
23
|
}
|
|
73
24
|
return rows.filter((row) => axes.some((axis) => text(axis.order(row)).toLocaleLowerCase().includes(needle)));
|
|
74
25
|
};
|
|
75
|
-
/**
|
|
76
|
-
* Rows in the asked-for order, stably.
|
|
77
|
-
*
|
|
78
|
-
* **Stable, and that is load-bearing rather than tidy.** `Array.prototype.sort`
|
|
79
|
-
* is required to be stable since ES2019, so equal rows keep the order they
|
|
80
|
-
* arrived in, which means sorting by one column and then another leaves the
|
|
81
|
-
* first as the tiebreak. A reader who sorts by status and then by name expects
|
|
82
|
-
* exactly that, and an unstable sort would shuffle equal rows on every
|
|
83
|
-
* re-render, which reads as the table twitching.
|
|
84
|
-
*/
|
|
85
26
|
export const sort = (rows, spec, axes) => {
|
|
86
27
|
const axis = spec ? axes.find((one) => one.field === spec.field) : undefined;
|
|
87
28
|
if (!spec || !axis) {
|
|
@@ -90,14 +31,6 @@ export const sort = (rows, spec, axes) => {
|
|
|
90
31
|
const sign = spec.direction === 'asc' ? 1 : -1;
|
|
91
32
|
return [...rows].sort((left, right) => compare(axis.order(left), axis.order(right), sign));
|
|
92
33
|
};
|
|
93
|
-
/**
|
|
94
|
-
* One page of rows, clamped.
|
|
95
|
-
*
|
|
96
|
-
* A page past the end returns the last page rather than nothing, because the
|
|
97
|
-
* usual way to be past the end is a bookmarked link after the result shrank, and
|
|
98
|
-
* an empty table is a worse answer than the last page of a smaller one. The
|
|
99
|
-
* same clamp `helix-pagination` already applies to what it reports.
|
|
100
|
-
*/
|
|
101
34
|
export const window_ = (rows, page, size) => {
|
|
102
35
|
if (size <= 0) {
|
|
103
36
|
return [...rows];
|
|
@@ -107,12 +40,6 @@ export const window_ = (rows, page, size) => {
|
|
|
107
40
|
const from = (wanted - 1) * size;
|
|
108
41
|
return rows.slice(from, from + size);
|
|
109
42
|
};
|
|
110
|
-
/**
|
|
111
|
-
* A value as text, for matching.
|
|
112
|
-
*
|
|
113
|
-
* `null` and `undefined` become empty rather than their own names, so a search
|
|
114
|
-
* for "null" does not match every hole in the data.
|
|
115
|
-
*/
|
|
116
43
|
const text = (value) => {
|
|
117
44
|
if (value === null || value === undefined) {
|
|
118
45
|
return '';
|
|
@@ -122,26 +49,6 @@ const text = (value) => {
|
|
|
122
49
|
}
|
|
123
50
|
return String(value);
|
|
124
51
|
};
|
|
125
|
-
/**
|
|
126
|
-
* Two cell values ordered, with the types a table actually holds.
|
|
127
|
-
*
|
|
128
|
-
* **Numbers compare as numbers and strings collate.** `String(a) < String(b)`
|
|
129
|
-
* puts 10 before 9 and puts every accented word after every unaccented one, and
|
|
130
|
-
* both look like the sort is broken while every value in the column is correct.
|
|
131
|
-
* `localeCompare` handles the second, and the numeric branch handles the first.
|
|
132
|
-
*
|
|
133
|
-
* **Holes sort last in both directions**, which is deliberate and is the one
|
|
134
|
-
* choice here a reader could disagree with. A missing value is not smaller than
|
|
135
|
-
* a present one, it is absent, and burying the holes at the end is what a reader
|
|
136
|
-
* scanning for the largest or smallest actual value wants. Reversing them with
|
|
137
|
-
* the direction puts every hole first on a descending sort, which is the least
|
|
138
|
-
* useful screen the data can produce.
|
|
139
|
-
*
|
|
140
|
-
* **`sign` is applied in here rather than to the result**, and that is why it is
|
|
141
|
-
* a parameter rather than the caller's multiplication. Written the obvious way,
|
|
142
|
-
* as `sign * compare(a, b)`, the constant this returns for a hole flips with the
|
|
143
|
-
* direction and the holes lead a descending sort.
|
|
144
|
-
*/
|
|
145
52
|
const compare = (left, right, sign) => {
|
|
146
53
|
const leftMissing = left === null || left === undefined || left === '';
|
|
147
54
|
const rightMissing = right === null || right === undefined || right === '';
|
|
@@ -159,13 +66,6 @@ const compare = (left, right, sign) => {
|
|
|
159
66
|
}
|
|
160
67
|
return (sign * text(left).localeCompare(text(right), undefined, { numeric: true, sensitivity: 'base' }));
|
|
161
68
|
};
|
|
162
|
-
/**
|
|
163
|
-
* The whole pass, in the one order that is correct.
|
|
164
|
-
*
|
|
165
|
-
* Exported as a single function so a caller cannot get the order wrong by
|
|
166
|
-
* calling the three in the sequence that reads naturally, which is the sequence
|
|
167
|
-
* that pages before it sorts.
|
|
168
|
-
*/
|
|
169
69
|
export const ordered = (rows, axes, { page, query, size, spec }) => {
|
|
170
70
|
const matched = filter(rows, query, axes);
|
|
171
71
|
const arranged = sort(matched, parse(spec), axes);
|