@swiftbrowser/web 0.2.0 → 0.3.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/README.md +98 -57
- package/app/index.html +10 -10
- package/node/dist/node/examples.d.ts.map +1 -1
- package/node/dist/node/examples.js +1 -0
- package/node/dist/node/examples.js.map +1 -1
- package/node/dist/plugins/asset-catalogs.js +10 -10
- package/node/dist/plugins/asset-catalogs.js.map +1 -1
- package/package.json +1 -1
- package/plugins/asset-catalogs.ts +10 -10
- package/src/interaction.ts +50 -2
- package/src/largeTitle.ts +79 -0
- package/src/layout/engine.ts +491 -164
- package/src/layoutDom.ts +13 -3
- package/src/main.ts +4 -1
- package/src/mock.ts +2 -2
- package/src/protocol.ts +22 -2
- package/src/renderer.ts +19 -8
- package/src/scanner.ts +1 -1
- package/src/styles.css +330 -97
- package/src/styles.ts +30 -0
- package/src/symbols.ts +97 -10
- package/src/webAuth.ts +1 -1
package/src/interaction.ts
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* 2. the wrappers between the target and the root, innermost first: a
|
|
10
10
|
* `gesture` (drag, long press), a `contextmenu` (long press) and a swipe
|
|
11
11
|
* row (`swipeactions`, or a deletable `listrow` without one);
|
|
12
|
-
* 3. the sheet card (drag down to dismiss)
|
|
12
|
+
* 3. the sheet card (drag down to dismiss): inside a vertical scroll view
|
|
13
|
+
* only while that view is at its top, as UIKit hands a sheet's scroll
|
|
14
|
+
* view's downward drag to the sheet there (a touch's first downward move
|
|
15
|
+
* is kept from scrolling natively so the pointer reaches the sheet);
|
|
13
16
|
* 4. pull to refresh on a `refreshable` scroller at its top.
|
|
14
17
|
*
|
|
15
18
|
* Nothing owns the pointer at first. On each move the candidates are asked in
|
|
@@ -188,6 +191,8 @@ export class InteractionController {
|
|
|
188
191
|
private wheelPull: { scroller: HTMLElement; pulled: number; sent: boolean; timer: number } | null = null;
|
|
189
192
|
/** Touch-driven pull (the browser cancels the pointer once the list scrolls). */
|
|
190
193
|
private touchPull: { scroller: HTMLElement; id: ElementId; startY: number; sent: boolean } | null = null;
|
|
194
|
+
/** A touch in a sheet's scroll view at its top: its first move decides who scrolls. */
|
|
195
|
+
private touchSheet: { scroller: HTMLElement; startY: number } | null = null;
|
|
191
196
|
|
|
192
197
|
constructor(private readonly host: InteractionHost) {
|
|
193
198
|
const root = host.root;
|
|
@@ -198,6 +203,7 @@ export class InteractionController {
|
|
|
198
203
|
root.addEventListener('scroll', this.onScroll, true);
|
|
199
204
|
root.addEventListener('touchstart', this.onTouchStart, { passive: true });
|
|
200
205
|
root.addEventListener('touchmove', this.onTouchMove, { passive: true });
|
|
206
|
+
root.addEventListener('touchmove', this.onSheetTouchMove, { passive: false });
|
|
201
207
|
root.addEventListener('touchend', this.onTouchEnd, { passive: true });
|
|
202
208
|
root.addEventListener('touchcancel', this.onTouchEnd, { passive: true });
|
|
203
209
|
document.addEventListener('pointerdown', this.onDocumentPointerDown, true);
|
|
@@ -212,6 +218,7 @@ export class InteractionController {
|
|
|
212
218
|
root.removeEventListener('scroll', this.onScroll, true);
|
|
213
219
|
root.removeEventListener('touchstart', this.onTouchStart);
|
|
214
220
|
root.removeEventListener('touchmove', this.onTouchMove);
|
|
221
|
+
root.removeEventListener('touchmove', this.onSheetTouchMove);
|
|
215
222
|
root.removeEventListener('touchend', this.onTouchEnd);
|
|
216
223
|
root.removeEventListener('touchcancel', this.onTouchEnd);
|
|
217
224
|
document.removeEventListener('pointerdown', this.onDocumentPointerDown, true);
|
|
@@ -405,7 +412,9 @@ export class InteractionController {
|
|
|
405
412
|
}
|
|
406
413
|
const card = target.closest<HTMLElement>('.sb-sheet-card');
|
|
407
414
|
const sheet = card?.parentElement;
|
|
408
|
-
|
|
415
|
+
// A scroll view in the sheet that is scrolled down keeps the drag.
|
|
416
|
+
const sheetScroller = card ? verticalScrollerIn(target, card) : null;
|
|
417
|
+
if (card && sheet?.classList.contains('sb-sheet') && sheet.dataset['sbSheetPosition'] !== 'below' && !(sheetScroller && sheetScroller.scrollTop > 0)) {
|
|
409
418
|
const id = Number(sheet.dataset[ID_ATTR]);
|
|
410
419
|
const detents = (sheet.dataset['sbDetents'] ?? '').split(' ');
|
|
411
420
|
// A full-screen cover (`detents: ["full"]`) has no drag to dismiss.
|
|
@@ -946,7 +955,15 @@ export class InteractionController {
|
|
|
946
955
|
|
|
947
956
|
private readonly onTouchStart = (event: TouchEvent): void => {
|
|
948
957
|
const touch = event.touches[0];
|
|
958
|
+
this.touchSheet = null;
|
|
949
959
|
if (!touch || event.touches.length !== 1) return;
|
|
960
|
+
const target = event.target as HTMLElement;
|
|
961
|
+
const card = target.closest<HTMLElement>('.sb-sheet-card');
|
|
962
|
+
const sheetScroller = card ? verticalScrollerIn(target, card) : null;
|
|
963
|
+
// A refreshable scroller keeps its pull to refresh.
|
|
964
|
+
if (sheetScroller && sheetScroller.scrollTop <= 0 && sheetScroller.dataset['sbRefreshable'] === undefined) {
|
|
965
|
+
this.touchSheet = { scroller: sheetScroller, startY: touch.clientY };
|
|
966
|
+
}
|
|
950
967
|
const scroller = this.refreshableScrollerOf(event.target as Element);
|
|
951
968
|
if (!scroller || scroller.el.scrollTop > 0) return;
|
|
952
969
|
this.touchPull = { scroller: scroller.el, id: scroller.id, startY: touch.clientY, sent: false };
|
|
@@ -969,6 +986,25 @@ export class InteractionController {
|
|
|
969
986
|
|
|
970
987
|
private readonly onTouchEnd = (): void => {
|
|
971
988
|
this.touchPull = null;
|
|
989
|
+
this.touchSheet = null;
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* The first move of a touch in a sheet's scroll view at its top: downwards,
|
|
994
|
+
* it is kept from scrolling (or overscrolling) the view, so the pointer
|
|
995
|
+
* stays with the sheet's drag; upwards, the view scrolls as usual.
|
|
996
|
+
*/
|
|
997
|
+
private readonly onSheetTouchMove = (event: TouchEvent): void => {
|
|
998
|
+
const pending = this.touchSheet;
|
|
999
|
+
const touch = event.touches[0];
|
|
1000
|
+
if (!pending || !touch) return;
|
|
1001
|
+
const dy = touch.clientY - pending.startY;
|
|
1002
|
+
if (dy === 0) return;
|
|
1003
|
+
if (dy > 0 && pending.scroller.scrollTop <= 0 && event.cancelable) {
|
|
1004
|
+
event.preventDefault();
|
|
1005
|
+
return; // keep deciding: every move of this drag stays with the sheet
|
|
1006
|
+
}
|
|
1007
|
+
this.touchSheet = null;
|
|
972
1008
|
};
|
|
973
1009
|
|
|
974
1010
|
private readonly onContextMenu = (event: MouseEvent): void => {
|
|
@@ -1088,6 +1124,18 @@ function clearSelectionIn(root: HTMLElement): void {
|
|
|
1088
1124
|
}
|
|
1089
1125
|
|
|
1090
1126
|
/** A scroll view that scrolls horizontally (`ScrollView(.horizontal)` or both axes). */
|
|
1127
|
+
/** The innermost vertically scrolling `list` / `scrollview` from `target` up to `limit` (excluded). */
|
|
1128
|
+
function verticalScrollerIn(target: HTMLElement, limit: HTMLElement): HTMLElement | null {
|
|
1129
|
+
for (let el: HTMLElement | null = target; el && el !== limit; el = el.parentElement) {
|
|
1130
|
+
if (el.classList.contains('sb-list')) return el;
|
|
1131
|
+
if (el.classList.contains('sb-scrollview')) {
|
|
1132
|
+
const axes = el.dataset['sbAxes'];
|
|
1133
|
+
if (axes === 'vertical' || axes === 'both' || axes === undefined) return el;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1091
1139
|
function isHorizontalScroller(el: HTMLElement): boolean {
|
|
1092
1140
|
if (!el.classList.contains('sb-scrollview')) return false;
|
|
1093
1141
|
const axes = el.dataset['sbAxes'];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A navigation screen's large title scrolls with the screen's root list or
|
|
3
|
+
* scroll view (iOS): the engine starts that scroll view under the bar, insets
|
|
4
|
+
* its content by the title row and marks it with a `largeTitle` chrome rect
|
|
5
|
+
* (layout/engine.ts, `placeNavScreen`). Here the title follows the scroll
|
|
6
|
+
* offset and fades out at the bar's edge, as the scrolled content does
|
|
7
|
+
* (`data-sb-scrolled`, iOS 26's scroll edge effect), and once it has gone
|
|
8
|
+
* under the bar the screen gets `data-sb-collapsed`, which shows the inline
|
|
9
|
+
* title (styles.css). Pulled past the top (a touch overscroll), the title
|
|
10
|
+
* moves down with the content (iOS 26 no longer grows it).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The scroll offset past which the title has gone under the bar. */
|
|
14
|
+
const COLLAPSE_OFFSET = 44;
|
|
15
|
+
/** How far below the bar's edge content fades in (styles.css matches). */
|
|
16
|
+
const EDGE_FADE = 16;
|
|
17
|
+
|
|
18
|
+
const bound = new WeakSet<HTMLElement>();
|
|
19
|
+
|
|
20
|
+
function largeTitleOf(scroller: HTMLElement): { screen: HTMLElement; title: HTMLElement } | null {
|
|
21
|
+
const screen = scroller.closest<HTMLElement>('.sb-navscreen');
|
|
22
|
+
const title = screen?.querySelector<HTMLElement>(':scope > .sb-navscreen-body > .sb-navbar-large-title');
|
|
23
|
+
return screen && title ? { screen, title } : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Called for every list and scroll view the layout reaches: `scrolls` is
|
|
28
|
+
* whether the engine marked it as carrying its screen's large title.
|
|
29
|
+
*/
|
|
30
|
+
export function bindLargeTitle(scroller: HTMLElement, scrolls: boolean): void {
|
|
31
|
+
if (!scrolls) {
|
|
32
|
+
if (scroller.dataset['sbScrollsTitle'] !== undefined) {
|
|
33
|
+
delete scroller.dataset['sbScrollsTitle'];
|
|
34
|
+
delete scroller.dataset['sbScrolled'];
|
|
35
|
+
const parts = largeTitleOf(scroller);
|
|
36
|
+
if (parts) reset(parts.screen, parts.title);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
scroller.dataset['sbScrollsTitle'] = '';
|
|
41
|
+
if (!bound.has(scroller)) {
|
|
42
|
+
bound.add(scroller);
|
|
43
|
+
scroller.addEventListener('scroll', () => syncLargeTitle(scroller), { passive: true });
|
|
44
|
+
}
|
|
45
|
+
syncLargeTitle(scroller);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function syncLargeTitle(scroller: HTMLElement): void {
|
|
49
|
+
if (scroller.dataset['sbScrollsTitle'] === undefined) return;
|
|
50
|
+
const parts = largeTitleOf(scroller);
|
|
51
|
+
if (!parts) return;
|
|
52
|
+
const { screen, title } = parts;
|
|
53
|
+
const offset = scroller.scrollTop;
|
|
54
|
+
const s = title.style;
|
|
55
|
+
if (offset > 0) {
|
|
56
|
+
s.transform = `translateY(${-offset}px)`;
|
|
57
|
+
// Hide what has gone above the title's row (under the bar), fading in
|
|
58
|
+
// below the edge.
|
|
59
|
+
setMask(title, `linear-gradient(to bottom, transparent ${offset}px, #000 ${offset + EDGE_FADE}px)`);
|
|
60
|
+
scroller.dataset['sbScrolled'] = '';
|
|
61
|
+
} else {
|
|
62
|
+
s.transform = offset < 0 ? `translateY(${-offset}px)` : '';
|
|
63
|
+
setMask(title, '');
|
|
64
|
+
delete scroller.dataset['sbScrolled'];
|
|
65
|
+
}
|
|
66
|
+
if (offset > COLLAPSE_OFFSET) screen.dataset['sbCollapsed'] = '';
|
|
67
|
+
else delete screen.dataset['sbCollapsed'];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function setMask(el: HTMLElement, mask: string): void {
|
|
71
|
+
el.style.setProperty('mask-image', mask);
|
|
72
|
+
el.style.setProperty('-webkit-mask-image', mask);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function reset(screen: HTMLElement, title: HTMLElement): void {
|
|
76
|
+
title.style.transform = '';
|
|
77
|
+
setMask(title, '');
|
|
78
|
+
delete screen.dataset['sbCollapsed'];
|
|
79
|
+
}
|