@celestia-island/hikari 0.43.6 → 0.43.7
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/package.json
CHANGED
|
@@ -347,19 +347,17 @@
|
|
|
347
347
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
-
/* The scale runs ALONG each list's own drag axis
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
* the
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
* no-op with no drop ring anywhere. Chips therefore scale on X (a
|
|
362
|
-
* horizontal strip), rows on Y (a vertical list). */
|
|
350
|
+
/* The scale runs ALONG each list's own drag axis — chips on X (a horizontal
|
|
351
|
+
* strip), rows on Y (a vertical list). That split is TASTE, not geometry: a
|
|
352
|
+
* small scale that follows the gesture keeps the lifted item tight in a
|
|
353
|
+
* dense chip row. A drag resolves its landing slot from the items' LIVE
|
|
354
|
+
* rects (usePointerReorder's `indexAt`): the item's MIDPOINT on the drag
|
|
355
|
+
* axis picks the slot, and on the axis ACROSS it every item's span decides
|
|
356
|
+
* which line the pointer is on — a filter that started out a no-op precisely
|
|
357
|
+
* because every row spans the list's full width. The item being dragged may
|
|
358
|
+
* not decide that line (a drop onto its own slot is already a no-op), so a
|
|
359
|
+
* centre-anchored transform on it — along the axis or across it — leaves the
|
|
360
|
+
* resolution exactly where it was. */
|
|
363
361
|
.hk-tag-input-tag-dragging {
|
|
364
362
|
transform: scaleX(1.03);
|
|
365
363
|
}
|
|
@@ -152,10 +152,14 @@ type HkTagStop =
|
|
|
152
152
|
* with the library's usual reduced-motion opt-outs (the lift's scale
|
|
153
153
|
* and shadow stay — only their 0.12s ramp is dropped, so a
|
|
154
154
|
* reduced-motion user keeps the state marker without the motion). The
|
|
155
|
-
* lift scales ALONG each list's drag axis
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
155
|
+
* lift scales ALONG each list's drag axis by TASTE, not by necessity: a
|
|
156
|
+
* small scale that follows the gesture keeps the lifted item's own
|
|
157
|
+
* footprint tight in a dense chip row. The drop slot is resolved from
|
|
158
|
+
* the items' live rects, but the dragged item never decides which line
|
|
159
|
+
* the pointer is on (it is only ever a candidate in the slot loop, and
|
|
160
|
+
* a drop onto its own slot is already a no-op), so a centre-preserving
|
|
161
|
+
* transform on it — along the axis, across it, or an isotropic
|
|
162
|
+
* `scale()` — leaves the resolution exactly where it was;
|
|
159
163
|
* - a key in `modelValue` that the catalog does not carry (a custom
|
|
160
164
|
* tag, or an option deleted since) degrades to its raw key as the
|
|
161
165
|
* tag label — the same rule as HkAffixPicker.tagEntries. Such a key
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { effectScope, type EffectScope } from "vue";
|
|
3
3
|
|
|
4
|
-
import { usePointerReorder, type PointerReorder } from "./usePointerReorder";
|
|
4
|
+
import { usePointerReorder, indexAt, type PointerReorder } from "./usePointerReorder";
|
|
5
5
|
|
|
6
6
|
/** Items with PINNED rects — happy-dom ships no layout engine, so every
|
|
7
7
|
* item is placed by hand along the axis under test (size 40, the same
|
|
@@ -34,6 +34,22 @@ function strip(axis: "x" | "y", count = 3, size = 40) {
|
|
|
34
34
|
return { container, items };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Items with hand-placed boxes on BOTH axes — the wrapping-strip pattern,
|
|
38
|
+
* for the geometries only a transform produces (a band widened across the
|
|
39
|
+
* strip while the item keeps its place along it). */
|
|
40
|
+
function boxes(geometry: Array<{ left: number; right: number; top: number; bottom: number }>): HTMLElement[] {
|
|
41
|
+
return geometry.map((box) => {
|
|
42
|
+
const el = document.createElement("div");
|
|
43
|
+
Object.defineProperty(el, "getBoundingClientRect", {
|
|
44
|
+
configurable: true,
|
|
45
|
+
value: () =>
|
|
46
|
+
({ ...box, width: box.right - box.left, height: box.bottom - box.top, x: box.left, y: box.top }) as DOMRect,
|
|
47
|
+
});
|
|
48
|
+
document.body.appendChild(el);
|
|
49
|
+
return el;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
/** Waits out `frames` animation frames — the auto-scroll loop steps once
|
|
38
54
|
* per frame (the repo's useApproachEnd pattern). */
|
|
39
55
|
async function frames(count: number): Promise<void> {
|
|
@@ -395,6 +411,112 @@ describe("usePointerReorder", () => {
|
|
|
395
411
|
]);
|
|
396
412
|
});
|
|
397
413
|
|
|
414
|
+
it("resolves past a dragged chip whose own band is widened across the strip", () => {
|
|
415
|
+
// One line of three chips, the DRAGGED one (index 1) reporting a band
|
|
416
|
+
// widened ACROSS the strip — which is what any transform on it does: a
|
|
417
|
+
// host's `scale()` on a wrapper, a zoomed container, a theme animating a
|
|
418
|
+
// chip. The pointer sits 3px outside the SIBLINGS' band (43 against
|
|
419
|
+
// 0-40) and inside the widened one, so the dragged chip is the unique
|
|
420
|
+
// zero-gap match: left to define the line on its own it filters every
|
|
421
|
+
// sibling out and the slot collapses onto its own index — a silent
|
|
422
|
+
// no-op, no reorder and no drop ring.
|
|
423
|
+
const items = boxes([
|
|
424
|
+
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
425
|
+
{ left: 100, right: 200, top: -8, bottom: 48 },
|
|
426
|
+
{ left: 200, right: 300, top: 0, bottom: 40 },
|
|
427
|
+
]);
|
|
428
|
+
const { handle, drops } = harness("x", items);
|
|
429
|
+
|
|
430
|
+
press(handle, items, 1, { x: 150, y: 20 });
|
|
431
|
+
move(260, 43);
|
|
432
|
+
expect(handle.dragging.value).toBe(true);
|
|
433
|
+
expect(handle.dragOver.value, "260 is past item 2's midpoint of 250").toBe(2);
|
|
434
|
+
up(260, 43);
|
|
435
|
+
expect(drops).toEqual([[1, 2]]);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("resolves the panel rows the same way when the dragged row's band is widened", () => {
|
|
439
|
+
// The same defect on the vertical strip, whose cross axis is x: the
|
|
440
|
+
// dragged row (index 1) reports its band widened from 0-40 to -8-48, so
|
|
441
|
+
// the pointer at x 44 is 4px outside the siblings' band and inside the
|
|
442
|
+
// dragged one. Both ends of the strip answer — the leading edge, then
|
|
443
|
+
// the last row.
|
|
444
|
+
const items = boxes([
|
|
445
|
+
{ left: 0, right: 40, top: 0, bottom: 40 },
|
|
446
|
+
{ left: -8, right: 48, top: 40, bottom: 80 },
|
|
447
|
+
{ left: 0, right: 40, top: 80, bottom: 120 },
|
|
448
|
+
]);
|
|
449
|
+
const { handle, drops } = harness("y", items);
|
|
450
|
+
|
|
451
|
+
press(handle, items, 1, { x: 20, y: 60 });
|
|
452
|
+
move(44, 5);
|
|
453
|
+
expect(handle.dragging.value).toBe(true);
|
|
454
|
+
expect(handle.dragOver.value, "5 is before every row's midpoint").toBe(0);
|
|
455
|
+
up(44, 5);
|
|
456
|
+
expect(drops).toEqual([[1, 0]]);
|
|
457
|
+
|
|
458
|
+
// …and 140 is past the last row's midpoint of 100.
|
|
459
|
+
press(handle, items, 1, { x: 20, y: 60 });
|
|
460
|
+
move(44, 140);
|
|
461
|
+
expect(handle.dragOver.value).toBe(2);
|
|
462
|
+
up(44, 140);
|
|
463
|
+
expect(drops).toEqual([
|
|
464
|
+
[1, 0],
|
|
465
|
+
[1, 2],
|
|
466
|
+
]);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("resolves the no-drag path (from = -1) exactly as it always did", () => {
|
|
470
|
+
// `from = -1` is the call with no drag in flight: nothing is the item
|
|
471
|
+
// being dragged, so nothing may be left out of the line — the exclusion
|
|
472
|
+
// is INERT there, not a rewrite of the resolution. The widened band
|
|
473
|
+
// therefore still owns the line on its own, which is the pre-fix value
|
|
474
|
+
// the strip resolved before the dragged item was ever excluded (the same
|
|
475
|
+
// gesture with `from = 1` answers 0 instead — see the panel-rows test).
|
|
476
|
+
const items = boxes([
|
|
477
|
+
{ left: 0, right: 40, top: 0, bottom: 40 },
|
|
478
|
+
{ left: -8, right: 48, top: 40, bottom: 80 },
|
|
479
|
+
{ left: 0, right: 40, top: 80, bottom: 120 },
|
|
480
|
+
]);
|
|
481
|
+
const rects = items.map((item) => item.getBoundingClientRect());
|
|
482
|
+
|
|
483
|
+
expect(indexAt(rects, "y", 5, 44, -1), "the widened row still owns the line").toBe(1);
|
|
484
|
+
// …and the geometry the fix never touched: the ends, and an empty strip.
|
|
485
|
+
expect(indexAt(rects, "y", 5000, 20, -1), "past every midpoint").toBe(3);
|
|
486
|
+
expect(indexAt(rects, "y", -5, 20, -1), "before every midpoint").toBe(0);
|
|
487
|
+
expect(indexAt([], "x", 0, 0, -1), "an empty strip has no slot").toBe(-1);
|
|
488
|
+
|
|
489
|
+
// The user-visible half of the same invariant: with no gesture at all, a
|
|
490
|
+
// pointer move resolves nothing and publishes nothing.
|
|
491
|
+
const { handle, drops } = harness("y", items);
|
|
492
|
+
move(44, 5);
|
|
493
|
+
expect(handle.dragOver.value).toBe(-1);
|
|
494
|
+
expect(handle.dragFrom.value).toBe(-1);
|
|
495
|
+
expect(drops).toEqual([]);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("lets the dragged item speak for a line no sibling shares", () => {
|
|
499
|
+
// A chip wrapped onto a line of its own (index 2) while it is dragged:
|
|
500
|
+
// no sibling is on that line, so the dragged item's band IS the line the
|
|
501
|
+
// pointer is on and a drag within it stays the no-op it always was.
|
|
502
|
+
// Ignoring the dragged item's band outright instead would let the
|
|
503
|
+
// nearest OTHER line — a full row above — answer, and a 90px drag along
|
|
504
|
+
// its own line would jump the chip up into it.
|
|
505
|
+
const items = boxes([
|
|
506
|
+
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
507
|
+
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
508
|
+
{ left: 0, right: 60, top: 40, bottom: 80 },
|
|
509
|
+
]);
|
|
510
|
+
const { handle, drops } = harness("x", items);
|
|
511
|
+
|
|
512
|
+
press(handle, items, 2, { x: 5, y: 60 });
|
|
513
|
+
move(100, 60);
|
|
514
|
+
expect(handle.dragging.value).toBe(true);
|
|
515
|
+
expect(handle.dragOver.value, "the pointer is still on the chip's own line").toBe(2);
|
|
516
|
+
up(100, 60);
|
|
517
|
+
expect(drops).toEqual([]);
|
|
518
|
+
});
|
|
519
|
+
|
|
398
520
|
it("retires a stale click trap when a new press starts", () => {
|
|
399
521
|
const { items } = strip("x");
|
|
400
522
|
const { handle } = harness("x", items);
|
|
@@ -77,6 +77,95 @@ export interface PointerReorder {
|
|
|
77
77
|
start: (event: PointerEvent, index: number) => void;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** The display index the dragged item lands ON for a pointer at
|
|
81
|
+
* (`main`, `cross`) — the pointer's coordinates split into the strip's
|
|
82
|
+
* axis and the axis across it — over `rects`, starting from index `from`
|
|
83
|
+
* (`-1` while no drag is in flight). The composable's own resolution, kept
|
|
84
|
+
* pure so its tests can pin the no-drag path: this module is internal to
|
|
85
|
+
* the package, so the export is not part of the published surface.
|
|
86
|
+
*
|
|
87
|
+
* Resolved in two steps. First the LINE: items whose cross-axis span
|
|
88
|
+
* holds the pointer (or is nearest to it) are the candidates, which is
|
|
89
|
+
* what makes a WRAPPING chip row behave — a chip on the second line can
|
|
90
|
+
* never be matched by a pointer on the first. A single-file strip (the
|
|
91
|
+
* panel's column, whose rows all span the list width) has every item in
|
|
92
|
+
* one span, so filtering by line is a strict no-op there — it only ever
|
|
93
|
+
* separates items that really are on different lines.
|
|
94
|
+
*
|
|
95
|
+
* Then the INSERTION POINT: the first candidate whose midpoint still
|
|
96
|
+
* lies beyond the pointer (past the line's end when it is beyond every
|
|
97
|
+
* candidate midpoint), which becomes a landing index by accounting for
|
|
98
|
+
* the dragged item's own removal — dragging past one neighbour's
|
|
99
|
+
* midpoint swaps with that neighbour instead of skipping it. -1 for an
|
|
100
|
+
* empty strip. */
|
|
101
|
+
export function indexAt(
|
|
102
|
+
rects: readonly DOMRect[],
|
|
103
|
+
axis: PointerReorderAxis,
|
|
104
|
+
main: number,
|
|
105
|
+
cross: number,
|
|
106
|
+
from: number,
|
|
107
|
+
): number {
|
|
108
|
+
if (rects.length === 0) return -1;
|
|
109
|
+
/** An item's span ACROSS the strip — the axis the line is read on. */
|
|
110
|
+
const band = (rect: DOMRect) =>
|
|
111
|
+
axis === "x" ? { lo: rect.top, hi: rect.bottom } : { lo: rect.left, hi: rect.right };
|
|
112
|
+
// Nearest line wins: items in the pointer's own band score 0, and a
|
|
113
|
+
// pointer between two bands takes the closer one. The half-pixel
|
|
114
|
+
// tolerance keeps the siblings of ONE wrapped line together, whose
|
|
115
|
+
// bands can differ by sub-pixel rounding.
|
|
116
|
+
const lineGap = rects.map((rect) => {
|
|
117
|
+
const { lo, hi } = band(rect);
|
|
118
|
+
return cross < lo ? lo - cross : cross > hi ? cross - hi : 0;
|
|
119
|
+
});
|
|
120
|
+
// The line is decided by the items OTHER than the one being dragged: a
|
|
121
|
+
// drop onto the dragged item's own slot is already a no-op, so it must
|
|
122
|
+
// never be the geometry that decides which line the pointer is on —
|
|
123
|
+
// which is what makes the resolution invariant to whatever transform the
|
|
124
|
+
// host paints on the item being moved. Its live rect reflects that
|
|
125
|
+
// transform (the drag lift, a zoomed container, a `scale()` on a wrapper),
|
|
126
|
+
// so a band widened across the strip would otherwise be the unique
|
|
127
|
+
// zero-gap match: every sibling filtered out, the slot collapsed onto the
|
|
128
|
+
// item's own index, and the whole drag a silent no-op.
|
|
129
|
+
let nearest = Number.POSITIVE_INFINITY;
|
|
130
|
+
for (let i = 0; i < lineGap.length; i += 1) {
|
|
131
|
+
if (i !== from && lineGap[i] < nearest) nearest = lineGap[i];
|
|
132
|
+
}
|
|
133
|
+
// …unless the dragged item is ALONE on its line — no sibling whose band
|
|
134
|
+
// holds its centre. A transform about the item's own centre (this
|
|
135
|
+
// library's lift, a zoomed container, a `scale()` on a wrapper) leaves
|
|
136
|
+
// that centre on the line the item was laid out on, whatever the band
|
|
137
|
+
// grew to, so a lone item's band IS that line: the exclusion above cannot
|
|
138
|
+
// see the line at all and the item speaks for itself — a single-file
|
|
139
|
+
// strip, or a chip wrapped onto a line of its own, keeps the answer it
|
|
140
|
+
// had. A transform that moves the centre OFF that line (an edge-anchored
|
|
141
|
+
// scale of ~2x or more, a translate of a whole line — nothing this
|
|
142
|
+
// library paints) is the one case these rects cannot read: the centre
|
|
143
|
+
// then decides from the line it landed on.
|
|
144
|
+
if (from >= 0 && from < rects.length) {
|
|
145
|
+
const own = band(rects[from]);
|
|
146
|
+
const centre = (own.lo + own.hi) / 2;
|
|
147
|
+
const shared = rects.some((rect, i) => {
|
|
148
|
+
if (i === from) return false;
|
|
149
|
+
const { lo, hi } = band(rect);
|
|
150
|
+
return centre >= lo - 0.5 && centre <= hi + 0.5;
|
|
151
|
+
});
|
|
152
|
+
if (!shared && lineGap[from] < nearest) nearest = lineGap[from];
|
|
153
|
+
}
|
|
154
|
+
let slot = -1;
|
|
155
|
+
for (let i = 0; i < rects.length; i += 1) {
|
|
156
|
+
if (lineGap[i] > nearest + 0.5) continue;
|
|
157
|
+
const rect = rects[i];
|
|
158
|
+
const mid = axis === "x" ? rect.left + rect.width / 2 : rect.top + rect.height / 2;
|
|
159
|
+
if (main < mid) {
|
|
160
|
+
slot = i;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
slot = i + 1;
|
|
164
|
+
}
|
|
165
|
+
if (slot < 0) slot = rects.length;
|
|
166
|
+
return from >= 0 && slot > from ? slot - 1 : slot;
|
|
167
|
+
}
|
|
168
|
+
|
|
80
169
|
/**
|
|
81
170
|
* usePointerReorder — pointer drag-to-reorder for a strip of elements.
|
|
82
171
|
*
|
|
@@ -146,53 +235,19 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
146
235
|
return options.items().filter((el): el is HTMLElement => el != null);
|
|
147
236
|
}
|
|
148
237
|
|
|
149
|
-
/** The
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
|
|
153
|
-
* Resolved in two steps. First the LINE: items whose cross-axis span
|
|
154
|
-
* holds the pointer (or is nearest to it) are the candidates, which is
|
|
155
|
-
* what makes a WRAPPING chip row behave — a chip on the second line can
|
|
156
|
-
* never be matched by a pointer on the first. A single-file strip (the
|
|
157
|
-
* panel's column, whose rows all span the list width) has every item in
|
|
158
|
-
* one span, so filtering by line is a strict no-op there — it only ever
|
|
159
|
-
* separates items that really are on different lines.
|
|
160
|
-
*
|
|
161
|
-
* Then the INSERTION POINT: the first candidate whose midpoint still
|
|
162
|
-
* lies beyond the pointer (past the line's end when it is beyond every
|
|
163
|
-
* candidate midpoint), which becomes a landing index by accounting for
|
|
164
|
-
* the dragged item's own removal — dragging past one neighbour's
|
|
165
|
-
* midpoint swaps with that neighbour instead of skipping it. -1 for an
|
|
166
|
-
* empty strip. */
|
|
167
|
-
function indexAt(main: number, cross: number, from: number): number {
|
|
238
|
+
/** The slot the live strip resolves for a pointer at (`main`, `cross`),
|
|
239
|
+
* starting from index `from` — the elements measured, then handed to
|
|
240
|
+
* `indexAt`, which carries the geometry's rules. */
|
|
241
|
+
function slotAt(main: number, cross: number, from: number): number {
|
|
168
242
|
const items = liveItems();
|
|
169
243
|
if (items.length === 0) return -1;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const lo = axis === "x" ? rect.top : rect.left;
|
|
178
|
-
const hi = axis === "x" ? rect.bottom : rect.right;
|
|
179
|
-
const gap = cross < lo ? lo - cross : cross > hi ? cross - hi : 0;
|
|
180
|
-
if (gap < nearest) nearest = gap;
|
|
181
|
-
return gap;
|
|
182
|
-
});
|
|
183
|
-
let slot = -1;
|
|
184
|
-
for (let i = 0; i < rects.length; i += 1) {
|
|
185
|
-
if (lineGap[i] > nearest + 0.5) continue;
|
|
186
|
-
const rect = rects[i];
|
|
187
|
-
const mid = axis === "x" ? rect.left + rect.width / 2 : rect.top + rect.height / 2;
|
|
188
|
-
if (main < mid) {
|
|
189
|
-
slot = i;
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
slot = i + 1;
|
|
193
|
-
}
|
|
194
|
-
if (slot < 0) slot = items.length;
|
|
195
|
-
return from >= 0 && slot > from ? slot - 1 : slot;
|
|
244
|
+
return indexAt(
|
|
245
|
+
items.map((item) => item.getBoundingClientRect()),
|
|
246
|
+
axis,
|
|
247
|
+
main,
|
|
248
|
+
cross,
|
|
249
|
+
from,
|
|
250
|
+
);
|
|
196
251
|
}
|
|
197
252
|
|
|
198
253
|
/** The pointer's coordinates split by axis: the position along the strip
|
|
@@ -248,7 +303,7 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
248
303
|
} else {
|
|
249
304
|
container.scrollTop = next;
|
|
250
305
|
}
|
|
251
|
-
const over =
|
|
306
|
+
const over = slotAt(lastMain, lastCross, pressedIndex);
|
|
252
307
|
if (over >= 0) dragOver.value = over;
|
|
253
308
|
scrollFrameId = requestAnimationFrame(scrollTick);
|
|
254
309
|
}
|
|
@@ -368,7 +423,7 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
368
423
|
const at = pointerAt(event);
|
|
369
424
|
lastMain = at.main;
|
|
370
425
|
lastCross = at.cross;
|
|
371
|
-
const over =
|
|
426
|
+
const over = slotAt(lastMain, lastCross, pressedIndex);
|
|
372
427
|
if (over >= 0) dragOver.value = over;
|
|
373
428
|
syncAutoScroll();
|
|
374
429
|
}
|
|
@@ -377,7 +432,7 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
377
432
|
if (!pressed || event.pointerId !== pointerId) return;
|
|
378
433
|
const from = pressedIndex;
|
|
379
434
|
const at = pointerAt(event);
|
|
380
|
-
const to = moved ?
|
|
435
|
+
const to = moved ? slotAt(at.main, at.cross, from) : from;
|
|
381
436
|
const dropped = moved && to >= 0 && to !== from;
|
|
382
437
|
// The drag is over either way — the trap only guards the click the
|
|
383
438
|
// browser is about to deliver.
|