@fusion.dev/helix-core 0.148.0 → 0.149.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 (70) hide show
  1. package/custom-elements.json +8518 -6050
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +1 -1
  3. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +2 -2
  4. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +5 -4
  5. package/dist/controllers/machine-controller/machine-controller.d.ts +69 -0
  6. package/dist/controllers/machine-controller/machine-controller.js +78 -0
  7. package/dist/controllers/machine-controller/machine.d.ts +93 -0
  8. package/dist/controllers/machine-controller/machine.js +20 -0
  9. package/dist/controllers/reorder-controller/reorder-controller.d.ts +285 -0
  10. package/dist/controllers/reorder-controller/reorder-controller.js +497 -0
  11. package/dist/controllers/source-controller/source-controller.js +1 -1
  12. package/dist/controllers/virtualize-controller/heights.js +1 -1
  13. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +117 -3
  14. package/dist/controllers/virtualize-controller/virtualize-controller.js +134 -32
  15. package/dist/events/before-reorder.d.ts +14 -0
  16. package/dist/events/before-reorder.js +11 -0
  17. package/dist/events/details.d.ts +28 -0
  18. package/dist/events/events.d.ts +4 -2
  19. package/dist/events/events.js +3 -1
  20. package/dist/events/names.js +1 -1
  21. package/dist/events/reorder.d.ts +18 -12
  22. package/dist/events/reordered.d.ts +4 -4
  23. package/dist/helix-element/helix-element.js +2 -1
  24. package/dist/helix-element/react-audit-off.d.ts +11 -0
  25. package/dist/helix-element/react-audit-off.js +1 -0
  26. package/dist/helix-element/react-audit.d.ts +20 -0
  27. package/dist/helix-element/react-audit.js +56 -0
  28. package/dist/helix-element/react-bridge.d.ts +2 -22
  29. package/dist/helix-element/react-bridge.js +1 -56
  30. package/dist/index.d.ts +10 -4
  31. package/dist/index.js +10 -4
  32. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +2 -2
  33. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +3 -0
  34. package/dist/mixins/mixin-collection/mixin-collection.js +4 -0
  35. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +3 -0
  36. package/dist/mixins/mixin-field/mixin-field.js +1 -1
  37. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +12 -0
  38. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +15 -2
  39. package/dist/mixins/mixin-tokens/mixin-tokens.d.ts +5 -0
  40. package/dist/mixins/mixin-tokens/mixin-tokens.js +13 -2
  41. package/dist/mixins/mixin-viewport/mixin-viewport.d.ts +17 -15
  42. package/dist/mixins/mixin-viewport/mixin-viewport.js +15 -34
  43. package/dist/utils/calendar/calendar.d.ts +27 -0
  44. package/dist/utils/calendar/calendar.js +17 -0
  45. package/dist/utils/count-label/count-label.d.ts +7 -4
  46. package/dist/utils/count-label/count-label.js +2 -5
  47. package/dist/utils/date-segments/date-segments.d.ts +2 -1
  48. package/dist/utils/date-segments/date-segments.js +10 -0
  49. package/dist/utils/locale-of/locale-of.d.ts +5 -0
  50. package/dist/utils/locale-of/locale-of.js +8 -2
  51. package/dist/utils/reorder/drag-preview.d.ts +46 -0
  52. package/dist/utils/reorder/drag-preview.js +58 -0
  53. package/dist/utils/reorder/reorder.d.ts +178 -0
  54. package/dist/utils/reorder/reorder.js +112 -0
  55. package/dist/utils/report-runtime/report-runtime.d.ts +7 -4
  56. package/dist/utils/slotted-words/slotted-words.d.ts +3 -2
  57. package/dist/utils/slotted-words/slotted-words.js +3 -1
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/dist/words/index.d.ts +3 -0
  61. package/dist/words/index.js +3 -0
  62. package/dist/words/message.d.ts +47 -0
  63. package/dist/words/message.js +152 -0
  64. package/dist/words/registry.d.ts +89 -0
  65. package/dist/words/registry.js +101 -0
  66. package/dist/words/words-locale.d.ts +34 -0
  67. package/dist/words/words-locale.js +30 -0
  68. package/dist/words/words.d.ts +52 -0
  69. package/dist/words/words.js +100 -0
  70. package/package.json +40 -25
