@fusion.dev/helix-headless 0.166.0 → 0.168.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.
Files changed (38) hide show
  1. package/custom-elements.json +2658 -481
  2. package/dist/canvas/arrange.d.ts +32 -0
  3. package/dist/canvas/arrange.js +144 -0
  4. package/dist/canvas/paste.d.ts +25 -0
  5. package/dist/canvas/paste.js +58 -0
  6. package/dist/canvas/root/canvas-root.d.ts +60 -3
  7. package/dist/canvas/root/canvas-root.js +166 -2
  8. package/dist/chat/composer/chat-composer.js +5 -1
  9. package/dist/combobox/root/combobox-root.d.ts +6 -1
  10. package/dist/combobox/root/combobox-root.js +2 -2
  11. package/dist/date-input/root/date-input-root.d.ts +1 -1
  12. package/dist/element-events.generated.js +2 -0
  13. package/dist/file-input/root/file-input-root.d.ts +1 -1
  14. package/dist/file-input/root/file-input-root.js +1 -1
  15. package/dist/format-date/root/format-date-root.d.ts +10 -3
  16. package/dist/format-date/root/format-date-root.js +6 -1
  17. package/dist/listbox/root/listbox-root.d.ts +12 -1
  18. package/dist/listbox/root/listbox-root.js +13 -1
  19. package/dist/select/root/select-root.js +1 -1
  20. package/dist/table/grid-controller.d.ts +10 -0
  21. package/dist/table/grid-controller.js +7 -0
  22. package/dist/table/root/table-root.d.ts +71 -1
  23. package/dist/table/root/table-root.js +122 -7
  24. package/dist/table/row/table-row.d.ts +17 -0
  25. package/dist/table/row/table-row.js +23 -0
  26. package/dist/time-input/root/time-input-root.d.ts +2 -1
  27. package/dist/time-input/root/time-input-root.js +1 -1
  28. package/dist/time-picker/root/time-picker-root.d.ts +48 -1
  29. package/dist/time-picker/root/time-picker-root.js +79 -21
  30. package/dist/virtual-list/root/virtual-list-root.d.ts +70 -2
  31. package/dist/virtual-list/root/virtual-list-root.js +216 -32
  32. package/dist/virtual-list/sections.d.ts +27 -0
  33. package/dist/virtual-list/sections.js +43 -0
  34. package/lint.json +1 -1
  35. package/package.json +5 -5
  36. package/types/jsx.d.ts +454 -76
  37. package/types/vue.d.ts +89 -7
  38. package/types/words.d.ts +27 -0
@@ -11,6 +11,13 @@ import { holdControls, releaseControls } from '../controls.js';
11
11
  import { fillHoles, keyOf, textsOf } from '../fill.js';
12
12
  import { skipDisabled, stepIndex } from '../keys.js';
13
13
  import { drawPlan, itemsOf, stopAfter } from '../plan.js';
14
+ import { entryOf, headOf, itemsIn, sectionAt, sectionsOf } from '../sections.js';
15
+ let sectionCount = 0;
16
+ const nextSectionId = () => {
17
+ sectionCount += 1;
18
+ return `virtual-list-${sectionCount}`;
19
+ };
20
+ const ROWS = ':scope > [data-index], :scope > [data-section] > [data-index]';
14
21
  let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixElement {
15
22
  constructor() {
16
23
  super(...arguments);
@@ -26,6 +33,8 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
26
33
  this.loadingLabel = 'Loading more';
27
34
  this.measured = false;
28
35
  this.minColumnWidth = 0;
36
+ this.orientation = 'vertical';
37
+ this.sectionField = '';
29
38
  this.selectionBehavior = 'toggle';
30
39
  this.textField = '';
31
40
  this.value = '';
@@ -41,9 +50,12 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
41
50
  });
42
51
  this.virtualize = new VirtualizeController(this, {
43
52
  container: () => this.partNamed('viewport'),
44
- count: () => Math.ceil(this.data.length / this.across),
53
+ count: () => this.sections
54
+ ? this.data.length + this.sections.starts.length
55
+ : Math.ceil(this.data.length / this.across),
45
56
  estimate: () => this.estimate,
46
57
  gap: () => this.space,
58
+ horizontal: () => this.horizontal,
47
59
  measured: () => this.measured,
48
60
  rows: () => this.lines(),
49
61
  });
@@ -94,13 +106,23 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
94
106
  }
