@fusion.dev/helix-core 0.148.0 → 0.149.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +8518 -6050
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +1 -1
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +2 -2
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +5 -4
- package/dist/controllers/machine-controller/machine-controller.d.ts +69 -0
- package/dist/controllers/machine-controller/machine-controller.js +78 -0
- package/dist/controllers/machine-controller/machine.d.ts +93 -0
- package/dist/controllers/machine-controller/machine.js +20 -0
- package/dist/controllers/reorder-controller/reorder-controller.d.ts +285 -0
- package/dist/controllers/reorder-controller/reorder-controller.js +497 -0
- package/dist/controllers/source-controller/source-controller.js +1 -1
- package/dist/controllers/virtualize-controller/heights.js +1 -1
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +117 -3
- package/dist/controllers/virtualize-controller/virtualize-controller.js +134 -32
- package/dist/events/before-reorder.d.ts +14 -0
- package/dist/events/before-reorder.js +11 -0
- package/dist/events/details.d.ts +28 -0
- package/dist/events/events.d.ts +4 -2
- package/dist/events/events.js +3 -1
- package/dist/events/names.js +1 -1
- package/dist/events/reorder.d.ts +18 -12
- package/dist/events/reordered.d.ts +4 -4
- package/dist/helix-element/helix-element.js +2 -1
- package/dist/helix-element/react-audit-off.d.ts +11 -0
- package/dist/helix-element/react-audit-off.js +1 -0
- package/dist/helix-element/react-audit.d.ts +20 -0
- package/dist/helix-element/react-audit.js +56 -0
- package/dist/helix-element/react-bridge.d.ts +2 -22
- package/dist/helix-element/react-bridge.js +1 -56
- package/dist/index.d.ts +10 -4
- package/dist/index.js +10 -4
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +2 -2
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +3 -0
- package/dist/mixins/mixin-collection/mixin-collection.js +4 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +3 -0
- package/dist/mixins/mixin-field/mixin-field.js +1 -1
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +12 -0
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +15 -2
- package/dist/mixins/mixin-tokens/mixin-tokens.d.ts +5 -0
- package/dist/mixins/mixin-tokens/mixin-tokens.js +13 -2
- package/dist/mixins/mixin-viewport/mixin-viewport.d.ts +17 -15
- package/dist/mixins/mixin-viewport/mixin-viewport.js +15 -34
- package/dist/utils/calendar/calendar.d.ts +27 -0
- package/dist/utils/calendar/calendar.js +17 -0
- package/dist/utils/count-label/count-label.d.ts +7 -4
- package/dist/utils/count-label/count-label.js +2 -5
- package/dist/utils/date-segments/date-segments.d.ts +2 -1
- package/dist/utils/date-segments/date-segments.js +10 -0
- package/dist/utils/locale-of/locale-of.d.ts +5 -0
- package/dist/utils/locale-of/locale-of.js +8 -2
- package/dist/utils/reorder/drag-preview.d.ts +46 -0
- package/dist/utils/reorder/drag-preview.js +58 -0
- package/dist/utils/reorder/reorder.d.ts +178 -0
- package/dist/utils/reorder/reorder.js +112 -0
- package/dist/utils/report-runtime/report-runtime.d.ts +7 -4
- package/dist/utils/slotted-words/slotted-words.d.ts +3 -2
- package/dist/utils/slotted-words/slotted-words.js +3 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/words/index.d.ts +3 -0
- package/dist/words/index.js +3 -0
- package/dist/words/message.d.ts +47 -0
- package/dist/words/message.js +152 -0
- package/dist/words/registry.d.ts +89 -0
- package/dist/words/registry.js +101 -0
- package/dist/words/words-locale.d.ts +34 -0
- package/dist/words/words-locale.js +30 -0
- package/dist/words/words.d.ts +52 -0
- package/dist/words/words.js +100 -0
- 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 '
|
|
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();
|
|
@@ -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
|
|
13
|
-
*
|
|
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
|
*
|