@@ -0,0 +1,497 @@
1
+ import { HelixBeforeReorderEvent } from '../../events/before-reorder.js';
2
+ import { HelixReorderEvent } from '../../events/reorder.js';
3
+ import { deepActiveElement } from '../../utils/focusable-elements/focusable-elements.js';
4
+ import { isRtl } from '../../utils/is-rtl/is-rtl.js';
5
+ import { boxOf, dragPreviewOf, drawDragPreview } from '../../utils/reorder/drag-preview.js';
6
+ import { centersOf, dropMark, edgeSpeed, IDLE, passedCount, placeKey, reorderKey, reorderStep, } from '../../utils/reorder/reorder.js';
7
+ import { scrollingAncestor } from '../../utils/scroll-item-into-view/scroll-item-into-view.js';
8
+ import { formatMessage } from '../../words/message.js';
9
+ import { formatWords } from '../../words/words.js';
10
+ import { wordsLocale } from '../../words/words-locale.js';
11
+ const SLOP = 4;
12
+ export const requestReorder = (host, detail) => {
13
+ const asked = new HelixBeforeReorderEvent(detail);
14
+ host.dispatchEvent(asked);
15
+ if (asked.defaultPrevented) {
16
+ return false;
17
+ }
18
+ const told = new HelixReorderEvent({ ...detail, keys: [...detail.keys] });
19
+ host.dispatchEvent(told);
20
+ return !told.defaultPrevented;
21
+ };
22
+ export class ReorderController {
23
+ constructor(host, options) {
24
+ this.state = IDLE;
25
+ this.holder = null;
26
+ this.pointer = { x: 0, y: 0 };
27
+ this.frame = 0;
28
+ this.shown = undefined;
29
+ this.anchor = { x: 0, y: 0 };
30
+ this.awaiting = undefined;
31
+ this.handleDown = (event) => {
32
+ if (this.state.phase === 'held' && this.state.by === 'keyboard') {
33
+ this.press();
34
+ return;
35
+ }
36
+ if (event.button !== 0 || !this.enabled() || this.state.phase !== 'idle') {
37
+ return;
38
+ }
39
+ const grip = this.options.grip(event);
40
+ const from = grip ? this.options.keys(grip.key).indexOf(grip.key) : -1;
41
+ if (!grip || from < 0) {
42
+ return;
43
+ }
44
+ if (this.options.holds !== false) {
45
+ event.preventDefault();
46
+ }
47
+ this.pointer = { x: event.clientX, y: event.clientY };
48
+ if (this.send({
49
+ from,
50
+ key: grip.key,
51
+ pointer: event.pointerId,
52
+ type: 'press',
53
+ x: event.clientX,
54
+ y: event.clientY,
55
+ })) {
56
+ this.listen(true);
57
+ }
58
+ };
59
+ this.handleMove = (event) => {
60
+ const state = this.state;
61
+ if ((state.phase !== 'pressed' && !(state.phase === 'held' && state.by === 'pointer')) ||
62
+ (state.phase === 'pressed' && event.pointerId !== state.pointer)) {
63
+ return;
64
+ }
65
+ this.pointer = { x: event.clientX, y: event.clientY };
66
+ this.drag();
67
+ this.glide();
68
+ };
69
+ this.handleUp = () => {
70
+ const dragged = this.state.phase === 'held';
71
+ this.send({ type: 'release' });
72
+ if (dragged) {
73
+ swallowClick();
74
+ }
75
+ };
76
+ this.handleCancel = () => {
77
+ this.send({ type: 'cancel' });
78
+ };
79
+ this.handleEscape = (event) => {
80
+ const state = this.state;
81
+ if (event.key === 'Escape' && state.phase === 'held' && state.by === 'pointer') {
82
+ event.preventDefault();
83
+ event.stopPropagation();
84
+ this.send({ type: 'cancel' });
85
+ window.addEventListener('pointerup', swallowClick, { once: true });
86
+ }
87
+ };
88
+ this.handleClick = (event) => {
89
+ if (this.options.holds === false || !this.enabled()) {
90
+ return;
91
+ }
92
+ const state = this.state;
93
+ const grip = this.options.grip(event);
94
+ if (state.phase === 'idle') {
95
+ const from = grip ? this.options.keys(grip.key).indexOf(grip.key) : -1;
96
+ if (grip && from >= 0) {
97
+ this.take(grip.element, { from, key: grip.key, type: 'pick' });
98
+ }
99
+ return;
100
+ }
101
+ if (state.phase !== 'held' || state.by !== 'keyboard') {
102
+ return;
103
+ }
104
+ const on = grip?.key ?? this.options.item?.(event);
105
+ const keys = this.options.keys(state.key);
106
+ if (on === undefined || !keys.includes(on)) {
107
+ return;
108
+ }
109
+ event.preventDefault();
110
+ event.stopPropagation();
111
+ if (on === state.key) {
112
+ this.send({ from: state.from, key: state.key, type: 'pick' });
113
+ return;
114
+ }
115
+ const to = event.detail > 0 ? this.placeAt(state.key, event.clientX, event.clientY) : keys.indexOf(on);
116
+ this.send({ to: Math.max(0, to), type: 'target' });
117
+ };
118
+ this.handleKey = (event) => {
119
+ if (event.ctrlKey || event.metaKey) {
120
+ return;
121
+ }
122
+ const state = this.state;
123
+ if (event.altKey) {
124
+ if (state.phase === 'idle' && !event.shiftKey) {
125
+ this.nudge(event);
126
+ }
127
+ return;
128
+ }
129
+ if (this.options.holds === false) {
130
+ return;
131
+ }
132
+ const origin = event.composedPath()[0];
133
+ const native = origin instanceof HTMLButtonElement;
134
+ const press = event.key === 'Enter' || event.key === ' ';
135
+ if (state.phase === 'idle') {
136
+ const grip = press && !native && this.enabled() ? this.options.grip(event) : undefined;
137
+ const from = grip ? this.options.keys(grip.key).indexOf(grip.key) : -1;
138
+ if (grip && from >= 0) {
139
+ event.preventDefault();
140
+ event.stopPropagation();
141
+ this.take(grip.element, { from, key: grip.key, type: 'pick' });
142
+ }
143
+ return;
144
+ }
145
+ if (state.phase !== 'held' || state.by !== 'keyboard') {
146
+ return;
147
+ }
148
+ if (event.key === 'Escape') {
149
+ event.preventDefault();
150
+ event.stopPropagation();
151
+ this.send({ type: 'cancel' });
152
+ return;
153
+ }
154
+ if (press) {
155
+ if (!native) {
156
+ event.preventDefault();
157
+ event.stopPropagation();
158
+ this.send({ type: 'drop' });
159
+ }
160
+ return;
161
+ }
162
+ const to = reorderKey(event.key, {
163
+ axis: this.options.axis,
164
+ count: this.options.keys(state.key).length,
165
+ rtl: isRtl(this.host),
166
+ to: state.to,
167
+ });
168
+ if (to !== null && !event.shiftKey) {
169
+ event.preventDefault();
170
+ event.stopPropagation();
171
+ this.send({ to, type: 'step' });
172
+ }
173
+ };
174
+ this.pressing = false;
175
+ this.handleFocusOut = () => {
176
+ if (this.state.phase === 'held' && this.state.by === 'keyboard') {
177
+ queueMicrotask(() => this.keepOrCancel());
178
+ }
179
+ };
180
+ this.host = host;
181
+ this.options = options;
182
+ host.addController(this);
183
+ }
184
+ get held() {
185
+ const state = this.state;
186
+ return state.phase === 'held'
187
+ ? { by: state.by, from: state.from, key: state.key, to: state.to }
188
+ : undefined;
189
+ }
190
+ get mark() {
191
+ const held = this.held;
192
+ const at = held && dropMark(held.from, held.to);
193
+ const key = at && held && this.options.keys(held.key)[at.index];
194
+ return at && key !== undefined ? { edge: at.edge, key } : undefined;
195
+ }
196
+ moveOf(key) {
197
+ const mark = this.mark;
198
+ return this.held?.key === key ? 'dragging' : mark?.key === key ? `drop-${mark.edge}` : undefined;
199
+ }
200
+ drawDragPreview() {
201
+ return drawDragPreview(this.enabled());
202
+ }
203
+ hostConnected() {
204
+ this.host.addEventListener('pointerdown', this.handleDown);
205
+ this.host.addEventListener('click', this.handleClick, true);
206
+ this.host.addEventListener('keydown', this.handleKey, true);
207
+ this.host.addEventListener('focusout', this.handleFocusOut);
208
+ }
209
+ hostDisconnected() {
210
+ this.host.removeEventListener('pointerdown', this.handleDown);
211
+ this.host.removeEventListener('click', this.handleClick, true);
212
+ this.host.removeEventListener('keydown', this.handleKey, true);
213
+ this.host.removeEventListener('focusout', this.handleFocusOut);
214
+ this.listen(false);
215
+ this.lower();
216
+ this.awaiting?.disconnect();
217
+ this.state = IDLE;
218
+ this.holder = null;
219
+ }
220
+ cancel() {
221
+ this.send({ type: 'cancel' });
222
+ }
223
+ send(input) {
224
+ const before = this.state;
225
+ const next = reorderStep(before, input);
226
+ if (!next) {
227
+ return false;
228
+ }
229
+ this.state = next;
230
+ this.host.requestUpdate();
231
+ this.enter(before, next);
232
+ return true;
233
+ }
234
+ enter(before, next) {
235
+ if (next.phase !== 'held') {
236
+ this.lower();
237
+ }
238
+ if (next.phase === 'held') {
239
+ if (before.phase !== 'held') {
240
+ if (next.by === 'keyboard') {
241
+ this.speak('pickedUp', next.key);
242
+ }
243
+ else {
244
+ if (before.phase === 'pressed') {
245
+ this.lift(next.key, before);
246
+ }
247
+ this.scroll();
248
+ }
249
+ }
250
+ else if (next.by === 'keyboard') {
251
+ this.speak('moved', next.key, next.to);
252
+ }
253
+ return;
254
+ }
255
+ if (next.phase === 'dropped') {
256
+ this.listen(false);
257
+ this.holder = null;
258
+ const moved = next.to !== next.from;
259
+ const stood = !moved || this.drop({ from: next.from, key: next.key, to: next.to });
260
+ this.speak(stood ? 'dropped' : 'canceled', next.key);
261
+ if (moved && stood && next.by === 'keyboard') {
262
+ this.follow(next.key);
263
+ }
264
+ this.send({ type: 'settle' });
265
+ return;
266
+ }
267
+ if (next.phase === 'canceled') {
268
+ this.listen(false);
269
+ this.holder = null;
270
+ this.speak('canceled', next.key);
271
+ this.send({ type: 'settle' });
272
+ return;
273
+ }
274
+ if (next.phase === 'idle') {
275
+ this.listen(false);
276
+ }
277
+ }
278
+ drop(move) {
279
+ if (this.options.drop) {
280
+ return this.options.drop(move);
281
+ }
282
+ const { key, to } = move;
283
+ const shown = this.options.keys(key);
284
+ const keys = placeKey(this.options.all?.(key) ?? shown, shown, key, to);
285
+ return requestReorder(this.host, {
286
+ ...move,
287
+ keys,
288
+ kind: this.options.kind ?? 'item',
289
+ to: keys.indexOf(key),
290
+ value: keys.join(','),
291
+ });
292
+ }
293
+ speak(which, key, to) {
294
+ const { name, words } = this.options;
295
+ const host = this.host;
296
+ const say = this.options.say ??
297
+ ('announced' in host
298
+ ? (said) => {
299
+ host.announced = said;
300
+ this.host.requestUpdate();
301
+ }
302
+ : undefined);
303
+ if (!say) {
304
+ return;
305
+ }
306
+ const values = {
307
+ count: this.options.keys(key).length,
308
+ name: name ? name(key) : key,
309
+ position: (to ?? 0) + 1,
310
+ };
311
+ say(words
312
+ ? formatMessage(words()[which], values, wordsLocale(this.host))
313
+ : formatWords(this.host, `${which}Label`, values));
314
+ }
315
+ placeAt(key, x, y) {
316
+ const block = this.options.axis === 'block';
317
+ const reversed = !block && isRtl(this.host);
318
+ const spans = this.options
319
+ .keys(key)
320
+ .filter((one) => one !== key)
321
+ .map((one) => {
322
+ const box = this.options.box(one);
323
+ return (box && (block ? { end: box.bottom, start: box.top } : { end: box.right, start: box.left }));
324
+ });
325
+ return passedCount(centersOf(spans, reversed), block ? y : x, reversed);
326
+ }
327
+ enabled() {
328
+ return this.options.enabled?.() ?? this.host.reorderable ?? true;
329
+ }
330
+ follow(key) {
331
+ const refocus = this.options.refocus;
332
+ this.awaiting?.disconnect();
333
+ const lost = () => {
334
+ const active = deepActiveElement();
335
+ return !active || active === document.body;
336
+ };
337
+ if (!refocus) {
338
+ return;
339
+ }
340
+ if (lost()) {
341
+ refocus(key);
342
+ return;
343
+ }
344
+ const watch = new MutationObserver(() => {
345
+ watch.disconnect();
346
+ if (lost()) {
347
+ refocus(key);
348
+ }
349
+ });
350
+ watch.observe(this.host, { childList: true, subtree: true });
351
+ this.awaiting = watch;
352
+ }
353
+ lift(key, from) {
354
+ const element = this.options.preview?.(key);
355
+ const root = this.host.renderRoot ?? this.host.shadowRoot;
356
+ const preview = root?.querySelector('[part="drag-preview"]');
357
+ if (!element || !preview) {
358
+ return;
359
+ }
360
+ const box = boxOf(element);
361
+ preview.style.cssText = `inset:auto;left:${box.left}px;top:${box.top}px;width:${box.width}px;height:${box.height}px;margin:0;padding:0;overflow:clip;pointer-events:none`;
362
+ preview.replaceChildren(...dragPreviewOf(element, true, box));
363
+ preview.showPopover?.();
364
+ this.shown = preview;
365
+ this.anchor = from;
366
+ this.glide();
367
+ }
368
+ glide() {
369
+ this.shown?.style.setProperty('translate', `${this.pointer.x - this.anchor.x}px ${this.pointer.y - this.anchor.y}px`);
370
+ }
371
+ lower() {
372
+ this.shown?.hidePopover?.();
373
+ this.shown?.replaceChildren();
374
+ this.shown = undefined;
375
+ }
376
+ drag() {
377
+ const state = this.state;
378
+ if (state.phase !== 'pressed' && state.phase !== 'held') {
379
+ return;
380
+ }
381
+ const { x, y } = this.pointer;
382
+ this.send({ slop: SLOP, to: this.placeAt(state.key, x, y), type: 'drag', x, y });
383
+ }
384
+ listen(on) {
385
+ if (on) {
386
+ window.addEventListener('pointermove', this.handleMove);
387
+ window.addEventListener('pointerup', this.handleUp);
388
+ window.addEventListener('pointercancel', this.handleCancel);
389
+ window.addEventListener('keydown', this.handleEscape, true);
390
+ return;
391
+ }
392
+ window.removeEventListener('pointermove', this.handleMove);
393
+ window.removeEventListener('pointerup', this.handleUp);
394
+ window.removeEventListener('pointercancel', this.handleCancel);
395
+ window.removeEventListener('keydown', this.handleEscape, true);
396
+ if (this.frame) {
397
+ cancelAnimationFrame(this.frame);
398
+ this.frame = 0;
399
+ }
400
+ }
401
+ scroll() {
402
+ const block = this.options.axis === 'block';
403
+ const scroller = this.options.scroller ??
404
+ (block
405
+ ? () => scrollingAncestor(this.host.firstElementChild ?? this.host, {
406
+ axis: 'y',
407
+ overflowing: true,
408
+ })
409
+ : undefined);
410
+ if (!scroller || this.frame || typeof requestAnimationFrame !== 'function') {
411
+ return;
412
+ }
413
+ const tick = () => {
414
+ this.frame = 0;
415
+ const state = this.state;
416
+ if (state.phase !== 'held' || state.by !== 'pointer') {
417
+ return;
418
+ }
419
+ const box = scroller();
420
+ const edges = box
421
+ ? box.getBoundingClientRect()
422
+ : { bottom: innerHeight, left: 0, right: innerWidth, top: 0 };
423
+ const speed = block
424
+ ? edgeSpeed(this.pointer.y, edges.top, edges.bottom)
425
+ : edgeSpeed(this.pointer.x, edges.left, edges.right);
426
+ if (speed !== 0) {
427
+ const by = block ? { top: speed } : { left: speed };
428
+ if (box) {
429
+ box.scrollBy(by);
430
+ }
431
+ else {
432
+ window.scrollBy(by);
433
+ }
434
+ this.drag();
435
+ }
436
+ this.frame = requestAnimationFrame(tick);
437
+ };
438
+ this.frame = requestAnimationFrame(tick);
439
+ }
440
+ take(element, input) {
441
+ if (deepActiveElement() !== element) {
442
+ element.focus();
443
+ }
444
+ this.holder = element;
445
+ this.send(input);
446
+ }
447
+ nudge(event) {
448
+ const key = this.enabled() ? this.options.current?.(event) : undefined;
449
+ const keys = key === undefined ? [] : this.options.keys(key);
450
+ const from = key === undefined ? -1 : keys.indexOf(key);
451
+ const to = key === undefined || from < 0
452
+ ? null
453
+ : reorderKey(event.key, {
454
+ axis: this.options.axis,
455
+ count: keys.length,
456
+ rtl: isRtl(this.host),
457
+ to: from,
458
+ });
459
+ if (key === undefined || to === null) {
460
+ return;
461
+ }
462
+ event.preventDefault();
463
+ event.stopPropagation();
464
+ if (to !== from) {
465
+ const stood = this.drop({ from, key, to });
466
+ this.speak(stood ? 'moved' : 'canceled', key, to);
467
+ if (stood) {
468
+ this.follow(key);
469
+ }
470
+ }
471
+ }
472
+ press() {
473
+ this.pressing = true;
474
+ window.addEventListener('pointerup', () => {
475
+ setTimeout(() => {
476
+ this.pressing = false;
477
+ this.keepOrCancel();
478
+ }, 0);
479
+ }, { once: true });
480
+ }
481
+ keepOrCancel() {
482
+ if (!this.pressing &&
483
+ this.state.phase === 'held' &&
484
+ this.state.by === 'keyboard' &&
485
+ deepActiveElement() !== this.holder) {
486
+ this.send({ type: 'cancel' });
487
+ }
488
+ }
489
+ }
490
+ const swallow = (event) => {
491
+ event.preventDefault();
492
+ event.stopPropagation();
493
+ };
494
+ const swallowClick = () => {
495
+ window.addEventListener('click', swallow, { capture: true, once: true });
496
+ setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 0);
497
+ };
@@ -1,4 +1,4 @@
1
- import { auditSource } from '#audit-source';
1
+ import { auditSource } from '@fusion.dev/helix-core/audits/source';
2
2
  import { elementById } from '../../utils/element-by-id/element-by-id.js';