95
107
  const count = this.data.length;
96
108
  const { first, last } = this.virtualize.visible();
97
- const stepped = stepIndex(event.key, at, count, last - first - 1, {
109
+ const forward = isRtl(this) ? 'ArrowLeft' : 'ArrowRight';
110
+ const key = !this.horizontal
111
+ ? event.key
112
+ : event.key === forward
113
+ ? 'ArrowDown'
114
+ : event.key === 'ArrowLeft' || event.key === 'ArrowRight'
115
+ ? 'ArrowUp'
116
+ : event.key === 'ArrowDown' || event.key === 'ArrowUp'
117
+ ? ''
118
+ : event.key;
119
+ const stepped = stepIndex(key, at, count, last - first - 1, {
98
120
  across: this.across,
99
- forward: isRtl(this) ? 'ArrowLeft' : 'ArrowRight',
121
+ forward,
100
122
  });
101
123
  const next = stepped === undefined
102
124
  ? undefined
103
- : skipDisabled(event.key, at, stepped, count, (one) => this.isDisabled(one), this.across);
125
+ : skipDisabled(key, at, stepped, count, (one) => this.isDisabled(one), this.across);
104
126
  if (next === undefined) {
105
127
  if (stepped !== undefined) {
106
128
  event.preventDefault();
@@ -122,7 +144,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
122
144
  return;
123
145
  }
124
146
  const path = event.composedPath();
125
- const row = path.find((node) => node instanceof HTMLElement && node.parentElement === this);
147
+ const row = path.find((node) => this.isRow(node));
126
148
  const at = this.indexOf(row ?? null);
127
149
  if (at === undefined) {
128
150
  return;
@@ -143,7 +165,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
143
165
  };
144
166
  this.handleFocusIn = (event) => {
145
167
  let node = event.target;
146
- while (node && node.parentNode !== this) {
168
+ while (node && node !== this && !this.isRow(node)) {
147
169
  node = node.parentNode;
148
170
  }
149
171
  const index = this.indexOf(node);
@@ -152,6 +174,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
152
174
  }
153
175
  this.requestUpdate();
154
176
  };
177
+ this.sectionId = nextSectionId();
155
178
  }
156
179
  static { this.words = 'helix-virtual-list'; }
157
180
  get range() {
@@ -186,7 +209,32 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
186
209
  this.widthWatch.observe(viewport);
187
210
  }
188
211
  itemsOn(lines) {
189
- return itemsOf(lines, this.across, this.data.length);
212
+ const sections = this.sections;
213
+ return sections
214
+ ? itemsIn(sections, lines, this.data.length)
215
+ : itemsOf(lines, this.across, this.data.length);
216
+ }
217
+ lineOf(at) {
218
+ const sections = this.sections;
219
+ return sections ? entryOf(sections, at) : Math.floor(at / this.across);
220
+ }
221
+ get horizontal() {
222
+ return this.orientation === 'horizontal' && this.across === 1;
223
+ }
224
+ get sections() {
225
+ const data = this.data;
226
+ if (this.sectionField === '' || data.length === 0 || this.across !== 1) {
227
+ return undefined;
228
+ }
229
+ const read = this.sectionsRead;
230
+ if (read?.data !== data || read.field !== this.sectionField) {
231
+ this.sectionsRead = {
232
+ data,
233
+ field: this.sectionField,
234
+ sections: sectionsOf(data, this.sectionField),
235
+ };
236
+ }
237
+ return this.sectionsRead?.sections;
190
238
  }
191
239
  scrollToIndex(index, options = {}) {
192
240
  const count = this.data.length;
@@ -194,14 +242,14 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
194
242
  return;
195
243
  }
196
244
  const at = Math.max(0, Math.min(Math.trunc(index), count - 1));
197
- this.virtualize.reveal(Math.floor(at / this.across), 0, options);
245
+ this.virtualize.reveal(this.lineOf(at), 0, options);
198
246
  this.virtualize.measure();
199
247
  }
200
248
  get data() {
201
249
  return this.items ?? this.rows;
202
250
  }
203
251
  drawn() {
204
- return [...this.querySelectorAll(':scope > [data-index]')].map((element) => ({
252
+ return [...this.querySelectorAll(ROWS)].map((element) => ({
205
253
  element,
206
254
  index: Number(element.dataset.index),
207
255
  }));
@@ -209,6 +257,13 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
209
257
  lines() {
210
258
  const across = this.across;
211
259
  const drawn = this.drawn();
260
+ const sections = this.sections;
261
+ if (sections) {
262
+ return [
263
+ ...drawn.map(({ element, index }) => ({ element, index: entryOf(sections, index) })),
264
+ ...[...this.querySelectorAll(':scope > [data-section] > [data-entry]')].map((element) => ({ element, index: Number(element.dataset.entry) })),
265
+ ];
266
+ }
212
267
  return across === 1
213
268
  ? drawn
214
269
  : drawn
@@ -226,6 +281,9 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
226
281
  this.measuredAcross = across;
227
282
  this.virtualize.forget();
228
283
  }
284
+ if (this.hasUpdated && (changed.has('orientation') || changed.has('sectionField'))) {
285
+ this.virtualize.forget();
286
+ }
229
287
  if (this.hasUpdated) {
230
288
  if (changed.has('items')) {
231
289
  this.keepPlace(changed.get('items'));
@@ -245,10 +303,13 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
245
303
  if (!same(by, 0) || !same(now.length - 1, was.length - 1)) {
246
304
  return;
247
305
  }
248
- if (by % this.across === 0) {
306
+ if (this.sections) {
307
+ this.virtualize.shift(this.lineOf(by) - 1);
308
+ }
309
+ else if (by % this.across === 0) {
249
310
  this.virtualize.shift(by / this.across);
250
311
  }
251
- for (const row of this.querySelectorAll(':scope > [data-index]')) {
312
+ for (const row of this.querySelectorAll(ROWS)) {
252
313
  row.dataset.index = String(Number(row.dataset.index) + by);
253
314
  }
254
315
  this.active += by;
@@ -324,6 +385,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
324
385
  super.updated?.(changed);
325
386
  this.toggleState('empty', this.readFor !== undefined && this.data.length === 0);
326
387
  this.toggleState('loading', this.loading);
388
+ this.toggleState('horizontal', this.horizontal);
327
389
  this.watchWidth();
328
390
  const was = this.virtualize.window;
329
391
  this.virtualize.measure();
@@ -376,14 +438,18 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
376
438
  return this.disabledKeys !== '' && this.disabled.has(this.keyAt(at));
377
439
  }
378
440
  indexOf(node) {
379
- if (!(node instanceof HTMLElement) || node.parentElement !== this) {
441
+ if (!this.isRow(node)) {
380
442
  return undefined;
381
443
  }
382
444
  const index = node.dataset.index;
383
445
  return index === undefined ? undefined : Number(index);
384
446
  }
447
+ isRow(node) {
448
+ const parent = node instanceof HTMLElement ? node.parentElement : null;
449
+ return (parent === this || (parent?.parentElement === this && parent.hasAttribute('data-section')));
450
+ }
385
451
  rowFor(index) {
386
- return this.querySelector(`:scope > [data-index="${index}"]`);
452
+ return ([...this.querySelectorAll(ROWS)].find((row) => row.dataset.index === String(index)) ?? null);
387
453
  }
388
454
  keyAt(at) {
389
455
  return keyOf(this.data[at], at, this.keyField);
@@ -437,7 +503,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
437
503
  async focusIndex(index) {
438
504
  this.active = index;
439
505
  this.focusing = index;
440
- this.virtualize.reveal(Math.floor(index / this.across));
506
+ this.virtualize.reveal(this.lineOf(index));
441
507
  this.virtualize.measure();
442
508
  this.requestUpdate();
443
509
  try {
@@ -451,7 +517,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
451
517
  focusedIndex() {
452
518
  const root = this.getRootNode();
453
519
  let node = root.activeElement;
454
- while (node && node.parentElement !== this) {
520
+ while (node && node !== this && !this.isRow(node)) {
455
521
  node = node.parentElement;
456
522
  }
457
523
  const index = node instanceof HTMLElement ? node.dataset.index : undefined;
@@ -518,10 +584,20 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
518
584
  const onScreen = this.range.first;
519
585
  if (this.hasUpdated) {
520
586
  const spacers = this.spacerSizes();
521
- this.partNamed('before')?.style.setProperty('block-size', `${spacers.before}px`);
522
- this.partNamed('after')?.style.setProperty('block-size', `${spacers.after}px`);
523
- }
524
- const children = [...this.children].filter((child) => child !== template && child instanceof HTMLElement && !child.hasAttribute('slot'));
587
+ const size = this.horizontal ? 'inline-size' : 'block-size';
588
+ this.partNamed('before')?.style.setProperty(size, `${spacers.before}px`);
589
+ this.partNamed('after')?.style.setProperty(size, `${spacers.after}px`);
590
+ }
591
+ const sections = this.sections;
592
+ const regroup = sections !== this.placedSections;
593
+ this.placedSections = sections;
594
+ const children = [
595
+ ...[...this.children].filter((child) => child !== template &&
596
+ child instanceof HTMLElement &&
597
+ !child.hasAttribute('slot') &&
598
+ !child.hasAttribute('data-section')),
599
+ ...this.querySelectorAll(':scope > [data-section] > [data-index]'),
600
+ ];
525
601
  const plan = drawPlan({
526
602
  across: this.across,
527
603
  count,
@@ -529,7 +605,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
529
605
  first,
530
606
  kept: this.keptIndex(),
531
607
  last,
532
- redraw: (this.filledBy === undefined) !== (this.renderItem === undefined),
608
+ redraw: regroup || (this.filledBy === undefined) !== (this.renderItem === undefined),
533
609
  });
534
610
  const drawn = new Map();
535
611
  for (const child of children) {
@@ -541,26 +617,34 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
541
617
  }
542
618
  drawn.set(index, [...(drawn.get(index) ?? []), child]);
543
619
  }
544
- const refill = this.filledFrom !== this.data || this.filledBy !== this.renderItem;
620
+ const refill = this.filledFrom !== this.data ||
621
+ this.filledBy !== this.renderItem ||
622
+ this.headedBy !== this.renderSection;
545
623
  this.filledFrom = this.data;
546
624
  this.filledBy = this.renderItem;
625
+ this.headedBy = this.renderSection;
626
+ const groups = this.groups(sections, plan.wanted, regroup, refill);
547
627
  const fresh = new Set();
548
- let anchor = drawn.values().next().value?.[0] ?? null;
628
+ const anchors = new Map();
549
629
  for (const at of plan.wanted) {
630
+ const parent = (sections && groups.get(sectionAt(sections, at))) || this;
631
+ if (!anchors.has(parent)) {
632
+ anchors.set(parent, parent.querySelector(':scope > [data-index]'));
633
+ }
550
634
  const group = drawn.get(at);
551
635
  if (group) {
552
636
  if (refill) {
553
637
  this.fill(group, at);
554
638
  fresh.add(at);
555
639
  }
556
- anchor = group.at(-1).nextElementSibling;
640
+ anchors.set(parent, group.at(-1).nextElementSibling);
557
641
  continue;
558
642
  }
559
643
  const fragment = this.draw(template, at);
560
644
  drawn.set(at, [...fragment.children]);
561
645
  this.watchSize([...fragment.children]);
562
646
  fresh.add(at);
563
- this.insertBefore(fragment, anchor);
647
+ parent.insertBefore(fragment, anchors.get(parent) ?? null);
564
648
  }
565
649
  this.active = stopAfter(this.active, plan.wanted, {
566
650
  count,
@@ -594,13 +678,100 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
594
678
  }, 0);
595
679
  }
596
680
  }
681
+ groups(sections, wanted, regroup, refill) {
682
+ const keep = new Set(sections ? wanted.map((at) => sectionAt(sections, at)) : []);
683
+ const groups = new Map();
684
+ for (const group of this.querySelectorAll(':scope > [data-section]')) {
685
+ const at = Number(group.dataset.section);
686
+ if (regroup || !keep.has(at)) {
687
+ this.sizes?.unobserve(group.firstElementChild);
688
+ group.remove();
689
+ }
690
+ else {
691
+ groups.set(at, group);
692
+ }
693
+ }
694
+ if (!sections) {
695
+ return groups;
696
+ }
697
+ for (const at of [...keep].sort((a, b) => a - b)) {
698
+ let group = groups.get(at);
699
+ const fresh = group === undefined;
700
+ if (!group) {
701
+ group = document.createElement('div');
702
+ const header = document.createElement('div');
703
+ header.id = `${this.sectionId}-${at}`;
704
+ header.dataset.entry = String(headOf(sections, at));
705
+ header.setAttribute('data-header', '');
706
+ group.dataset.section = String(at);
707
+ group.setAttribute('role', 'group');
708
+ group.setAttribute('aria-labelledby', header.id);
709
+ group.append(header);
710
+ const after = [...groups].find(([other]) => other > at)?.[1] ?? null;
711
+ this.insertBefore(group, after);
712
+ groups.set(at, group);
713
+ this.watchSize([header]);
714
+ }
715
+ const header = group.firstElementChild;
716
+ if (fresh || refill) {
717
+ const name = sections.names[at] ?? '';
718
+ if (this.renderSection) {
719
+ render(this.renderSection(name, at), header);
720
+ }
721
+ else {
722
+ header.textContent = name;
723
+ }
724
+ }
725
+ if (this.selectionMode) {
726
+ header.setAttribute('role', 'presentation');
727
+ }
728
+ else {
729
+ header.removeAttribute('role');
730
+ }
731
+ const gap = this.space;
732
+ group.style.cssText = this.horizontal
733
+ ? `display: flex; column-gap: ${gap}px`
734
+ : gap === 0
735
+ ? ''
736
+ : `display: flex; flex-direction: column; row-gap: ${gap}px`;
737
+ }
738
+ return groups;
739
+ }
597
740
  spacerSizes() {
598
741
  const kept = this.hasUpdated ? this.keptIndex() : undefined;
599
- return this.virtualize.spacers(kept === undefined ? undefined : Math.floor(kept / this.across));
742
+ const room = this.virtualize.spacers(kept === undefined ? undefined : this.lineOf(kept));
743
+ const sections = this.sections;
744
+ if (!sections || !this.hasUpdated) {
745
+ return room;
746
+ }
747
+ const { first, last } = this.virtualize.window;
748
+ const items = this.itemsOn(this.virtualize.window);
749
+ const drawnIn = new Set();
750
+ if (items.last > items.first) {
751
+ drawnIn.add(sectionAt(sections, items.first));
752
+ }
753
+ if (kept !== undefined) {
754
+ drawnIn.add(sectionAt(sections, kept));
755
+ }
756
+ let { after, before } = room;
757
+ for (const section of drawnIn) {
758
+ const head = headOf(sections, section);
759
+ if (head < first) {
760
+ before = Math.max(0, before - this.virtualize.sizeOf(head));
761
+ }
762
+ else if (head >= last) {
763
+ after = Math.max(0, after - this.virtualize.sizeOf(head));
764
+ }
765
+ }
766
+ return { after, before };
600
767
  }
601
768
  render() {
602
769
  const { after, before } = this.spacerSizes();
603
770
  const across = this.across;
771
+ const horizontal = this.horizontal;
772
+ const size = horizontal ? 'inline-size' : 'block-size';
773
+ const flex = horizontal ? '; flex: none' : '';
774
+ const gap = this.space;
604
775
  const empty = this.readFor !== undefined &&
605
776
  !this.loading &&
606
777
  this.data.length === 0 &&
@@ -612,20 +783,24 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
612
783
  aria-multiselectable=${this.selectionMode === 'multiple' ? 'true' : nothing}
613
784
  aria-busy=${this.loading ? 'true' : nothing}
614
785
  aria-label=${this.label}
786
+ aria-orientation=${this.selectionMode && horizontal ? 'horizontal' : nothing}
787
+ style=${horizontal ? 'display: flex' : nothing}
615
788
  @scroll=${() => {
616
789
  this.virtualize.measure();
617
790
  this.report();
618
791
  }}
619
792
  >
620
- <div part="before" style="block-size: ${before}px"></div>
793
+ <div part="before" style="${size}: ${before}px${flex}"></div>
621
794
  <slot
622
- style=${across === 1
623
- ? this.space === 0
624
- ? nothing
625
- : `display: flex; flex-direction: column; row-gap: ${this.space}px; padding-block-end: ${this.space}px`
626
- : `display: grid; grid-template-columns: repeat(${across}, minmax(0, 1fr))${this.space === 0 ? '' : `; gap: ${this.space}px; padding-block-end: ${this.space}px`}`}
795
+ style=${horizontal
796
+ ? `display: flex; flex: none${gap === 0 ? '' : `; column-gap: ${gap}px; padding-inline-end: ${gap}px`}`
797
+ : across === 1
798
+ ? gap === 0
799
+ ? nothing
800
+ : `display: flex; flex-direction: column; row-gap: ${gap}px; padding-block-end: ${gap}px`
801
+ : `display: grid; grid-template-columns: repeat(${across}, minmax(0, 1fr))${gap === 0 ? '' : `; gap: ${gap}px; padding-block-end: ${gap}px`}`}
627
802
  ></slot>
628
- <div part="after" style="block-size: ${after}px"></div>
803
+ <div part="after" style="${size}: ${after}px${flex}"></div>
629
804
  ${this.loading
630
805
  ? html `<div part="loading" role=${this.selectionMode ? 'option' : 'listitem'}>
631
806
  <slot name="loading">${this.loadingLabel}</slot>
@@ -679,9 +854,18 @@ __decorate([
679
854
  __decorate([
680
855
  property({ attribute: 'min-column-width', type: Number })
681
856
  ], HelixHeadlessVirtualList.prototype, "minColumnWidth", void 0);
857
+ __decorate([
858
+ property()
859
+ ], HelixHeadlessVirtualList.prototype, "orientation", void 0);
682
860
  __decorate([
683
861
  property({ attribute: false })
684
862
  ], HelixHeadlessVirtualList.prototype, "renderItem", void 0);
863
+ __decorate([
864
+ property({ attribute: false })
865
+ ], HelixHeadlessVirtualList.prototype, "renderSection", void 0);
866
+ __decorate([
867
+ property({ attribute: 'section-field' })
868
+ ], HelixHeadlessVirtualList.prototype, "sectionField", void 0);
685
869
  __decorate([
686
870
  property({ attribute: 'selection-behavior' })
687
871
  ], HelixHeadlessVirtualList.prototype, "selectionBehavior", void 0);
@@ -0,0 +1,27 @@
1
+ /** Where each section starts in the items, and what it is called. */
2
+ export interface Sections {
3
+ /** The section names, in order. */
4
+ names: string[];
5
+ /** The index of each section's first item, in order. */
6
+ starts: number[];
7
+ }
8
+ /** The sections `field` divides the items into. */
9
+ export declare const sectionsOf: (items: readonly unknown[], field: string) => Sections;
10
+ /** The section item `item` is in. */
11
+ export declare const sectionAt: ({ starts }: Sections, item: number) => number;
12
+ /** The entry item `item` is. */
13
+ export declare const entryOf: (sections: Sections, item: number) => number;
14
+ /** The entry of section `section`'s header. */
15
+ export declare const headOf: ({ starts }: Sections, section: number) => number;
16
+ /**
17
+ * The items a span of entries holds: what a window of entries draws, or what
18
+ * a reader sees. A header at either end holds no item, so the span starts at
19
+ * its section's first item.
20
+ */
21
+ export declare const itemsIn: (sections: Sections, entries: {
22
+ first: number;
23
+ last: number;
24
+ }, count: number) => {
25
+ first: number;
26
+ last: number;
27
+ };
@@ -0,0 +1,43 @@
1
+ import { valueAt } from '@fusion.dev/helix-core';
2
+ import { textOf } from './fill.js';
3
+ export const sectionsOf = (items, field) => {
4
+ const names = [];
5
+ const starts = [];
6
+ let last;
7
+ for (const [at, item] of items.entries()) {
8
+ const name = textOf(valueAt(item, field));
9
+ if (at === 0 || name !== last) {
10
+ names.push(name);
11
+ starts.push(at);
12
+ last = name;
13
+ }
14
+ }
15
+ return { names, starts };
16
+ };
17
+ const lastUpTo = (length, at, target) => {
18
+ let low = 0;
19
+ let high = length - 1;
20
+ while (low < high) {
21
+ const middle = Math.ceil((low + high) / 2);
22
+ if (at(middle) <= target) {
23
+ low = middle;
24
+ }
25
+ else {
26
+ high = middle - 1;
27
+ }
28
+ }
29
+ return low;
30
+ };
31
+ export const sectionAt = ({ starts }, item) => lastUpTo(starts.length, (index) => starts[index] ?? 0, item);
32
+ export const entryOf = (sections, item) => item + sectionAt(sections, item) + 1;
33
+ export const headOf = ({ starts }, section) => (starts[section] ?? 0) + section;
34
+ export const itemsIn = (sections, entries, count) => {
35
+ const item = (entry) => {
36
+ if (entry >= count + sections.starts.length) {
37
+ return count;
38
+ }
39
+ const section = lastUpTo(sections.starts.length, (index) => headOf(sections, index), entry);
40
+ return Math.max(sections.starts[section] ?? 0, entry - section - 1);
41
+ };
42
+ return { first: item(entries.first), last: Math.max(item(entries.first), item(entries.last)) };
43
+ };