@immense/vue-pom-generator 1.0.52 → 1.0.54
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 +71 -27
- package/RELEASE_NOTES.md +38 -27
- package/class-generation/base-page.ts +18 -6
- package/class-generation/callout.ts +229 -679
- package/class-generation/floating-ui-callout.ts +857 -0
- package/class-generation/index.ts +24 -15
- package/class-generation/pointer.ts +152 -109
- package/dist/class-generation/base-page.d.ts +11 -5
- package/dist/class-generation/base-page.d.ts.map +1 -1
- package/dist/class-generation/callout.d.ts +44 -1
- package/dist/class-generation/callout.d.ts.map +1 -1
- package/dist/class-generation/floating-ui-callout.d.ts +4 -0
- package/dist/class-generation/floating-ui-callout.d.ts.map +1 -0
- package/dist/class-generation/index.d.ts +3 -2
- package/dist/class-generation/index.d.ts.map +1 -1
- package/dist/class-generation/pointer.d.ts +24 -5
- package/dist/class-generation/pointer.d.ts.map +1 -1
- package/dist/index.cjs +783 -231
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +783 -231
- package/dist/index.mjs.map +1 -1
- package/dist/plugin/create-vue-pom-generator-plugins.d.ts +2 -2
- package/dist/plugin/create-vue-pom-generator-plugins.d.ts.map +1 -1
- package/dist/plugin/nuxt-discovery.d.ts +47 -0
- package/dist/plugin/nuxt-discovery.d.ts.map +1 -0
- package/dist/plugin/path-utils.d.ts +4 -5
- package/dist/plugin/path-utils.d.ts.map +1 -1
- package/dist/plugin/support/build-plugin.d.ts +6 -3
- package/dist/plugin/support/build-plugin.d.ts.map +1 -1
- package/dist/plugin/support/dev-plugin.d.ts +6 -3
- package/dist/plugin/support/dev-plugin.d.ts.map +1 -1
- package/dist/plugin/support/virtual-modules.d.ts +2 -2
- package/dist/plugin/support/virtual-modules.d.ts.map +1 -1
- package/dist/plugin/support-plugins.d.ts +5 -2
- package/dist/plugin/support-plugins.d.ts.map +1 -1
- package/dist/plugin/types.d.ts +35 -19
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/vue-plugin.d.ts +1 -1
- package/dist/plugin/vue-plugin.d.ts.map +1 -1
- package/dist/router-introspection.d.ts +4 -2
- package/dist/router-introspection.d.ts.map +1 -1
- package/dist/tests/nuxt-discovery.test.d.ts +2 -0
- package/dist/tests/nuxt-discovery.test.d.ts.map +1 -0
- package/dist/vite.config.d.ts.map +1 -1
- package/package.json +9 -13
- package/dist/plugin/support/generation-metrics.d.ts +0 -10
- package/dist/plugin/support/generation-metrics.d.ts.map +0 -1
- package/dist/tests/generation-metrics.test.d.ts +0 -2
- package/dist/tests/generation-metrics.test.d.ts.map +0 -1
|
@@ -1,68 +1,27 @@
|
|
|
1
|
-
import { computePosition, limitShift, offset, shift } from "./floating-ui";
|
|
2
1
|
import type { PwLocator, PwPage } from "./playwright-types";
|
|
3
2
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"[contenteditable]:not([contenteditable='false'])",
|
|
27
|
-
].join(",");
|
|
28
|
-
const __PW_CURSOR_ANNOTATION_MARGIN__ = 18;
|
|
29
|
-
const __PW_CURSOR_ANNOTATION_GAP__ = 18;
|
|
30
|
-
const __PW_CURSOR_ANNOTATION_ARROW_SIZE__ = 14;
|
|
31
|
-
const __PW_CURSOR_ANNOTATION_AVOID_PADDING__ = 12;
|
|
32
|
-
const __PW_CURSOR_ANNOTATION_BACKGROUND__ = "#dc2626";
|
|
33
|
-
const __PW_CURSOR_ANNOTATION_BORDER__ = "0px solid transparent";
|
|
34
|
-
const __PW_CURSOR_ANNOTATION_BOX_SHADOW__ = "0 20px 44px rgba(127, 29, 29, 0.32)";
|
|
35
|
-
const __PW_CURSOR_ANNOTATION_TEXT_COLOR__ = "#f8fafc";
|
|
36
|
-
const __PW_CURSOR_ANNOTATION_RADIUS__ = 0;
|
|
37
|
-
|
|
38
|
-
type Placement =
|
|
39
|
-
| "top"
|
|
40
|
-
| "top-start"
|
|
41
|
-
| "top-end"
|
|
42
|
-
| "right"
|
|
43
|
-
| "right-start"
|
|
44
|
-
| "right-end"
|
|
45
|
-
| "bottom"
|
|
46
|
-
| "bottom-start"
|
|
47
|
-
| "bottom-end"
|
|
48
|
-
| "left"
|
|
49
|
-
| "left-start"
|
|
50
|
-
| "left-end";
|
|
51
|
-
|
|
52
|
-
const __PW_CURSOR_ALLOWED_PLACEMENTS__: Placement[] = [
|
|
53
|
-
"top-start",
|
|
54
|
-
"top",
|
|
55
|
-
"top-end",
|
|
56
|
-
"right-start",
|
|
57
|
-
"right",
|
|
58
|
-
"right-end",
|
|
59
|
-
"bottom-start",
|
|
60
|
-
"bottom",
|
|
61
|
-
"bottom-end",
|
|
62
|
-
"left-start",
|
|
63
|
-
"left",
|
|
64
|
-
"left-end",
|
|
65
|
-
];
|
|
3
|
+
export const POINTER_CALLOUT_IDS = {
|
|
4
|
+
annotation: "__pw_pointer_callout__",
|
|
5
|
+
arrow: "__pw_pointer_callout_arrow__",
|
|
6
|
+
content: "__pw_pointer_callout_content__",
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export const POINTER_CALLOUT_THEME = {
|
|
10
|
+
arrowPadding: 10,
|
|
11
|
+
arrowSize: 14,
|
|
12
|
+
avoidPadding: 12,
|
|
13
|
+
background: "#dc2626",
|
|
14
|
+
border: "0px solid transparent",
|
|
15
|
+
borderRadius: 0,
|
|
16
|
+
boxShadow: "0 20px 44px rgba(127, 29, 29, 0.32)",
|
|
17
|
+
charsPerLine: 28,
|
|
18
|
+
gap: 18,
|
|
19
|
+
margin: 18,
|
|
20
|
+
maxWidth: 320,
|
|
21
|
+
minHeight: 52,
|
|
22
|
+
minWidth: 180,
|
|
23
|
+
textColor: "#f8fafc",
|
|
24
|
+
} as const;
|
|
66
25
|
|
|
67
26
|
export type ElementTarget = string | PwLocator;
|
|
68
27
|
|
|
@@ -73,354 +32,50 @@ export interface CalloutTargetBox {
|
|
|
73
32
|
height: number;
|
|
74
33
|
}
|
|
75
34
|
|
|
76
|
-
interface CalloutContext {
|
|
77
|
-
avoidRects: CalloutTargetBox[];
|
|
78
|
-
protectedTargetRects: CalloutTargetBox[];
|
|
79
|
-
viewportHeight: number;
|
|
80
|
-
viewportWidth: number;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interface FloatingVirtualElement extends CalloutTargetBox {
|
|
84
|
-
kind: "arrow" | "floating" | "reference";
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface CalloutLayout {
|
|
88
|
-
arrowX: number | null;
|
|
89
|
-
arrowY: number | null;
|
|
90
|
-
placement: Placement;
|
|
91
|
-
staticSide: "bottom" | "left" | "right" | "top";
|
|
92
|
-
x: number;
|
|
93
|
-
y: number;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
35
|
export interface ShowCalloutOptions {
|
|
97
36
|
skipScroll?: boolean;
|
|
98
37
|
targetBox?: CalloutTargetBox;
|
|
99
38
|
}
|
|
100
39
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
function __pw_rect_center_distance__(first: CalloutTargetBox, second: CalloutTargetBox): number {
|
|
108
|
-
const firstCenterX = first.x + first.width / 2;
|
|
109
|
-
const firstCenterY = first.y + first.height / 2;
|
|
110
|
-
const secondCenterX = second.x + second.width / 2;
|
|
111
|
-
const secondCenterY = second.y + second.height / 2;
|
|
112
|
-
return Math.hypot(firstCenterX - secondCenterX, firstCenterY - secondCenterY);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function __pw_rect_gap__(first: CalloutTargetBox, second: CalloutTargetBox): number {
|
|
116
|
-
const horizontalGap = Math.max(0, Math.max(second.x - (first.x + first.width), first.x - (second.x + second.width)));
|
|
117
|
-
const verticalGap = Math.max(0, Math.max(second.y - (first.y + first.height), first.y - (second.y + second.height)));
|
|
118
|
-
return Math.max(horizontalGap, verticalGap);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function __pw_expand_rect__(rect: CalloutTargetBox, padding: number): CalloutTargetBox {
|
|
122
|
-
return {
|
|
123
|
-
height: rect.height + (padding * 2),
|
|
124
|
-
width: rect.width + (padding * 2),
|
|
125
|
-
x: rect.x - padding,
|
|
126
|
-
y: rect.y - padding,
|
|
127
|
-
};
|
|
40
|
+
export interface CalloutRenderRequest {
|
|
41
|
+
overlayIds: string[];
|
|
42
|
+
target: ElementTarget;
|
|
43
|
+
targetBox: CalloutTargetBox;
|
|
44
|
+
text: string;
|
|
128
45
|
}
|
|
129
46
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const [side, align] = placement.split("-") as [Placement extends `${infer Side}-${string}` ? Side : never, "end" | "start" | undefined];
|
|
135
|
-
return {
|
|
136
|
-
align: align ?? "center",
|
|
137
|
-
side,
|
|
138
|
-
};
|
|
47
|
+
export interface CalloutRenderer {
|
|
48
|
+
readonly overlayIds?: string[];
|
|
49
|
+
hide: (page: PwPage) => Promise<void>;
|
|
50
|
+
show: (page: PwPage, request: CalloutRenderRequest) => Promise<void>;
|
|
139
51
|
}
|
|
140
52
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
height: rect.height,
|
|
145
|
-
left: rect.x,
|
|
146
|
-
right: rect.x + rect.width,
|
|
147
|
-
top: rect.y,
|
|
148
|
-
width: rect.width,
|
|
149
|
-
x: rect.x,
|
|
150
|
-
y: rect.y,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function __pw_create_virtual_element__(rect: CalloutTargetBox, kind: FloatingVirtualElement["kind"]): FloatingVirtualElement {
|
|
155
|
-
return {
|
|
156
|
-
height: rect.height,
|
|
157
|
-
kind,
|
|
158
|
-
width: rect.width,
|
|
159
|
-
x: rect.x,
|
|
160
|
-
y: rect.y,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function __pw_create_platform__(viewportWidth: number, viewportHeight: number) {
|
|
165
|
-
const offsetParent = {
|
|
166
|
-
clientHeight: viewportHeight,
|
|
167
|
-
clientLeft: 0,
|
|
168
|
-
clientTop: 0,
|
|
169
|
-
clientWidth: viewportWidth,
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
convertOffsetParentRelativeRectToViewportRelativeRect: ({ rect }: { rect: CalloutTargetBox }) => rect,
|
|
174
|
-
getClientRects: (element: FloatingVirtualElement) => [__pw_to_client_rect__(element)],
|
|
175
|
-
getClippingRect: () => ({
|
|
176
|
-
height: viewportHeight,
|
|
177
|
-
width: viewportWidth,
|
|
178
|
-
x: 0,
|
|
179
|
-
y: 0,
|
|
180
|
-
}),
|
|
181
|
-
getDimensions: (element: FloatingVirtualElement) => ({
|
|
182
|
-
height: element.height,
|
|
183
|
-
width: element.width,
|
|
184
|
-
}),
|
|
185
|
-
getDocumentElement: () => ({
|
|
186
|
-
clientHeight: viewportHeight,
|
|
187
|
-
clientWidth: viewportWidth,
|
|
188
|
-
}),
|
|
189
|
-
getElementRects: ({
|
|
190
|
-
floating,
|
|
191
|
-
reference,
|
|
192
|
-
}: {
|
|
193
|
-
floating: FloatingVirtualElement;
|
|
194
|
-
reference: FloatingVirtualElement;
|
|
195
|
-
strategy: string;
|
|
196
|
-
}) => ({
|
|
197
|
-
floating: {
|
|
198
|
-
height: floating.height,
|
|
199
|
-
width: floating.width,
|
|
200
|
-
x: 0,
|
|
201
|
-
y: 0,
|
|
202
|
-
},
|
|
203
|
-
reference: {
|
|
204
|
-
height: reference.height,
|
|
205
|
-
width: reference.width,
|
|
206
|
-
x: reference.x,
|
|
207
|
-
y: reference.y,
|
|
208
|
-
},
|
|
209
|
-
}),
|
|
210
|
-
getOffsetParent: () => offsetParent,
|
|
211
|
-
getScale: () => ({ x: 1, y: 1 }),
|
|
212
|
-
isElement: () => false,
|
|
213
|
-
isRTL: () => false,
|
|
214
|
-
};
|
|
53
|
+
export interface CalloutOptions {
|
|
54
|
+
extraOverlayIds?: string[];
|
|
55
|
+
renderer?: CalloutRenderer;
|
|
215
56
|
}
|
|
216
57
|
|
|
217
|
-
|
|
218
|
-
referenceRect: CalloutTargetBox,
|
|
219
|
-
floatingRect: CalloutTargetBox,
|
|
220
|
-
placement: Placement,
|
|
221
|
-
protectedRects: CalloutTargetBox[],
|
|
222
|
-
viewportWidth: number,
|
|
223
|
-
viewportHeight: number,
|
|
224
|
-
): Promise<{
|
|
225
|
-
adjustmentDistance: number;
|
|
226
|
-
arrowX: number | null;
|
|
227
|
-
arrowY: number | null;
|
|
228
|
-
placement: Placement;
|
|
229
|
-
x: number;
|
|
230
|
-
y: number;
|
|
231
|
-
}> {
|
|
232
|
-
const platform = __pw_create_platform__(viewportWidth, viewportHeight);
|
|
233
|
-
const floatingElement = __pw_create_virtual_element__(floatingRect, "floating");
|
|
234
|
-
const referenceElement = __pw_create_virtual_element__(referenceRect, "reference");
|
|
235
|
-
const result = await computePosition(referenceElement, floatingElement, {
|
|
236
|
-
middleware: [
|
|
237
|
-
offset(__PW_CURSOR_ANNOTATION_GAP__),
|
|
238
|
-
shift({
|
|
239
|
-
limiter: limitShift({}),
|
|
240
|
-
padding: __PW_CURSOR_ANNOTATION_MARGIN__,
|
|
241
|
-
}),
|
|
242
|
-
],
|
|
243
|
-
placement,
|
|
244
|
-
platform,
|
|
245
|
-
strategy: "fixed",
|
|
246
|
-
});
|
|
247
|
-
const resolvedPlacement = result.placement as Placement;
|
|
248
|
-
const { side } = __pw_parse_placement__(resolvedPlacement);
|
|
249
|
-
let x = Math.round(result.x);
|
|
250
|
-
let y = Math.round(result.y);
|
|
251
|
-
const baseX = x;
|
|
252
|
-
const baseY = y;
|
|
253
|
-
let layoutAdjustedForProtectedRect = false;
|
|
254
|
-
|
|
255
|
-
for (const protectedRect of protectedRects) {
|
|
256
|
-
const horizontalOverlap = x < protectedRect.x + protectedRect.width && x + floatingRect.width > protectedRect.x;
|
|
257
|
-
const verticalOverlap = y < protectedRect.y + protectedRect.height && y + floatingRect.height > protectedRect.y;
|
|
258
|
-
|
|
259
|
-
if ((side === "top" || side === "bottom") && horizontalOverlap) {
|
|
260
|
-
if (side === "top" && verticalOverlap) {
|
|
261
|
-
y = Math.min(y, protectedRect.y - floatingRect.height);
|
|
262
|
-
layoutAdjustedForProtectedRect = true;
|
|
263
|
-
}
|
|
264
|
-
if (side === "bottom" && verticalOverlap) {
|
|
265
|
-
y = Math.max(y, protectedRect.y + protectedRect.height);
|
|
266
|
-
layoutAdjustedForProtectedRect = true;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if ((side === "left" || side === "right") && verticalOverlap) {
|
|
271
|
-
if (side === "left" && horizontalOverlap) {
|
|
272
|
-
x = Math.min(x, protectedRect.x - floatingRect.width);
|
|
273
|
-
layoutAdjustedForProtectedRect = true;
|
|
274
|
-
}
|
|
275
|
-
if (side === "right" && horizontalOverlap) {
|
|
276
|
-
x = Math.max(x, protectedRect.x + protectedRect.width);
|
|
277
|
-
layoutAdjustedForProtectedRect = true;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
x = Math.min(
|
|
283
|
-
Math.max(x, __PW_CURSOR_ANNOTATION_MARGIN__),
|
|
284
|
-
Math.max(__PW_CURSOR_ANNOTATION_MARGIN__, viewportWidth - floatingRect.width - __PW_CURSOR_ANNOTATION_MARGIN__),
|
|
285
|
-
);
|
|
286
|
-
y = Math.min(
|
|
287
|
-
Math.max(y, __PW_CURSOR_ANNOTATION_MARGIN__),
|
|
288
|
-
Math.max(__PW_CURSOR_ANNOTATION_MARGIN__, viewportHeight - floatingRect.height - __PW_CURSOR_ANNOTATION_MARGIN__),
|
|
289
|
-
);
|
|
290
|
-
const adjustmentDistance = Math.abs(x - baseX) + Math.abs(y - baseY);
|
|
291
|
-
const referenceCenterX = referenceRect.x + referenceRect.width / 2;
|
|
292
|
-
const referenceCenterY = referenceRect.y + referenceRect.height / 2;
|
|
293
|
-
const arrowPadding = 10;
|
|
294
|
-
const arrowHalf = __PW_CURSOR_ANNOTATION_ARROW_SIZE__ / 2;
|
|
295
|
-
const middlewareData = result.middlewareData as { arrow?: { x?: number; y?: number } };
|
|
296
|
-
const baseArrowData = middlewareData.arrow;
|
|
297
|
-
const arrowX = side === "top" || side === "bottom"
|
|
298
|
-
? !layoutAdjustedForProtectedRect && typeof baseArrowData?.x === "number"
|
|
299
|
-
? Math.round(baseArrowData.x)
|
|
300
|
-
: Math.min(
|
|
301
|
-
Math.max(referenceCenterX - x - arrowHalf, arrowPadding),
|
|
302
|
-
Math.max(arrowPadding, floatingRect.width - __PW_CURSOR_ANNOTATION_ARROW_SIZE__ - arrowPadding),
|
|
303
|
-
)
|
|
304
|
-
: null;
|
|
305
|
-
const arrowY = side === "left" || side === "right"
|
|
306
|
-
? !layoutAdjustedForProtectedRect && typeof baseArrowData?.y === "number"
|
|
307
|
-
? Math.round(baseArrowData.y)
|
|
308
|
-
: Math.min(
|
|
309
|
-
Math.max(referenceCenterY - y - arrowHalf, arrowPadding),
|
|
310
|
-
Math.max(arrowPadding, floatingRect.height - __PW_CURSOR_ANNOTATION_ARROW_SIZE__ - arrowPadding),
|
|
311
|
-
)
|
|
312
|
-
: null;
|
|
313
|
-
|
|
58
|
+
export function measureCalloutBubble(text: string): { bubbleHeight: number; bubbleWidth: number } {
|
|
314
59
|
return {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
60
|
+
bubbleWidth: Math.min(
|
|
61
|
+
POINTER_CALLOUT_THEME.maxWidth,
|
|
62
|
+
Math.max(
|
|
63
|
+
POINTER_CALLOUT_THEME.minWidth,
|
|
64
|
+
Math.min(text.length, POINTER_CALLOUT_THEME.charsPerLine) * 7 + 44,
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
bubbleHeight: Math.max(
|
|
68
|
+
POINTER_CALLOUT_THEME.minHeight,
|
|
69
|
+
Math.ceil(Math.max(text.length, 1) / POINTER_CALLOUT_THEME.charsPerLine) * 20 + 24,
|
|
70
|
+
),
|
|
321
71
|
};
|
|
322
72
|
}
|
|
323
73
|
|
|
324
|
-
async function
|
|
325
|
-
targetRect: CalloutTargetBox,
|
|
326
|
-
floatingRect: CalloutTargetBox,
|
|
327
|
-
context: CalloutContext,
|
|
328
|
-
): Promise<CalloutLayout> {
|
|
329
|
-
const referenceRect = targetRect;
|
|
330
|
-
let bestLayout: (CalloutLayout & { score: number }) | null = null;
|
|
331
|
-
|
|
332
|
-
for (const placement of __PW_CURSOR_ALLOWED_PLACEMENTS__) {
|
|
333
|
-
const result = await __pw_compute_shifted_position__(
|
|
334
|
-
referenceRect,
|
|
335
|
-
floatingRect,
|
|
336
|
-
placement,
|
|
337
|
-
context.protectedTargetRects,
|
|
338
|
-
context.viewportWidth,
|
|
339
|
-
context.viewportHeight,
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
const positionedRect: CalloutTargetBox = {
|
|
343
|
-
x: result.x,
|
|
344
|
-
y: result.y,
|
|
345
|
-
width: floatingRect.width,
|
|
346
|
-
height: floatingRect.height,
|
|
347
|
-
};
|
|
348
|
-
const protectedOverlap = context.protectedTargetRects.reduce(
|
|
349
|
-
(sum, avoidRect) => sum + __pw_overlap_area__(positionedRect, avoidRect),
|
|
350
|
-
0,
|
|
351
|
-
);
|
|
352
|
-
const avoidOverlap = context.avoidRects.reduce(
|
|
353
|
-
(sum, avoidRect) => sum + __pw_overlap_area__(positionedRect, __pw_expand_rect__(avoidRect, __PW_CURSOR_ANNOTATION_AVOID_PADDING__)),
|
|
354
|
-
0,
|
|
355
|
-
);
|
|
356
|
-
const targetGap = __pw_rect_gap__(positionedRect, targetRect);
|
|
357
|
-
const score = (protectedOverlap * 10)
|
|
358
|
-
+ (avoidOverlap * 8)
|
|
359
|
-
+ (result.adjustmentDistance * 60)
|
|
360
|
-
+ (targetGap * 40)
|
|
361
|
-
+ (__pw_rect_center_distance__(positionedRect, referenceRect) * 0.08);
|
|
362
|
-
|
|
363
|
-
if (!bestLayout || score < bestLayout.score) {
|
|
364
|
-
bestLayout = {
|
|
365
|
-
arrowX: result.arrowX,
|
|
366
|
-
arrowY: result.arrowY,
|
|
367
|
-
placement: result.placement,
|
|
368
|
-
score,
|
|
369
|
-
staticSide: (() => {
|
|
370
|
-
const side = __pw_parse_placement__(result.placement).side;
|
|
371
|
-
if (side === "bottom") return "top";
|
|
372
|
-
if (side === "left") return "right";
|
|
373
|
-
if (side === "right") return "left";
|
|
374
|
-
return "bottom";
|
|
375
|
-
})(),
|
|
376
|
-
x: result.x,
|
|
377
|
-
y: result.y,
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (!bestLayout) {
|
|
383
|
-
return {
|
|
384
|
-
arrowX: null,
|
|
385
|
-
arrowY: null,
|
|
386
|
-
placement: "bottom",
|
|
387
|
-
staticSide: "top",
|
|
388
|
-
x: targetRect.x,
|
|
389
|
-
y: targetRect.y,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return bestLayout;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function __pw_get_callout_dimensions__(annotationText: string): { bubbleHeight: number; bubbleWidth: number } {
|
|
397
|
-
const charsPerLine = 28;
|
|
398
|
-
return {
|
|
399
|
-
bubbleWidth: Math.min(320, Math.max(180, Math.min(annotationText.length, charsPerLine) * 7 + 44)),
|
|
400
|
-
bubbleHeight: Math.max(52, Math.ceil(Math.max(annotationText.length, 1) / charsPerLine) * 20 + 24),
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
async function __pw_ensure_callout__(page: PwPage): Promise<void> {
|
|
405
|
-
const exists = await page.evaluate(
|
|
406
|
-
({ contentId, annotationId, arrowId }: { contentId: string; annotationId: string; arrowId: string }) =>
|
|
407
|
-
document.getElementById(annotationId) != null
|
|
408
|
-
&& document.getElementById(contentId) != null
|
|
409
|
-
&& document.getElementById(arrowId) != null,
|
|
410
|
-
{
|
|
411
|
-
arrowId: __PW_CURSOR_ANNOTATION_ARROW_ID__,
|
|
412
|
-
contentId: __PW_CURSOR_ANNOTATION_CONTENT_ID__,
|
|
413
|
-
annotationId: __PW_CURSOR_ANNOTATION_ID__,
|
|
414
|
-
},
|
|
415
|
-
);
|
|
416
|
-
if (exists) return;
|
|
417
|
-
|
|
74
|
+
async function __pw_ensure_simple_callout__(page: PwPage): Promise<void> {
|
|
418
75
|
await page.evaluate(
|
|
419
76
|
({
|
|
420
77
|
annotationId,
|
|
421
78
|
contentId,
|
|
422
|
-
arrowId,
|
|
423
|
-
arrowSize,
|
|
424
79
|
background,
|
|
425
80
|
border,
|
|
426
81
|
borderRadius,
|
|
@@ -429,16 +84,23 @@ async function __pw_ensure_callout__(page: PwPage): Promise<void> {
|
|
|
429
84
|
}: {
|
|
430
85
|
annotationId: string;
|
|
431
86
|
contentId: string;
|
|
432
|
-
arrowId: string;
|
|
433
|
-
arrowSize: number;
|
|
434
87
|
background: string;
|
|
435
88
|
border: string;
|
|
436
89
|
borderRadius: number;
|
|
437
90
|
boxShadow: string;
|
|
438
91
|
textColor: string;
|
|
439
92
|
}) => {
|
|
440
|
-
const
|
|
441
|
-
|
|
93
|
+
const ensureElement = <T extends HTMLElement>(id: string, tagName: keyof HTMLElementTagNameMap): T => {
|
|
94
|
+
const existing = document.getElementById(id);
|
|
95
|
+
if (existing instanceof HTMLElement) {
|
|
96
|
+
return existing as T;
|
|
97
|
+
}
|
|
98
|
+
const created = document.createElement(tagName);
|
|
99
|
+
created.id = id;
|
|
100
|
+
return created as T;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const annotation = ensureElement<HTMLDivElement>(annotationId, "div");
|
|
442
104
|
annotation.setAttribute(
|
|
443
105
|
"style",
|
|
444
106
|
[
|
|
@@ -461,64 +123,39 @@ async function __pw_ensure_callout__(page: PwPage): Promise<void> {
|
|
|
461
123
|
"white-space:normal",
|
|
462
124
|
"transform:translate3d(0,0,0)",
|
|
463
125
|
"transform-origin:center",
|
|
464
|
-
"isolation:isolate",
|
|
465
126
|
].join(";"),
|
|
466
127
|
);
|
|
467
128
|
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
contentEl.setAttribute("style", "position:relative;z-index:1;");
|
|
129
|
+
const content = ensureElement<HTMLDivElement>(contentId, "div");
|
|
130
|
+
content.setAttribute("style", "position:relative;z-index:1;");
|
|
471
131
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
"width:" + arrowSize + "px",
|
|
479
|
-
"height:" + arrowSize + "px",
|
|
480
|
-
"background:" + background,
|
|
481
|
-
"transform:rotate(45deg)",
|
|
482
|
-
"pointer-events:none",
|
|
483
|
-
"left:0",
|
|
484
|
-
"top:0",
|
|
485
|
-
"box-shadow:0 12px 24px rgba(15,23,42,0.18)",
|
|
486
|
-
"z-index:-1",
|
|
487
|
-
"opacity:0",
|
|
488
|
-
].join(";"),
|
|
489
|
-
);
|
|
490
|
-
|
|
491
|
-
annotation.appendChild(contentEl);
|
|
492
|
-
annotation.appendChild(arrowEl);
|
|
493
|
-
document.body.appendChild(annotation);
|
|
132
|
+
if (!annotation.isConnected) {
|
|
133
|
+
document.body.appendChild(annotation);
|
|
134
|
+
}
|
|
135
|
+
if (content.parentElement !== annotation) {
|
|
136
|
+
annotation.appendChild(content);
|
|
137
|
+
}
|
|
494
138
|
},
|
|
495
139
|
{
|
|
496
|
-
annotationId:
|
|
497
|
-
contentId:
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
boxShadow: __PW_CURSOR_ANNOTATION_BOX_SHADOW__,
|
|
504
|
-
textColor: __PW_CURSOR_ANNOTATION_TEXT_COLOR__,
|
|
140
|
+
annotationId: POINTER_CALLOUT_IDS.annotation,
|
|
141
|
+
contentId: POINTER_CALLOUT_IDS.content,
|
|
142
|
+
background: POINTER_CALLOUT_THEME.background,
|
|
143
|
+
border: POINTER_CALLOUT_THEME.border,
|
|
144
|
+
borderRadius: POINTER_CALLOUT_THEME.borderRadius,
|
|
145
|
+
boxShadow: POINTER_CALLOUT_THEME.boxShadow,
|
|
146
|
+
textColor: POINTER_CALLOUT_THEME.textColor,
|
|
505
147
|
},
|
|
506
148
|
);
|
|
507
149
|
}
|
|
508
150
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
return typeof target === "string" ? this.page.locator(target) : target;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
public async hide(): Promise<void> {
|
|
521
|
-
await this.page.evaluate(
|
|
151
|
+
const __pw_default_callout_renderer__: CalloutRenderer = {
|
|
152
|
+
overlayIds: [
|
|
153
|
+
POINTER_CALLOUT_IDS.annotation,
|
|
154
|
+
POINTER_CALLOUT_IDS.content,
|
|
155
|
+
POINTER_CALLOUT_IDS.arrow,
|
|
156
|
+
],
|
|
157
|
+
async hide(page) {
|
|
158
|
+
await page.evaluate(
|
|
522
159
|
({ annotationId, contentId, arrowId }: { annotationId: string; contentId: string; arrowId: string }) => {
|
|
523
160
|
const annotation = document.getElementById(annotationId) as HTMLDivElement | null;
|
|
524
161
|
const content = document.getElementById(contentId) as HTMLDivElement | null;
|
|
@@ -544,231 +181,42 @@ export class Callout {
|
|
|
544
181
|
}
|
|
545
182
|
},
|
|
546
183
|
{
|
|
547
|
-
annotationId:
|
|
548
|
-
contentId:
|
|
549
|
-
arrowId:
|
|
184
|
+
annotationId: POINTER_CALLOUT_IDS.annotation,
|
|
185
|
+
contentId: POINTER_CALLOUT_IDS.content,
|
|
186
|
+
arrowId: POINTER_CALLOUT_IDS.arrow,
|
|
550
187
|
},
|
|
551
188
|
);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
options?: ShowCalloutOptions,
|
|
558
|
-
): Promise<void> {
|
|
559
|
-
const text = annotationText.trim();
|
|
560
|
-
if (!text) {
|
|
561
|
-
await this.hide();
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const locator = this.toLocator(target);
|
|
566
|
-
if (!options?.skipScroll) {
|
|
567
|
-
try {
|
|
568
|
-
await locator.first().scrollIntoViewIfNeeded();
|
|
569
|
-
}
|
|
570
|
-
catch {
|
|
571
|
-
// Element may detach during navigation; the bounding-box lookup will surface the failure.
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const targetBox = options?.targetBox ?? await locator.first().boundingBox();
|
|
576
|
-
if (!targetBox) {
|
|
577
|
-
throw new Error("Callout.showForElement: target has no bounding box");
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
await __pw_ensure_callout__(this.page);
|
|
581
|
-
const { bubbleHeight, bubbleWidth } = __pw_get_callout_dimensions__(text);
|
|
582
|
-
const context = await this.page.evaluate(
|
|
189
|
+
},
|
|
190
|
+
async show(page, request) {
|
|
191
|
+
await __pw_ensure_simple_callout__(page);
|
|
192
|
+
const { bubbleHeight, bubbleWidth } = measureCalloutBubble(request.text);
|
|
193
|
+
await page.evaluate(
|
|
583
194
|
({
|
|
584
195
|
annotationId,
|
|
585
|
-
arrowId,
|
|
586
|
-
avoidSelector,
|
|
587
196
|
contentId,
|
|
588
|
-
cursorId,
|
|
589
|
-
ex,
|
|
590
|
-
ey,
|
|
591
|
-
}: {
|
|
592
|
-
annotationId: string;
|
|
593
|
-
arrowId: string;
|
|
594
|
-
avoidSelector: string;
|
|
595
|
-
contentId: string;
|
|
596
|
-
cursorId: string;
|
|
597
|
-
ex: number;
|
|
598
|
-
ey: number;
|
|
599
|
-
}) => {
|
|
600
|
-
type BrowserRect = { x: number; y: number; width: number; height: number };
|
|
601
|
-
|
|
602
|
-
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
|
|
603
|
-
const viewportWidth = Math.max(window.innerWidth || 0, document.documentElement.clientWidth, 1280);
|
|
604
|
-
const viewportHeight = Math.max(window.innerHeight || 0, document.documentElement.clientHeight, 720);
|
|
605
|
-
const viewportArea = viewportWidth * viewportHeight;
|
|
606
|
-
const overlayIds = new Set([annotationId, arrowId, contentId, cursorId]);
|
|
607
|
-
|
|
608
|
-
const toViewportRect = (candidateRect: { left: number; top: number; right: number; bottom: number } | DOMRect): BrowserRect | null => {
|
|
609
|
-
const left = clamp(candidateRect.left, 0, viewportWidth);
|
|
610
|
-
const top = clamp(candidateRect.top, 0, viewportHeight);
|
|
611
|
-
const right = clamp(candidateRect.right, 0, viewportWidth);
|
|
612
|
-
const bottom = clamp(candidateRect.bottom, 0, viewportHeight);
|
|
613
|
-
const width = right - left;
|
|
614
|
-
const height = bottom - top;
|
|
615
|
-
if (width < 24 || height < 24) {
|
|
616
|
-
return null;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
return { x: left, y: top, width, height };
|
|
620
|
-
};
|
|
621
|
-
|
|
622
|
-
const pushUniqueRect = (rects: BrowserRect[], rect: BrowserRect | null) => {
|
|
623
|
-
if (!rect) {
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
const duplicate = rects.some((existing) =>
|
|
628
|
-
Math.abs(existing.x - rect.x) < 1
|
|
629
|
-
&& Math.abs(existing.y - rect.y) < 1
|
|
630
|
-
&& Math.abs(existing.width - rect.width) < 1
|
|
631
|
-
&& Math.abs(existing.height - rect.height) < 1,
|
|
632
|
-
);
|
|
633
|
-
if (!duplicate) {
|
|
634
|
-
rects.push(rect);
|
|
635
|
-
}
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
const collectAvoidRects = (): BrowserRect[] =>
|
|
639
|
-
Array.from(document.querySelectorAll<HTMLElement>(avoidSelector))
|
|
640
|
-
.filter((candidate) => !overlayIds.has(candidate.id))
|
|
641
|
-
.flatMap((candidate) => {
|
|
642
|
-
const computedStyle = window.getComputedStyle(candidate);
|
|
643
|
-
if (
|
|
644
|
-
computedStyle.display === "none"
|
|
645
|
-
|| computedStyle.visibility === "hidden"
|
|
646
|
-
|| Number.parseFloat(computedStyle.opacity || "1") <= 0.05
|
|
647
|
-
) {
|
|
648
|
-
return [];
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const normalizedRect = toViewportRect(candidate.getBoundingClientRect());
|
|
652
|
-
if (!normalizedRect) {
|
|
653
|
-
return [];
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
if (!candidate.hasAttribute("data-callout-avoid") && normalizedRect.width * normalizedRect.height > viewportArea * 0.35) {
|
|
657
|
-
return [];
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
return [normalizedRect];
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
const collectProtectedTargetRects = (): BrowserRect[] => {
|
|
664
|
-
const protectedRects: BrowserRect[] = [];
|
|
665
|
-
if (typeof document.elementFromPoint !== "function") {
|
|
666
|
-
return protectedRects;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
const targetElement = document.elementFromPoint(ex, ey);
|
|
670
|
-
if (!(targetElement instanceof HTMLElement)) {
|
|
671
|
-
return protectedRects;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const targetRect = toViewportRect(targetElement.getBoundingClientRect());
|
|
675
|
-
pushUniqueRect(protectedRects, targetRect);
|
|
676
|
-
if (!targetRect) {
|
|
677
|
-
return protectedRects;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const targetArea = targetRect.width * targetRect.height;
|
|
681
|
-
let ancestor = targetElement.parentElement;
|
|
682
|
-
while (ancestor && ancestor !== document.body) {
|
|
683
|
-
const computedStyle = window.getComputedStyle(ancestor);
|
|
684
|
-
if (
|
|
685
|
-
computedStyle.display === "none"
|
|
686
|
-
|| computedStyle.visibility === "hidden"
|
|
687
|
-
|| Number.parseFloat(computedStyle.opacity || "1") <= 0.05
|
|
688
|
-
) {
|
|
689
|
-
ancestor = ancestor.parentElement;
|
|
690
|
-
continue;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const ancestorRect = toViewportRect(ancestor.getBoundingClientRect());
|
|
694
|
-
if (!ancestorRect) {
|
|
695
|
-
ancestor = ancestor.parentElement;
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
const ancestorArea = ancestorRect.width * ancestorRect.height;
|
|
700
|
-
const clearlyBiggerThanTarget = ancestorArea >= Math.max(targetArea * 1.75, 18_000);
|
|
701
|
-
const stillReasonablyLocal = ancestorArea <= viewportArea * 0.28;
|
|
702
|
-
const containsTarget = ancestorRect.x <= targetRect.x
|
|
703
|
-
&& ancestorRect.y <= targetRect.y
|
|
704
|
-
&& ancestorRect.x + ancestorRect.width >= targetRect.x + targetRect.width
|
|
705
|
-
&& ancestorRect.y + ancestorRect.height >= targetRect.y + targetRect.height;
|
|
706
|
-
if (clearlyBiggerThanTarget && stillReasonablyLocal && containsTarget) {
|
|
707
|
-
pushUniqueRect(protectedRects, ancestorRect);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (protectedRects.length >= 3) {
|
|
711
|
-
break;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
ancestor = ancestor.parentElement;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return protectedRects;
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
return {
|
|
721
|
-
avoidRects: collectAvoidRects(),
|
|
722
|
-
protectedTargetRects: collectProtectedTargetRects(),
|
|
723
|
-
viewportHeight,
|
|
724
|
-
viewportWidth,
|
|
725
|
-
};
|
|
726
|
-
},
|
|
727
|
-
{
|
|
728
|
-
annotationId: __PW_CURSOR_ANNOTATION_ID__,
|
|
729
|
-
arrowId: __PW_CURSOR_ANNOTATION_ARROW_ID__,
|
|
730
|
-
avoidSelector: __PW_CURSOR_ANNOTATION_AVOID_SELECTOR__,
|
|
731
|
-
contentId: __PW_CURSOR_ANNOTATION_CONTENT_ID__,
|
|
732
|
-
cursorId: __PW_CURSOR_ID__,
|
|
733
|
-
ex: targetBox.x + targetBox.width / 2,
|
|
734
|
-
ey: targetBox.y + targetBox.height / 2,
|
|
735
|
-
},
|
|
736
|
-
);
|
|
737
|
-
const layout = await __pw_compute_callout_layout__(
|
|
738
|
-
{ x: targetBox.x, y: targetBox.y, width: targetBox.width, height: targetBox.height },
|
|
739
|
-
{ x: 0, y: 0, width: bubbleWidth, height: bubbleHeight },
|
|
740
|
-
context,
|
|
741
|
-
);
|
|
742
|
-
|
|
743
|
-
await this.page.evaluate(
|
|
744
|
-
({
|
|
745
|
-
annotationId,
|
|
746
197
|
arrowId,
|
|
747
|
-
arrowSize,
|
|
748
|
-
background,
|
|
749
|
-
border,
|
|
750
|
-
borderRadius,
|
|
751
198
|
bubbleHeight,
|
|
752
199
|
bubbleWidth,
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
200
|
+
border,
|
|
201
|
+
borderRadius,
|
|
202
|
+
background,
|
|
203
|
+
gap,
|
|
757
204
|
text,
|
|
205
|
+
targetBox,
|
|
206
|
+
margin,
|
|
758
207
|
}: {
|
|
759
208
|
annotationId: string;
|
|
209
|
+
contentId: string;
|
|
760
210
|
arrowId: string;
|
|
761
|
-
arrowSize: number;
|
|
762
|
-
background: string;
|
|
763
|
-
border: string;
|
|
764
|
-
borderRadius: number;
|
|
765
211
|
bubbleHeight: number;
|
|
766
212
|
bubbleWidth: number;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
213
|
+
border: string;
|
|
214
|
+
borderRadius: number;
|
|
215
|
+
background: string;
|
|
216
|
+
gap: number;
|
|
771
217
|
text: string;
|
|
218
|
+
targetBox: CalloutTargetBox;
|
|
219
|
+
margin: number;
|
|
772
220
|
}) => {
|
|
773
221
|
const annotation = document.getElementById(annotationId) as HTMLDivElement | null;
|
|
774
222
|
const content = document.getElementById(contentId) as HTMLDivElement | null;
|
|
@@ -777,6 +225,52 @@ export class Callout {
|
|
|
777
225
|
return;
|
|
778
226
|
}
|
|
779
227
|
|
|
228
|
+
const viewportWidth = Math.max(window.innerWidth || 0, document.documentElement.clientWidth, 1280);
|
|
229
|
+
const viewportHeight = Math.max(window.innerHeight || 0, document.documentElement.clientHeight, 720);
|
|
230
|
+
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
|
|
231
|
+
const targetCenterX = targetBox.x + targetBox.width / 2;
|
|
232
|
+
const targetCenterY = targetBox.y + targetBox.height / 2;
|
|
233
|
+
const maxX = Math.max(margin, viewportWidth - bubbleWidth - margin);
|
|
234
|
+
const maxY = Math.max(margin, viewportHeight - bubbleHeight - margin);
|
|
235
|
+
const candidates = [
|
|
236
|
+
{
|
|
237
|
+
placement: "right",
|
|
238
|
+
x: targetBox.x + targetBox.width + gap,
|
|
239
|
+
y: targetCenterY - bubbleHeight / 2,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
placement: "bottom",
|
|
243
|
+
x: targetCenterX - bubbleWidth / 2,
|
|
244
|
+
y: targetBox.y + targetBox.height + gap,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
placement: "left",
|
|
248
|
+
x: targetBox.x - bubbleWidth - gap,
|
|
249
|
+
y: targetCenterY - bubbleHeight / 2,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
placement: "top",
|
|
253
|
+
x: targetCenterX - bubbleWidth / 2,
|
|
254
|
+
y: targetBox.y - bubbleHeight - gap,
|
|
255
|
+
},
|
|
256
|
+
] as const;
|
|
257
|
+
|
|
258
|
+
let placement = "center";
|
|
259
|
+
let resolvedX = clamp(targetCenterX - bubbleWidth / 2, margin, maxX);
|
|
260
|
+
let resolvedY = clamp(targetCenterY - bubbleHeight / 2, margin, maxY);
|
|
261
|
+
|
|
262
|
+
for (const candidate of candidates) {
|
|
263
|
+
const clampedX = clamp(candidate.x, margin, maxX);
|
|
264
|
+
const clampedY = clamp(candidate.y, margin, maxY);
|
|
265
|
+
const fitsWithoutShift = Math.abs(clampedX - candidate.x) < 1 && Math.abs(clampedY - candidate.y) < 1;
|
|
266
|
+
if (fitsWithoutShift) {
|
|
267
|
+
placement = candidate.placement;
|
|
268
|
+
resolvedX = candidate.x;
|
|
269
|
+
resolvedY = candidate.y;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
780
274
|
content.textContent = text;
|
|
781
275
|
annotation.style.width = `${bubbleWidth}px`;
|
|
782
276
|
annotation.style.minHeight = `${bubbleHeight}px`;
|
|
@@ -785,43 +279,99 @@ export class Callout {
|
|
|
785
279
|
annotation.style.borderRadius = `${borderRadius}px`;
|
|
786
280
|
annotation.style.transition = "opacity 120ms ease-in-out, transform 160ms ease-in-out";
|
|
787
281
|
annotation.style.willChange = "left, top, opacity, transform";
|
|
788
|
-
annotation.style.left = `${
|
|
789
|
-
annotation.style.top = `${
|
|
282
|
+
annotation.style.left = `${Math.round(resolvedX)}px`;
|
|
283
|
+
annotation.style.top = `${Math.round(resolvedY)}px`;
|
|
790
284
|
annotation.style.opacity = "1";
|
|
791
285
|
annotation.style.transform = "scale(1)";
|
|
792
|
-
annotation.setAttribute("data-placement",
|
|
793
|
-
|
|
794
|
-
|
|
286
|
+
annotation.setAttribute("data-placement", placement);
|
|
287
|
+
if (arrow) {
|
|
288
|
+
arrow.style.opacity = "0";
|
|
795
289
|
arrow.style.left = "";
|
|
796
290
|
arrow.style.top = "";
|
|
797
291
|
arrow.style.right = "";
|
|
798
292
|
arrow.style.bottom = "";
|
|
799
|
-
arrow.style.transform = "rotate(45deg)";
|
|
800
|
-
if (layout.arrowX !== null) {
|
|
801
|
-
arrow.style.left = `${layout.arrowX}px`;
|
|
802
|
-
}
|
|
803
|
-
if (layout.arrowY !== null) {
|
|
804
|
-
arrow.style.top = `${layout.arrowY}px`;
|
|
805
|
-
}
|
|
806
|
-
arrow.style.setProperty(layout.staticSide, `${Math.round(arrowSize / -2)}px`);
|
|
807
|
-
arrow.style.opacity = "1";
|
|
808
293
|
}
|
|
809
294
|
},
|
|
810
295
|
{
|
|
811
|
-
annotationId:
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
background: __PW_CURSOR_ANNOTATION_BACKGROUND__,
|
|
815
|
-
border: __PW_CURSOR_ANNOTATION_BORDER__,
|
|
816
|
-
borderRadius: __PW_CURSOR_ANNOTATION_RADIUS__,
|
|
296
|
+
annotationId: POINTER_CALLOUT_IDS.annotation,
|
|
297
|
+
contentId: POINTER_CALLOUT_IDS.content,
|
|
298
|
+
arrowId: POINTER_CALLOUT_IDS.arrow,
|
|
817
299
|
bubbleHeight,
|
|
818
300
|
bubbleWidth,
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
text,
|
|
301
|
+
border: POINTER_CALLOUT_THEME.border,
|
|
302
|
+
borderRadius: POINTER_CALLOUT_THEME.borderRadius,
|
|
303
|
+
background: POINTER_CALLOUT_THEME.background,
|
|
304
|
+
gap: POINTER_CALLOUT_THEME.gap,
|
|
305
|
+
text: request.text,
|
|
306
|
+
targetBox: request.targetBox,
|
|
307
|
+
margin: POINTER_CALLOUT_THEME.margin,
|
|
824
308
|
},
|
|
825
309
|
);
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export const simpleCalloutRenderer = __pw_default_callout_renderer__;
|
|
314
|
+
|
|
315
|
+
export class Callout {
|
|
316
|
+
private readonly page: PwPage;
|
|
317
|
+
private readonly extraOverlayIds: string[];
|
|
318
|
+
private readonly renderer: CalloutRenderer;
|
|
319
|
+
|
|
320
|
+
public constructor(page: PwPage, options?: CalloutOptions) {
|
|
321
|
+
this.page = page;
|
|
322
|
+
this.extraOverlayIds = options?.extraOverlayIds ?? [];
|
|
323
|
+
this.renderer = options?.renderer ?? __pw_default_callout_renderer__;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private toLocator(target: ElementTarget): PwLocator {
|
|
327
|
+
return typeof target === "string" ? this.page.locator(target) : target;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
public async hide(): Promise<void> {
|
|
331
|
+
await this.renderer.hide(this.page);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
public async showForElement(
|
|
335
|
+
target: ElementTarget,
|
|
336
|
+
annotationText: string,
|
|
337
|
+
options?: ShowCalloutOptions,
|
|
338
|
+
): Promise<void> {
|
|
339
|
+
const text = annotationText.trim();
|
|
340
|
+
if (!text) {
|
|
341
|
+
await this.hide();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const locator = this.toLocator(target);
|
|
346
|
+
if (!options?.skipScroll) {
|
|
347
|
+
try {
|
|
348
|
+
await locator.first().scrollIntoViewIfNeeded();
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// Element may detach during navigation; the bounding-box lookup will surface the failure.
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const targetBox = options?.targetBox ?? await locator.first().boundingBox();
|
|
356
|
+
if (!targetBox) {
|
|
357
|
+
throw new Error("Callout.showForElement: target has no bounding box");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const overlayIds = Array.from(new Set([
|
|
361
|
+
...(this.renderer.overlayIds ?? []),
|
|
362
|
+
...this.extraOverlayIds,
|
|
363
|
+
]));
|
|
364
|
+
|
|
365
|
+
await this.renderer.show(this.page, {
|
|
366
|
+
overlayIds,
|
|
367
|
+
target,
|
|
368
|
+
targetBox: {
|
|
369
|
+
height: targetBox.height,
|
|
370
|
+
width: targetBox.width,
|
|
371
|
+
x: targetBox.x,
|
|
372
|
+
y: targetBox.y,
|
|
373
|
+
},
|
|
374
|
+
text,
|
|
375
|
+
});
|
|
826
376
|
}
|
|
827
377
|
}
|