3
3
  import { readRows, readSeries, seriesKey, } from '../../utils/read-series/read-series.js';
4
4
  const HIDDEN = new WeakMap();
@@ -13,7 +13,7 @@ export class Heights {
13
13
  if (index < 0 || !Number.isFinite(size) || size <= 0) {
14
14
  return false;
15
15
  }
16
- const rounded = Math.round(size);
16
+ const rounded = Math.round(size * 64) / 64;
17
17
  if (this.seen.get(index) === rounded) {
18
18
  return false;
19
19
  }
@@ -9,8 +9,13 @@ export interface VirtualizeOptions {
9
9
  estimate: () => number;
10
10
  /**
11
11
  * Rows kept either side of what fits, so a scroll reveals something drawn
12
- * rather than something blank. Four by default, which is roughly a frame of
13
- * fast scrolling at a 32 pixel row.
12
+ * rather than something blank. Four by default.
13
+ *
14
+ * While the box scrolls, one more screen of rows is drawn on the side it is
15
+ * scrolling toward. On iOS the scroll runs in another process, and the page
16
+ * hears where it is a frame or more later. Four rows of 32 pixels cover
17
+ * about 18 milliseconds of a fling at 7,000 pixels a second, so without
18
+ * that screen the leading edge showed the empty spacer.
14
19
  */
15
20
  overscan?: number;
16
21
  /**
@@ -46,6 +51,17 @@ export interface VirtualizeOptions {
46
51
  * whole set the first time it is asked.
47
52
  */
48
53
  measured?: () => boolean;
54
+ /**
55
+ * Whether the controller holds the reader still on this pass when rows
56
+ * above them turn out a different height than estimated. True by default.
57
+ *
58
+ * A host that keeps its own reader in place answers false while it does,
59
+ * because two holds on one box each undo part of the other. `Chat` holds the
60
+ * turn being read while the log is still, and answers true while the reader
61
+ * scrolls, so the controller's spacer takes the move and nothing writes the
62
+ * scroll offset during a fling.
63
+ */
64
+ anchor?: () => boolean;
49
65
  }
50
66
  /** A row that has been drawn, and which item of the set it drew. */
51
67
  export interface Rendered {
@@ -140,12 +156,92 @@ export declare class VirtualizeController implements ReactiveController {
140
156
  private sample;
141
157
  /** The length of the set when `endInSight` last answered true. */
142
158
  private askedAt;
159
+ /**
160
+ * How far the drawn rows sit from where the measured heights place them, in
161
+ * pixels, while the reader is scrolling.
162
+ *
163
+ * **A correction written to `scrollTop` during a fling ends the fling on
164
+ * iOS.** A row above the reader that turns out shorter than its estimate
165
+ * moves everything below it, and the controller has to put the reader back.
166
+ * While the box scrolls, the before spacer takes the difference and
167
+ * `scrollTop` is left alone. The spacer and the scroll offset are squared
168
+ * once the box has been still for `REST` milliseconds with no finger on it,
169
+ * or straight away when the spacer would have to be negative or the first
170
+ * row would sit below the top of the set.
171
+ */
172
+ private drift;
173
+ /**
174
+ * The scroll offset at the last scroll event or the last write of this
175
+ * controller's own, which gives the direction, and a move still to be heard.
176
+ */
177
+ private lastTop;
178
+ /**
179
+ * The item `reveal` scrolled to, which corrections hold still until the
180
+ * reader scrolls. Rows measured between the top of the box and that item
181
+ * would otherwise move it back out of view.
182
+ */
183
+ private target;
184
+ /** The way the box last scrolled: 1 down, -1 up, 0 before any scroll. */
185
+ private heading;
186
+ /** Whether a finger is on the box. iOS keeps scrolling after it lifts. */
187
+ private touching;
188
+ /** The timer that ends a scroll, set again on every scroll event. */
189
+ private resting;
190
+ /**
191
+ * Whether the rows drawn on the straight line have come out different
192
+ * heights. From then on the list is placed by measurement, whatever
193
+ * `measured` says. A list whose rows are one height on a wide screen can
194
+ * wrap on a phone, and a straight line through rows of different heights
195
+ * moves the content by the difference each time a row enters or leaves.
196
+ */
197
+ private uneven;
143
198
  constructor(host: ReactiveControllerHost, options: VirtualizeOptions);
144
199
  hostConnected(): void;
145
200
  hostDisconnected(): void;
146
201
  /** Attaches to the box the first time there is one to attach to. */
147
202
  private watch;
148
203
  private readonly handleScroll;
204
+ private readonly handleTouchStart;
205
+ private readonly handleTouchEnd;
206
+ /**
207
+ * Whether the reader is scrolling: a finger is on the box, the box moved
208
+ * within the last 150 milliseconds, or it has moved and its `scroll` event
209
+ * has not arrived yet.
210
+ *
211
+ * For a host that holds its own reader: a write to the scroll offset while
212
+ * this is true ends a fling on iOS, and a reader position noted before the
213
+ * last move is out of date.
214
+ */
215
+ scrolling(): boolean;
216
+ /** Squares the spacer and the scroll offset once the box is still. */
217
+ private readonly rest;
218
+ /**
219
+ * Moves the drift into the scroll offset, so the rows do not move on screen
220
+ * when the render takes it out of the spacer.
221
+ *
222
+ * **The write goes on whichever side of the render keeps the room it
223
+ * needs.** A spacer that shrinks takes the room with it, and at the end of
224
+ * the set the box clamps the offset to the new height before `hostUpdated`
225
+ * runs. Written there as well, the move was made twice: in Chromium, End in
226
+ * a windowed Object Inspector left the focused row 61 pixels below the box.
227
+ * So a shrinking spacer is paid for now, and a growing one after the render.
228
+ *
229
+ * **A move owed to the render asks for that render.** At the start of the
230
+ * set the spacer is already zero, so squaring leaves the window the same and
231
+ * `measure` asks for no update. The owed move then waited for whatever
232
+ * rendered next. In Chromium, a chat scrolled to its top measured the turns
233
+ * above the reader 62 pixels taller than their estimates, and the history
234
+ * prepended after that paid the 62 pixels along with its own room, so the
235
+ * turn being read moved 62 pixels.
236
+ */
237
+ private square;
238
+ /**
239
+ * Where the top of the box is in the measured heights, in pixels: the
240
+ * scroll offset, with what is owed to it and less the drift.
241
+ */
242
+ private offset;
243
+ /** How far past the screen to draw toward the side the box is scrolling to. */
244
+ private lead;
149
245
  /**
150
246
  * Reads the scroll offset and settles the window.
151
247
  *
@@ -178,7 +274,7 @@ export declare class VirtualizeController implements ReactiveController {
178
274
  * box stays there.
179
275
  */
180
276
  private sampleRows;
181
- /** Whether this pass places by measurement: offered, and wanted. */
277
+ /** Whether this pass places by measurement: offered, and wanted or needed. */
182
278
  private measuring;
183
279
  /**
184
280
  * The measured version, which asks where each item starts rather than dividing.
@@ -254,6 +350,24 @@ export declare class VirtualizeController implements ReactiveController {
254
350
  * Skipped at the very top, where there is nothing above to have moved.
255
351
  */
256
352
  hostUpdated(): void;
353
+ /**
354
+ * Measures the rows in the tree now, and holds the reader still if they moved.
355
+ *
356
+ * `hostUpdated` calls this. A host that draws its rows after `hostUpdated`,
357
+ * in `updated`, calls it again once they are drawn, so the rows of this
358
+ * window are measured in the pass that draws them. Measured a pass later, a
359
+ * window chosen by the estimate stopped short of the bottom of the box and
360
+ * stayed there until the next scroll: in Chromium, 5,000 rows of 21 pixels
361
+ * against an estimate of 32 left 142 to 157 pixels of the box empty after
362
+ * each jump of 1,500 pixels, and the next scroll moved the reader back 297.
363
+ *
364
+ * **The reader is held by the row at the top of the box.** Rows drawn above
365
+ * it for the first time move it by what they came to, less what they were
366
+ * estimated at. The window's first row is the wrong anchor: no row before
367
+ * it is drawn, so nothing before it is measured, and the rows that do move
368
+ * the reader were never counted.
369
+ */
370
+ settle(): void;
257
371
  /**
258
372
  * Tells the controller the set grew by `by` items at its start.
259
373
  *