@swiftbrowser/web 0.17.7 → 0.18.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/node/dist/node/apps.d.ts +5 -0
- package/node/dist/node/apps.d.ts.map +1 -1
- package/node/dist/node/apps.js +3 -0
- package/node/dist/node/apps.js.map +1 -1
- package/node/dist/node/config.d.ts +2 -0
- package/node/dist/node/config.d.ts.map +1 -1
- package/node/dist/node/config.js +2 -0
- package/node/dist/node/config.js.map +1 -1
- package/package.json +1 -1
- package/public/_headers +5 -0
- package/src/animation.ts +60 -27
- package/src/appFiles.ts +21 -16
- package/src/audioRecorder.ts +392 -0
- package/src/editorSelection.ts +80 -0
- package/src/files.ts +442 -0
- package/src/gestureComposition.ts +92 -0
- package/src/hostRequests.ts +50 -5
- package/src/interaction.ts +108 -27
- package/src/layout/engine.ts +295 -19
- package/src/layout/types.ts +6 -0
- package/src/layoutDom.ts +13 -5
- package/src/main.ts +10 -1
- package/src/notifications.ts +134 -1
- package/src/pinch.ts +9 -1
- package/src/pinnedViews.ts +131 -0
- package/src/plainEditor.ts +79 -0
- package/src/protocol.ts +307 -14
- package/src/renderer.ts +354 -43
- package/src/richEditor.ts +330 -0
- package/src/scrollGeometry.ts +79 -0
- package/src/scrollTransition.ts +145 -0
- package/src/styles.css +142 -11
- package/src/symbols.ts +22 -0
- package/src/touchPaging.ts +23 -2
- package/src/undo.ts +126 -0
- package/src/video.ts +59 -8
- package/src/vite-env.d.ts +2 -0
package/src/interaction.ts
CHANGED
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
* directly for pull to refresh (the browser cancels the pointer once it
|
|
36
36
|
* scrolls the list).
|
|
37
37
|
*
|
|
38
|
+
* A gesture's `mode` changes its place in the order (src/gestureComposition.ts):
|
|
39
|
+
* `highPriority` is asked before the wrappers inside it, and `simultaneous`
|
|
40
|
+
* never claims: it begins when its own distance is met and receives every
|
|
41
|
+
* move beside the owner, whoever that turns out to be, so a canvas can track
|
|
42
|
+
* a finger its items are also dragging.
|
|
43
|
+
*
|
|
38
44
|
* Two more candidates lift something (src/dragDrop.ts does the DOM work):
|
|
39
45
|
* a movable `listrow` (by its edit-mode grip from the pointer down, or by a
|
|
40
46
|
* long press when the list is not editing) is reordered among its
|
|
@@ -44,9 +50,10 @@
|
|
|
44
50
|
*/
|
|
45
51
|
|
|
46
52
|
import { RowReorder, ViewDrag } from './dragDrop';
|
|
53
|
+
import { isSimultaneous, placeWrapper } from './gestureComposition';
|
|
47
54
|
import type { LayoutNode } from './layout';
|
|
48
55
|
import { symbolOf, textOf, tintOf, unwrapStyledNode } from './nodeQueries';
|
|
49
|
-
import type { DragEvent, ElementId, RendererEvent } from './protocol';
|
|
56
|
+
import type { DragEvent, ElementId, GestureCoordinateSpace, GestureMode, RendererEvent } from './protocol';
|
|
50
57
|
import { colorToCSS } from './styles';
|
|
51
58
|
import {
|
|
52
59
|
SWIPE_ACTION_WIDTH,
|
|
@@ -136,17 +143,27 @@ interface SwipeHost {
|
|
|
136
143
|
actions: Record<SwipeEdge, ActionSpec[]>;
|
|
137
144
|
}
|
|
138
145
|
|
|
146
|
+
interface GestureCandidate {
|
|
147
|
+
kind: 'gesture';
|
|
148
|
+
el: HTMLElement;
|
|
149
|
+
id: ElementId;
|
|
150
|
+
drag: boolean;
|
|
151
|
+
longPress: boolean;
|
|
152
|
+
minDistance: number;
|
|
153
|
+
minDuration: number;
|
|
154
|
+
/** `.highPriorityGesture` / `.simultaneousGesture`; absent for `.gesture`. */
|
|
155
|
+
mode?: GestureMode;
|
|
156
|
+
/** The space of the drag's `location` and `startLocation`. */
|
|
157
|
+
space: GestureCoordinateSpace;
|
|
158
|
+
/** The element's box when the drag began (locations are relative to it). */
|
|
159
|
+
rect: DOMRect | null;
|
|
160
|
+
/** A simultaneous gesture that has begun (it runs beside the owner). */
|
|
161
|
+
active: boolean;
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
type Candidate =
|
|
140
165
|
| { kind: 'swipeback'; screen: HTMLElement; below: HTMLElement | null; id: ElementId; width: number }
|
|
141
|
-
|
|
|
142
|
-
kind: 'gesture';
|
|
143
|
-
el: HTMLElement;
|
|
144
|
-
id: ElementId;
|
|
145
|
-
drag: boolean;
|
|
146
|
-
longPress: boolean;
|
|
147
|
-
minDistance: number;
|
|
148
|
-
minDuration: number;
|
|
149
|
-
}
|
|
166
|
+
| GestureCandidate
|
|
150
167
|
| { kind: 'contextmenu'; el: HTMLElement; id: ElementId }
|
|
151
168
|
| { kind: 'swipe'; host: SwipeHost }
|
|
152
169
|
| { kind: 'sheet'; sheet: HTMLElement; card: HTMLElement; id: ElementId }
|
|
@@ -165,15 +182,18 @@ interface Session {
|
|
|
165
182
|
samples: Sample[];
|
|
166
183
|
candidates: Candidate[];
|
|
167
184
|
claimed: Candidate | null;
|
|
185
|
+
/** The simultaneous gestures running beside the owner (or alone, before any claim). */
|
|
186
|
+
along: GestureCandidate[];
|
|
187
|
+
/** The root's box, for `global` drag locations. */
|
|
188
|
+
rootRect: DOMRect | null;
|
|
168
189
|
/** A long press fired: the session ignores further motion. */
|
|
169
190
|
done: boolean;
|
|
170
191
|
suppressClick: boolean;
|
|
171
192
|
longPressTimer: number | null;
|
|
172
193
|
/** The pointer went down on a pushed screen's edge strip (touch): a plain tap is forwarded. */
|
|
173
194
|
edgeStrip: HTMLElement | null;
|
|
174
|
-
// gesture drag
|
|
175
|
-
|
|
176
|
-
pendingDrag: DragEvent | null;
|
|
195
|
+
// gesture drag (one per gesture: the owner's and the simultaneous ones')
|
|
196
|
+
pendingDrag: Map<ElementId, DragEvent> | null;
|
|
177
197
|
raf: number | null;
|
|
178
198
|
// swipe row
|
|
179
199
|
swipeStart: number;
|
|
@@ -322,11 +342,12 @@ export class InteractionController {
|
|
|
322
342
|
samples: [{ t: event.timeStamp, x: event.clientX, y: event.clientY }],
|
|
323
343
|
candidates,
|
|
324
344
|
claimed: null,
|
|
345
|
+
along: [],
|
|
346
|
+
rootRect: null,
|
|
325
347
|
done: false,
|
|
326
348
|
suppressClick: false,
|
|
327
349
|
longPressTimer: null,
|
|
328
350
|
edgeStrip,
|
|
329
|
-
gestureRect: null,
|
|
330
351
|
pendingDrag: null,
|
|
331
352
|
raf: null,
|
|
332
353
|
swipeStart: 0,
|
|
@@ -353,8 +374,12 @@ export class InteractionController {
|
|
|
353
374
|
session.longPressTimer = window.setTimeout(() => this.fireLongPress(session, presser), duration);
|
|
354
375
|
}
|
|
355
376
|
// An immediate drag gesture (`minimumDistance: 0`) and a row's grip own the pointer from the start.
|
|
356
|
-
const immediate = candidates.find((c) => (c.kind === 'gesture' && c.drag && c.minDistance <= 0) || (c.kind === 'reorder' && c.immediate));
|
|
357
|
-
if (immediate && candidates
|
|
377
|
+
const immediate = candidates.find((c) => (c.kind === 'gesture' && c.drag && c.minDistance <= 0 && !isSimultaneous(c)) || (c.kind === 'reorder' && c.immediate));
|
|
378
|
+
if (immediate && candidates.find((c) => !isSimultaneous(c)) === immediate) this.claim(session, immediate, event.pointerId);
|
|
379
|
+
// An immediate simultaneous gesture begins beside whatever else there is.
|
|
380
|
+
for (const c of candidates) {
|
|
381
|
+
if (c.kind === 'gesture' && isSimultaneous(c) && c.drag && c.minDistance <= 0) this.beginAlong(session, c);
|
|
382
|
+
}
|
|
358
383
|
};
|
|
359
384
|
|
|
360
385
|
/** A lifted row or view moves with the finger: the list under it must not scroll. */
|
|
@@ -378,12 +403,16 @@ export class InteractionController {
|
|
|
378
403
|
window.clearTimeout(session.longPressTimer);
|
|
379
404
|
session.longPressTimer = null;
|
|
380
405
|
}
|
|
406
|
+
// A simultaneous gesture begins once its own distance is met, claiming nothing.
|
|
407
|
+
for (const c of session.candidates) {
|
|
408
|
+
if (c.kind === 'gesture' && isSimultaneous(c) && !c.active && this.wants(c, dx, dy, distance)) this.beginAlong(session, c);
|
|
409
|
+
}
|
|
381
410
|
if (!session.claimed) {
|
|
382
|
-
const owner = session.candidates.find((c) => this.wants(c, dx, dy, distance));
|
|
383
|
-
if (
|
|
384
|
-
this.claim(session, owner, event.pointerId);
|
|
411
|
+
const owner = session.candidates.find((c) => !isSimultaneous(c) && this.wants(c, dx, dy, distance));
|
|
412
|
+
if (owner) this.claim(session, owner, event.pointerId);
|
|
385
413
|
}
|
|
386
414
|
if (session.claimed) this.move(session, session.claimed, dx, dy);
|
|
415
|
+
for (const c of session.along) this.move(session, c, dx, dy);
|
|
387
416
|
};
|
|
388
417
|
|
|
389
418
|
private readonly onPointerUp = (event: PointerEvent): void => {
|
|
@@ -412,10 +441,20 @@ export class InteractionController {
|
|
|
412
441
|
const dx = session.x - session.startX;
|
|
413
442
|
const dy = session.y - session.startY;
|
|
414
443
|
const velocity = velocityFrom(session.samples);
|
|
444
|
+
for (const c of session.along) {
|
|
445
|
+
if (cancelled) this.cancel(session, c, dx, dy);
|
|
446
|
+
else this.end(session, c, dx, dy, velocity);
|
|
447
|
+
}
|
|
415
448
|
if (session.claimed) {
|
|
416
449
|
if (cancelled) this.cancel(session, session.claimed, dx, dy);
|
|
417
450
|
else this.end(session, session.claimed, dx, dy, velocity);
|
|
418
451
|
session.suppressClick = true;
|
|
452
|
+
} else if (!cancelled && !session.done && this.tapImmediateGesture(session)) {
|
|
453
|
+
session.suppressClick = true;
|
|
454
|
+
} else if (session.along.length > 0 && Math.hypot(dx, dy) >= LONG_PRESS_SLOP) {
|
|
455
|
+
// A simultaneous gesture alone leaves a tap to the taps under it (as
|
|
456
|
+
// SwiftUI's tap and simultaneous drag both fire); a real drag was no tap.
|
|
457
|
+
session.suppressClick = true;
|
|
419
458
|
} else if (!cancelled && session.edgeStrip && Math.hypot(dx, dy) < LONG_PRESS_SLOP && !session.done) {
|
|
420
459
|
// A plain tap on the (touch-only) edge strip goes to whatever is under it.
|
|
421
460
|
this.forwardTap(session.edgeStrip, session.x, session.y);
|
|
@@ -424,6 +463,21 @@ export class InteractionController {
|
|
|
424
463
|
if (session.suppressClick) suppressNextClick();
|
|
425
464
|
}
|
|
426
465
|
|
|
466
|
+
/**
|
|
467
|
+
* A tap that nothing claimed still reaches an immediate drag gesture
|
|
468
|
+
* (`minimumDistance: 0`) under it, as `began` then `ended`: a back swipe's
|
|
469
|
+
* edge, first in line there, keeps the gesture from claiming at the
|
|
470
|
+
* pointer down, but on iOS the edge pan only begins with motion.
|
|
471
|
+
*/
|
|
472
|
+
private tapImmediateGesture(session: Session): boolean {
|
|
473
|
+
if (session.candidates[0]?.kind !== 'swipeback') return false;
|
|
474
|
+
const gesture = session.candidates.find((c): c is GestureCandidate => c.kind === 'gesture' && c.drag && c.minDistance <= 0 && !isSimultaneous(c));
|
|
475
|
+
if (!gesture) return false;
|
|
476
|
+
this.claim(session, gesture, session.pointerId);
|
|
477
|
+
this.end(session, gesture, session.x - session.startX, session.y - session.startY, { x: 0, y: 0 });
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
|
|
427
481
|
private detachWindow(): void {
|
|
428
482
|
window.removeEventListener('pointermove', this.onPointerMove);
|
|
429
483
|
window.removeEventListener('pointerup', this.onPointerUp);
|
|
@@ -453,6 +507,7 @@ export class InteractionController {
|
|
|
453
507
|
}
|
|
454
508
|
// A horizontal scroll view takes horizontal motion from the rows around it.
|
|
455
509
|
let inHorizontalScroller = false;
|
|
510
|
+
const wrapperStart = out.length;
|
|
456
511
|
for (let el: HTMLElement | null = target; el && el !== this.host.root; el = el.parentElement) {
|
|
457
512
|
const id = Number(el.dataset[ID_ATTR]);
|
|
458
513
|
if (isHorizontalScroller(el)) inHorizontalScroller = true;
|
|
@@ -464,7 +519,21 @@ export class InteractionController {
|
|
|
464
519
|
if (!drag && !longPress) continue;
|
|
465
520
|
const minDistance = typeof props['minimumDistance'] === 'number' ? Math.max(0, props['minimumDistance']) : DRAG_MIN_DISTANCE;
|
|
466
521
|
const minDuration = typeof props['minimumDuration'] === 'number' ? Math.max(0, props['minimumDuration']) * 1000 : LONG_PRESS_MS;
|
|
467
|
-
|
|
522
|
+
const mode = props['mode'];
|
|
523
|
+
const candidate: GestureCandidate = {
|
|
524
|
+
kind: 'gesture',
|
|
525
|
+
el,
|
|
526
|
+
id,
|
|
527
|
+
drag,
|
|
528
|
+
longPress,
|
|
529
|
+
minDistance,
|
|
530
|
+
minDuration,
|
|
531
|
+
space: props['coordinateSpace'] === 'global' ? 'global' : 'local',
|
|
532
|
+
rect: null,
|
|
533
|
+
active: false,
|
|
534
|
+
};
|
|
535
|
+
if (mode === 'highPriority' || mode === 'simultaneous') candidate.mode = mode;
|
|
536
|
+
placeWrapper(out, candidate, wrapperStart);
|
|
468
537
|
} else if (el.classList.contains('sb-contextmenu') && Number.isInteger(id)) {
|
|
469
538
|
out.push({ kind: 'contextmenu', el, id });
|
|
470
539
|
} else if (el.classList.contains('sb-draggable') && Number.isInteger(id)) {
|
|
@@ -533,9 +602,19 @@ export class InteractionController {
|
|
|
533
602
|
}
|
|
534
603
|
}
|
|
535
604
|
|
|
605
|
+
/** A simultaneous gesture begins: `began` goes out and it hears every move until the pointer lifts. */
|
|
606
|
+
private beginAlong(session: Session, c: GestureCandidate): void {
|
|
607
|
+
c.active = true;
|
|
608
|
+
c.rect = c.el.getBoundingClientRect();
|
|
609
|
+
session.rootRect ??= this.host.root.getBoundingClientRect();
|
|
610
|
+
session.along.push(c);
|
|
611
|
+
this.host.send(this.dragEvent(session, c, 'began', { x: 0, y: 0 }));
|
|
612
|
+
}
|
|
613
|
+
|
|
536
614
|
private claim(session: Session, c: Candidate, pointerId: number): void {
|
|
537
615
|
session.claimed = c;
|
|
538
|
-
|
|
616
|
+
// The simultaneous gestures stay: those running keep hearing the pointer, the others may still begin.
|
|
617
|
+
session.candidates = [c, ...session.candidates.filter((other) => other !== c && isSimultaneous(other))];
|
|
539
618
|
if (session.longPressTimer !== null) {
|
|
540
619
|
window.clearTimeout(session.longPressTimer);
|
|
541
620
|
session.longPressTimer = null;
|
|
@@ -548,7 +627,8 @@ export class InteractionController {
|
|
|
548
627
|
}
|
|
549
628
|
switch (c.kind) {
|
|
550
629
|
case 'gesture': {
|
|
551
|
-
|
|
630
|
+
c.rect = c.el.getBoundingClientRect();
|
|
631
|
+
session.rootRect ??= this.host.root.getBoundingClientRect();
|
|
552
632
|
this.host.send(this.dragEvent(session, c, 'began', { x: 0, y: 0 }));
|
|
553
633
|
break;
|
|
554
634
|
}
|
|
@@ -610,14 +690,14 @@ export class InteractionController {
|
|
|
610
690
|
private move(session: Session, c: Candidate, dx: number, dy: number): void {
|
|
611
691
|
switch (c.kind) {
|
|
612
692
|
case 'gesture': {
|
|
613
|
-
// Coalesced: the latest position goes out once per animation frame.
|
|
614
|
-
session.pendingDrag
|
|
693
|
+
// Coalesced: the latest position goes out once per animation frame (one per gesture).
|
|
694
|
+
(session.pendingDrag ??= new Map()).set(c.id, this.dragEvent(session, c, 'changed', { x: 0, y: 0 }));
|
|
615
695
|
if (session.raf === null) {
|
|
616
696
|
session.raf = requestAnimationFrame(() => {
|
|
617
697
|
session.raf = null;
|
|
618
698
|
const pending = session.pendingDrag;
|
|
619
699
|
session.pendingDrag = null;
|
|
620
|
-
if (pending && this.session === session) this.host.send(
|
|
700
|
+
if (pending && this.session === session) for (const event of pending.values()) this.host.send(event);
|
|
621
701
|
});
|
|
622
702
|
}
|
|
623
703
|
return;
|
|
@@ -754,8 +834,9 @@ export class InteractionController {
|
|
|
754
834
|
return reorder !== null && reorder.settling && reorder.row.parentElement === row.parentElement;
|
|
755
835
|
}
|
|
756
836
|
|
|
757
|
-
|
|
758
|
-
|
|
837
|
+
/** The event for `c` at the pointer's position, its locations in the gesture's space (the element's box, or the root's). */
|
|
838
|
+
private dragEvent(session: Session, c: GestureCandidate, phase: DragEvent['phase'], velocity: { x: number; y: number }): DragEvent {
|
|
839
|
+
const rect = c.space === 'global' ? (session.rootRect ??= this.host.root.getBoundingClientRect()) : (c.rect ??= c.el.getBoundingClientRect());
|
|
759
840
|
return {
|
|
760
841
|
type: 'drag',
|
|
761
842
|
id: c.id,
|