@bitvea/feedback-toolbar 0.0.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 +262 -0
- package/dist/FeedbackPanel.d.ts +117 -0
- package/dist/FeedbackPanel.d.ts.map +1 -0
- package/dist/FeedbackPanel.js +622 -0
- package/dist/FeedbackPanel.js.map +1 -0
- package/dist/FeedbackToolbar.d.ts +54 -0
- package/dist/FeedbackToolbar.d.ts.map +1 -0
- package/dist/FeedbackToolbar.js +1149 -0
- package/dist/FeedbackToolbar.js.map +1 -0
- package/dist/InboxSheet.d.ts +80 -0
- package/dist/InboxSheet.d.ts.map +1 -0
- package/dist/InboxSheet.js +580 -0
- package/dist/InboxSheet.js.map +1 -0
- package/dist/PinLayer.d.ts +59 -0
- package/dist/PinLayer.d.ts.map +1 -0
- package/dist/PinLayer.js +1702 -0
- package/dist/PinLayer.js.map +1 -0
- package/dist/RichText.d.ts +6 -0
- package/dist/RichText.d.ts.map +1 -0
- package/dist/RichText.js +135 -0
- package/dist/RichText.js.map +1 -0
- package/dist/browser/entry.d.ts +3 -0
- package/dist/browser/entry.d.ts.map +1 -0
- package/dist/browser/entry.js +13 -0
- package/dist/browser/entry.js.map +1 -0
- package/dist/browser/mount.d.ts +37 -0
- package/dist/browser/mount.d.ts.map +1 -0
- package/dist/browser/mount.js +150 -0
- package/dist/browser/mount.js.map +1 -0
- package/dist/browser/toolbar.js +38 -0
- package/dist/browser/toolbar.js.map +7 -0
- package/dist/capture.d.ts +58 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +98 -0
- package/dist/capture.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +196 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/fs.d.ts +16 -0
- package/dist/cli/fs.d.ts.map +1 -0
- package/dist/cli/fs.js +33 -0
- package/dist/cli/fs.js.map +1 -0
- package/dist/cli/init.d.ts +75 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +266 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/register.d.ts +55 -0
- package/dist/cli/register.d.ts.map +1 -0
- package/dist/cli/register.js +84 -0
- package/dist/cli/register.js.map +1 -0
- package/dist/cli/registration.d.ts +37 -0
- package/dist/cli/registration.d.ts.map +1 -0
- package/dist/cli/registration.js +69 -0
- package/dist/cli/registration.js.map +1 -0
- package/dist/cli/strategies.d.ts +98 -0
- package/dist/cli/strategies.d.ts.map +1 -0
- package/dist/cli/strategies.js +198 -0
- package/dist/cli/strategies.js.map +1 -0
- package/dist/client.d.ts +59 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +113 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +92 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +65 -0
- package/dist/config.js.map +1 -0
- package/dist/deepLink.d.ts +86 -0
- package/dist/deepLink.d.ts.map +1 -0
- package/dist/deepLink.js +185 -0
- package/dist/deepLink.js.map +1 -0
- package/dist/fontFace.d.ts +70 -0
- package/dist/fontFace.d.ts.map +1 -0
- package/dist/fontFace.js +88 -0
- package/dist/fontFace.js.map +1 -0
- package/dist/gating.d.ts +32 -0
- package/dist/gating.d.ts.map +1 -0
- package/dist/gating.js +49 -0
- package/dist/gating.js.map +1 -0
- package/dist/icons.d.ts +68 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +97 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +59 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +130 -0
- package/dist/markdown.js.map +1 -0
- package/dist/motion.d.ts +178 -0
- package/dist/motion.d.ts.map +1 -0
- package/dist/motion.js +302 -0
- package/dist/motion.js.map +1 -0
- package/dist/preferences.d.ts +71 -0
- package/dist/preferences.d.ts.map +1 -0
- package/dist/preferences.js +195 -0
- package/dist/preferences.js.map +1 -0
- package/dist/rasterise.d.ts +45 -0
- package/dist/rasterise.d.ts.map +1 -0
- package/dist/rasterise.js +490 -0
- package/dist/rasterise.js.map +1 -0
- package/dist/selector.d.ts +19 -0
- package/dist/selector.d.ts.map +1 -0
- package/dist/selector.js +142 -0
- package/dist/selector.js.map +1 -0
- package/dist/session.d.ts +19 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +36 -0
- package/dist/session.js.map +1 -0
- package/dist/shortcut.d.ts +88 -0
- package/dist/shortcut.d.ts.map +1 -0
- package/dist/shortcut.js +149 -0
- package/dist/shortcut.js.map +1 -0
- package/dist/tokens.d.ts +709 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +749 -0
- package/dist/tokens.js.map +1 -0
- package/dist/vendor/types/account.d.ts +26 -0
- package/dist/vendor/types/account.d.ts.map +1 -0
- package/dist/vendor/types/account.js +23 -0
- package/dist/vendor/types/account.js.map +1 -0
- package/dist/vendor/types/apiKey.d.ts +38 -0
- package/dist/vendor/types/apiKey.d.ts.map +1 -0
- package/dist/vendor/types/apiKey.js +36 -0
- package/dist/vendor/types/apiKey.js.map +1 -0
- package/dist/vendor/types/auth.d.ts +10 -0
- package/dist/vendor/types/auth.d.ts.map +1 -0
- package/dist/vendor/types/auth.js +15 -0
- package/dist/vendor/types/auth.js.map +1 -0
- package/dist/vendor/types/entity.d.ts +74 -0
- package/dist/vendor/types/entity.d.ts.map +1 -0
- package/dist/vendor/types/entity.js +111 -0
- package/dist/vendor/types/entity.js.map +1 -0
- package/dist/vendor/types/feedback.d.ts +285 -0
- package/dist/vendor/types/feedback.d.ts.map +1 -0
- package/dist/vendor/types/feedback.js +286 -0
- package/dist/vendor/types/feedback.js.map +1 -0
- package/dist/vendor/types/index.d.ts +14 -0
- package/dist/vendor/types/index.d.ts.map +1 -0
- package/dist/vendor/types/index.js +16 -0
- package/dist/vendor/types/index.js.map +1 -0
- package/dist/vendor/types/invite.d.ts +42 -0
- package/dist/vendor/types/invite.d.ts.map +1 -0
- package/dist/vendor/types/invite.js +30 -0
- package/dist/vendor/types/invite.js.map +1 -0
- package/dist/vendor/types/mcpToken.d.ts +38 -0
- package/dist/vendor/types/mcpToken.d.ts.map +1 -0
- package/dist/vendor/types/mcpToken.js +39 -0
- package/dist/vendor/types/mcpToken.js.map +1 -0
- package/dist/vendor/types/organization.d.ts +13 -0
- package/dist/vendor/types/organization.d.ts.map +1 -0
- package/dist/vendor/types/organization.js +12 -0
- package/dist/vendor/types/organization.js.map +1 -0
- package/dist/vendor/types/project.d.ts +73 -0
- package/dist/vendor/types/project.d.ts.map +1 -0
- package/dist/vendor/types/project.js +117 -0
- package/dist/vendor/types/project.js.map +1 -0
- package/dist/vendor/types/role.d.ts +12 -0
- package/dist/vendor/types/role.d.ts.map +1 -0
- package/dist/vendor/types/role.js +20 -0
- package/dist/vendor/types/role.js.map +1 -0
- package/dist/vendor/types/roleDefinition.d.ts +61 -0
- package/dist/vendor/types/roleDefinition.d.ts.map +1 -0
- package/dist/vendor/types/roleDefinition.js +45 -0
- package/dist/vendor/types/roleDefinition.js.map +1 -0
- package/dist/vendor/types/team.d.ts +25 -0
- package/dist/vendor/types/team.d.ts.map +1 -0
- package/dist/vendor/types/team.js +17 -0
- package/dist/vendor/types/team.js.map +1 -0
- package/dist/vendor/types/util.d.ts +39 -0
- package/dist/vendor/types/util.d.ts.map +1 -0
- package/dist/vendor/types/util.js +31 -0
- package/dist/vendor/types/util.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
// Turning the current viewport into pixels, with no permission prompt and no
|
|
2
|
+
// dependency.
|
|
3
|
+
//
|
|
4
|
+
// The technique is the platform's own: clone the visible DOM, write the styles
|
|
5
|
+
// that matter onto the clones as inline style, wrap the result in an SVG
|
|
6
|
+
// `<foreignObject>`, and let the browser decode that SVG as an image and draw
|
|
7
|
+
// it into a canvas. Everything here is synchronous DOM work plus one image
|
|
8
|
+
// decode, so it costs the reviewer nothing and asks them for nothing.
|
|
9
|
+
//
|
|
10
|
+
// Known and accepted limits, all of them consequences of how an SVG image is
|
|
11
|
+
// rendered - it is a sealed document that may not load external resources:
|
|
12
|
+
//
|
|
13
|
+
// - Web fonts do not load, so text falls back down its font stack. Metrics
|
|
14
|
+
// shift; the layout does not.
|
|
15
|
+
// - Cross-origin images and `background-image` URLs do not load. Same-origin
|
|
16
|
+
// and CORS-enabled `<img>` content is inlined below, so the common case
|
|
17
|
+
// survives; the rest render as empty boxes rather than broken icons.
|
|
18
|
+
// - Only the properties in `COPIED_PROPERTIES` are carried over. Copying all
|
|
19
|
+
// ~340 computed properties per element is correct but produces megabytes of
|
|
20
|
+
// CSS for an ordinary page, and the data URL that has to carry it grows
|
|
21
|
+
// faster still. The allowlist is the visual set.
|
|
22
|
+
//
|
|
23
|
+
// The spec puts cross-browser screenshot fidelity under exotic CSS out of scope
|
|
24
|
+
// for v1, and these are exactly that class of gap. A screenshot that is
|
|
25
|
+
// recognisably the page, taken silently, beats a pixel-exact one that costs a
|
|
26
|
+
// modal dialog per submission.
|
|
27
|
+
/** Elements that never contribute pixels, or that must not be in the shot. */
|
|
28
|
+
const SKIPPED_TAGS = new Set([
|
|
29
|
+
"SCRIPT",
|
|
30
|
+
"STYLE",
|
|
31
|
+
"LINK",
|
|
32
|
+
"META",
|
|
33
|
+
"TITLE",
|
|
34
|
+
"NOSCRIPT",
|
|
35
|
+
"TEMPLATE",
|
|
36
|
+
"BASE",
|
|
37
|
+
]);
|
|
38
|
+
/**
|
|
39
|
+
* The toolbar's own root carries this attribute. A screenshot of the page the
|
|
40
|
+
* reviewer is reporting on should not have the reporting UI sitting in it.
|
|
41
|
+
*/
|
|
42
|
+
const TOOLBAR_ROOT_ATTRIBUTE = "data-bitvea-feedback-toolbar";
|
|
43
|
+
/**
|
|
44
|
+
* Past this many elements the clone-and-restyle walk stops being something a
|
|
45
|
+
* reviewer would sit through. Bail rather than freeze the page they are trying
|
|
46
|
+
* to comment on; a null screenshot is a supported outcome, a hung tab is not.
|
|
47
|
+
*/
|
|
48
|
+
const MAX_ELEMENTS = 12_000;
|
|
49
|
+
/**
|
|
50
|
+
* Attributes worth carrying onto a clone. Everything else either only mattered
|
|
51
|
+
* through a stylesheet the SVG document cannot see, or is a framework
|
|
52
|
+
* attribute whose name is not valid XML.
|
|
53
|
+
*/
|
|
54
|
+
const KEPT_ATTRIBUTES = new Set([
|
|
55
|
+
"colspan",
|
|
56
|
+
"rowspan",
|
|
57
|
+
"span",
|
|
58
|
+
"dir",
|
|
59
|
+
"alt",
|
|
60
|
+
"start",
|
|
61
|
+
"reversed",
|
|
62
|
+
"type",
|
|
63
|
+
"value",
|
|
64
|
+
"checked",
|
|
65
|
+
"selected",
|
|
66
|
+
"open",
|
|
67
|
+
"lang",
|
|
68
|
+
]);
|
|
69
|
+
/** How long to wait for the browser to decode the SVG before giving up. */
|
|
70
|
+
const DECODE_TIMEOUT_MS = 5_000;
|
|
71
|
+
/**
|
|
72
|
+
* The properties written onto each clone. Longhands only: `getComputedStyle`
|
|
73
|
+
* resolves to longhands, and asking it for a shorthand is not portable.
|
|
74
|
+
*/
|
|
75
|
+
const COPIED_PROPERTIES = [
|
|
76
|
+
// Box
|
|
77
|
+
"display",
|
|
78
|
+
"position",
|
|
79
|
+
"top",
|
|
80
|
+
"right",
|
|
81
|
+
"bottom",
|
|
82
|
+
"left",
|
|
83
|
+
"float",
|
|
84
|
+
"clear",
|
|
85
|
+
"z-index",
|
|
86
|
+
"box-sizing",
|
|
87
|
+
"width",
|
|
88
|
+
"height",
|
|
89
|
+
"min-width",
|
|
90
|
+
"min-height",
|
|
91
|
+
"max-width",
|
|
92
|
+
"max-height",
|
|
93
|
+
"aspect-ratio",
|
|
94
|
+
"margin-top",
|
|
95
|
+
"margin-right",
|
|
96
|
+
"margin-bottom",
|
|
97
|
+
"margin-left",
|
|
98
|
+
"padding-top",
|
|
99
|
+
"padding-right",
|
|
100
|
+
"padding-bottom",
|
|
101
|
+
"padding-left",
|
|
102
|
+
"overflow-x",
|
|
103
|
+
"overflow-y",
|
|
104
|
+
"visibility",
|
|
105
|
+
"opacity",
|
|
106
|
+
// Border and outline
|
|
107
|
+
"border-top-width",
|
|
108
|
+
"border-right-width",
|
|
109
|
+
"border-bottom-width",
|
|
110
|
+
"border-left-width",
|
|
111
|
+
"border-top-style",
|
|
112
|
+
"border-right-style",
|
|
113
|
+
"border-bottom-style",
|
|
114
|
+
"border-left-style",
|
|
115
|
+
"border-top-color",
|
|
116
|
+
"border-right-color",
|
|
117
|
+
"border-bottom-color",
|
|
118
|
+
"border-left-color",
|
|
119
|
+
"border-top-left-radius",
|
|
120
|
+
"border-top-right-radius",
|
|
121
|
+
"border-bottom-left-radius",
|
|
122
|
+
"border-bottom-right-radius",
|
|
123
|
+
"outline-width",
|
|
124
|
+
"outline-style",
|
|
125
|
+
"outline-color",
|
|
126
|
+
"outline-offset",
|
|
127
|
+
// Paint
|
|
128
|
+
"background-color",
|
|
129
|
+
"background-image",
|
|
130
|
+
"background-size",
|
|
131
|
+
"background-position",
|
|
132
|
+
"background-repeat",
|
|
133
|
+
"background-clip",
|
|
134
|
+
"background-origin",
|
|
135
|
+
"box-shadow",
|
|
136
|
+
"filter",
|
|
137
|
+
"mix-blend-mode",
|
|
138
|
+
"clip-path",
|
|
139
|
+
"transform",
|
|
140
|
+
"transform-origin",
|
|
141
|
+
// Type
|
|
142
|
+
"color",
|
|
143
|
+
"font-family",
|
|
144
|
+
"font-size",
|
|
145
|
+
"font-style",
|
|
146
|
+
"font-weight",
|
|
147
|
+
"font-variant",
|
|
148
|
+
"font-stretch",
|
|
149
|
+
"letter-spacing",
|
|
150
|
+
"line-height",
|
|
151
|
+
"text-align",
|
|
152
|
+
"text-decoration-line",
|
|
153
|
+
"text-decoration-color",
|
|
154
|
+
"text-decoration-style",
|
|
155
|
+
"text-indent",
|
|
156
|
+
"text-transform",
|
|
157
|
+
"text-shadow",
|
|
158
|
+
"white-space",
|
|
159
|
+
"word-break",
|
|
160
|
+
"overflow-wrap",
|
|
161
|
+
"vertical-align",
|
|
162
|
+
"text-overflow",
|
|
163
|
+
"direction",
|
|
164
|
+
// Flex and grid
|
|
165
|
+
"flex-direction",
|
|
166
|
+
"flex-wrap",
|
|
167
|
+
"flex-grow",
|
|
168
|
+
"flex-shrink",
|
|
169
|
+
"flex-basis",
|
|
170
|
+
"justify-content",
|
|
171
|
+
"justify-items",
|
|
172
|
+
"justify-self",
|
|
173
|
+
"align-content",
|
|
174
|
+
"align-items",
|
|
175
|
+
"align-self",
|
|
176
|
+
"order",
|
|
177
|
+
"gap",
|
|
178
|
+
"row-gap",
|
|
179
|
+
"column-gap",
|
|
180
|
+
"grid-template-columns",
|
|
181
|
+
"grid-template-rows",
|
|
182
|
+
"grid-auto-flow",
|
|
183
|
+
"grid-auto-columns",
|
|
184
|
+
"grid-auto-rows",
|
|
185
|
+
"grid-column-start",
|
|
186
|
+
"grid-column-end",
|
|
187
|
+
"grid-row-start",
|
|
188
|
+
"grid-row-end",
|
|
189
|
+
// Tables, lists, media
|
|
190
|
+
"table-layout",
|
|
191
|
+
"border-collapse",
|
|
192
|
+
"border-spacing",
|
|
193
|
+
"caption-side",
|
|
194
|
+
"list-style-type",
|
|
195
|
+
"list-style-position",
|
|
196
|
+
"object-fit",
|
|
197
|
+
"object-position",
|
|
198
|
+
"fill",
|
|
199
|
+
"stroke",
|
|
200
|
+
"stroke-width",
|
|
201
|
+
];
|
|
202
|
+
/**
|
|
203
|
+
* Rasterise the visible viewport to a PNG data URL, or null if the browser
|
|
204
|
+
* would not produce one.
|
|
205
|
+
*
|
|
206
|
+
* @param maxDataUrlLength Longest data URL the API will accept. The image is
|
|
207
|
+
* re-encoded at half scale, twice, before giving up, so a very large or very
|
|
208
|
+
* dense viewport degrades in quality rather than disappearing.
|
|
209
|
+
*/
|
|
210
|
+
export async function rasteriseViewport(maxDataUrlLength) {
|
|
211
|
+
return rasteriseRegion(maxDataUrlLength, null);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Rasterise ONE element to a PNG data URL, or null if the browser would not
|
|
215
|
+
* produce one.
|
|
216
|
+
*
|
|
217
|
+
* A reviewer pointing at a broken button wants a picture of the button, not of
|
|
218
|
+
* the page it is on: the whole-viewport shot puts the thing they are reporting
|
|
219
|
+
* somewhere in a 1512x916 image and leaves the reader to find it. This is the
|
|
220
|
+
* camera in the composer.
|
|
221
|
+
*
|
|
222
|
+
* An element with no box - `display: none`, or detached - yields null rather
|
|
223
|
+
* than a 1x1 image, because a blank attachment is worse than none.
|
|
224
|
+
*/
|
|
225
|
+
export async function rasteriseElement(element, maxDataUrlLength) {
|
|
226
|
+
const rect = element.getBoundingClientRect();
|
|
227
|
+
if (rect.width < 1 || rect.height < 1)
|
|
228
|
+
return null;
|
|
229
|
+
return rasteriseRegion(maxDataUrlLength, rect);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The shared half. `region` is null for the viewport, or a rectangle in
|
|
233
|
+
* viewport coordinates for one element.
|
|
234
|
+
*
|
|
235
|
+
* @param maxDataUrlLength Longest data URL the API will accept. The image is
|
|
236
|
+
* re-encoded at half scale, twice, before giving up, so a very large or very
|
|
237
|
+
* dense capture degrades in quality rather than disappearing.
|
|
238
|
+
*/
|
|
239
|
+
async function rasteriseRegion(maxDataUrlLength, region) {
|
|
240
|
+
if (typeof document === "undefined" || !document.body)
|
|
241
|
+
return null;
|
|
242
|
+
const width = Math.max(1, Math.floor(region ? region.width : window.innerWidth));
|
|
243
|
+
const height = Math.max(1, Math.floor(region ? region.height : window.innerHeight));
|
|
244
|
+
const origin = region ? { x: region.x, y: region.y } : { x: 0, y: 0 };
|
|
245
|
+
// Ask for the drawing surface before doing any work: where there is no 2D
|
|
246
|
+
// context there is no screenshot, and finding that out first keeps the
|
|
247
|
+
// failure cheap.
|
|
248
|
+
const canvas = document.createElement("canvas");
|
|
249
|
+
const ctx = canvas.getContext("2d");
|
|
250
|
+
if (!ctx)
|
|
251
|
+
return null;
|
|
252
|
+
const svg = buildViewportSvg(width, height, origin);
|
|
253
|
+
if (svg === null)
|
|
254
|
+
return null;
|
|
255
|
+
const image = await decodeSvg(svg);
|
|
256
|
+
if (!image)
|
|
257
|
+
return null;
|
|
258
|
+
const background = resolvePageBackground();
|
|
259
|
+
let scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
260
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
261
|
+
canvas.width = Math.max(1, Math.round(width * scale));
|
|
262
|
+
canvas.height = Math.max(1, Math.round(height * scale));
|
|
263
|
+
ctx.fillStyle = background;
|
|
264
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
265
|
+
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
266
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
267
|
+
if (dataUrl.length <= maxDataUrlLength)
|
|
268
|
+
return dataUrl;
|
|
269
|
+
scale /= 2;
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Serialise the visible page as an SVG document string, or null if the page is
|
|
275
|
+
* too large to walk. Exported for the unit tests: it is pure DOM work with no
|
|
276
|
+
* canvas or image decoding in it, so it is the part that is provable in jsdom.
|
|
277
|
+
*/
|
|
278
|
+
export function buildViewportSvg(width, height,
|
|
279
|
+
/**
|
|
280
|
+
* Where in the viewport the picture starts. `{x: 0, y: 0}` is the viewport
|
|
281
|
+
* shot; an element shot passes its own top-left, and the body clone shifts
|
|
282
|
+
* by the same amount so the element lands at the picture's origin.
|
|
283
|
+
*
|
|
284
|
+
* Cropping by MOVING THE WHOLE PAGE rather than by cloning only the element
|
|
285
|
+
* is the decision here. An element does not carry its own layout: its width
|
|
286
|
+
* may come from a flex parent, its colour from a `.dark` ancestor, its font
|
|
287
|
+
* from `html`. Cloning it alone reproduces none of that, and the picture
|
|
288
|
+
* that comes back is not the thing the reviewer pointed at. The page is
|
|
289
|
+
* cloned exactly as it is and the viewport is narrowed instead.
|
|
290
|
+
*/
|
|
291
|
+
origin = { x: 0, y: 0 }) {
|
|
292
|
+
if (document.body.getElementsByTagName("*").length > MAX_ELEMENTS) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const clone = cloneStyled(document.body);
|
|
296
|
+
if (!clone)
|
|
297
|
+
return null;
|
|
298
|
+
// Placing the body clone at the viewport coordinates of the real body is
|
|
299
|
+
// what folds scroll position, body margins and body offset into one number
|
|
300
|
+
// each. Descendants then lay themselves out normally inside it, and
|
|
301
|
+
// `position: fixed` descendants resolve against the SVG viewport, which is
|
|
302
|
+
// the browser viewport - so a sticky header stays where it looks.
|
|
303
|
+
const rect = document.body.getBoundingClientRect();
|
|
304
|
+
clone.style.setProperty("position", "absolute");
|
|
305
|
+
clone.style.setProperty("left", `${rect.left - origin.x}px`);
|
|
306
|
+
clone.style.setProperty("top", `${rect.top - origin.y}px`);
|
|
307
|
+
clone.style.setProperty("width", `${rect.width}px`);
|
|
308
|
+
clone.style.setProperty("height", `${rect.height}px`);
|
|
309
|
+
clone.style.setProperty("margin", "0");
|
|
310
|
+
const container = document.createElement("div");
|
|
311
|
+
container.setAttribute("style", [
|
|
312
|
+
"position:relative",
|
|
313
|
+
`width:${width}px`,
|
|
314
|
+
`height:${height}px`,
|
|
315
|
+
"overflow:hidden",
|
|
316
|
+
`background:${resolvePageBackground()}`,
|
|
317
|
+
].join(";"));
|
|
318
|
+
container.appendChild(clone);
|
|
319
|
+
const body = new XMLSerializer().serializeToString(container);
|
|
320
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" ` +
|
|
321
|
+
`viewBox="0 0 ${width} ${height}">` +
|
|
322
|
+
`<foreignObject x="0" y="0" width="100%" height="100%">${body}</foreignObject>` +
|
|
323
|
+
`</svg>`);
|
|
324
|
+
}
|
|
325
|
+
/* ── The walk ─────────────────────────────────────────────────────────── */
|
|
326
|
+
function cloneStyled(source) {
|
|
327
|
+
if (SKIPPED_TAGS.has(source.tagName))
|
|
328
|
+
return null;
|
|
329
|
+
if (source.hasAttribute(TOOLBAR_ROOT_ATTRIBUTE))
|
|
330
|
+
return null;
|
|
331
|
+
const computed = window.getComputedStyle(source);
|
|
332
|
+
// `display: none` contributes nothing, and skipping it prunes whole subtrees
|
|
333
|
+
// - closed menus and offscreen drawers are usually most of a page's DOM.
|
|
334
|
+
if (computed.getPropertyValue("display") === "none")
|
|
335
|
+
return null;
|
|
336
|
+
// Inline SVG - icons, mostly - is already an XML island whose attributes are
|
|
337
|
+
// its geometry. Take the whole subtree verbatim and only add the inherited
|
|
338
|
+
// paint, which is what `currentColor` needs to resolve to.
|
|
339
|
+
if (source instanceof SVGElement) {
|
|
340
|
+
const svgClone = source.cloneNode(true);
|
|
341
|
+
svgClone.setAttribute("style", inlineStyleFor(source, computed));
|
|
342
|
+
return svgClone;
|
|
343
|
+
}
|
|
344
|
+
// A canvas paints from its bitmap, not its children, so it becomes an image.
|
|
345
|
+
const clone = (source instanceof HTMLCanvasElement
|
|
346
|
+
? document.createElement("img")
|
|
347
|
+
: source.cloneNode(false));
|
|
348
|
+
// Most attributes only mattered through stylesheets the SVG document cannot
|
|
349
|
+
// see, and framework attributes (`@click`, `[ngIf]`, `x-on:click`) are not
|
|
350
|
+
// valid XML names, so serialising them would break the whole document. Keep
|
|
351
|
+
// the few that still drive layout or content.
|
|
352
|
+
for (const name of [...clone.getAttributeNames()]) {
|
|
353
|
+
if (!KEPT_ATTRIBUTES.has(name))
|
|
354
|
+
clone.removeAttribute(name);
|
|
355
|
+
}
|
|
356
|
+
clone.setAttribute("style", inlineStyleFor(source, computed));
|
|
357
|
+
if (!inlineReplacement(source, clone)) {
|
|
358
|
+
for (const child of Array.from(source.childNodes)) {
|
|
359
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
360
|
+
clone.appendChild(child.cloneNode(false));
|
|
361
|
+
}
|
|
362
|
+
else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
363
|
+
const childClone = cloneStyled(child);
|
|
364
|
+
if (childClone)
|
|
365
|
+
clone.appendChild(childClone);
|
|
366
|
+
}
|
|
367
|
+
// Comments, processing instructions and the rest paint nothing.
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return clone;
|
|
371
|
+
}
|
|
372
|
+
function inlineStyleFor(source, computed) {
|
|
373
|
+
const declarations = [];
|
|
374
|
+
for (const property of COPIED_PROPERTIES) {
|
|
375
|
+
const value = computed.getPropertyValue(property);
|
|
376
|
+
if (value === "")
|
|
377
|
+
continue;
|
|
378
|
+
declarations.push(`${property}:${value}`);
|
|
379
|
+
}
|
|
380
|
+
// An element scrolled internally shows its scrolled content, and nothing in
|
|
381
|
+
// the computed style says so.
|
|
382
|
+
if (source.scrollTop || source.scrollLeft) {
|
|
383
|
+
declarations.push(`transform:translate(${-source.scrollLeft}px, ${-source.scrollTop}px)`);
|
|
384
|
+
}
|
|
385
|
+
return declarations.join(";");
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Elements whose pixels are not in their child nodes. Returns true when the
|
|
389
|
+
* clone has been dealt with and must not be walked into.
|
|
390
|
+
*/
|
|
391
|
+
function inlineReplacement(source, clone) {
|
|
392
|
+
if (source instanceof HTMLImageElement) {
|
|
393
|
+
const inlined = toDataUrl(source);
|
|
394
|
+
// No `src` at all beats a broken-image glyph where the picture was.
|
|
395
|
+
if (inlined)
|
|
396
|
+
clone.setAttribute("src", inlined);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
if (source instanceof HTMLCanvasElement) {
|
|
400
|
+
// `clone` is an `<img>` standing in for the canvas; see `cloneStyled`.
|
|
401
|
+
const inlined = safeCanvasDataUrl(source);
|
|
402
|
+
if (inlined)
|
|
403
|
+
clone.setAttribute("src", inlined);
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
if (source instanceof HTMLInputElement ||
|
|
407
|
+
source instanceof HTMLTextAreaElement) {
|
|
408
|
+
// What the reviewer typed is often the point of the report, and a value
|
|
409
|
+
// set by script lives in the property, not the attribute.
|
|
410
|
+
if (source instanceof HTMLTextAreaElement) {
|
|
411
|
+
clone.textContent = source.value;
|
|
412
|
+
}
|
|
413
|
+
else if (source.type !== "password") {
|
|
414
|
+
clone.setAttribute("value", source.value);
|
|
415
|
+
clone.setAttribute("type", source.type);
|
|
416
|
+
}
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
function toDataUrl(image) {
|
|
422
|
+
if (image.currentSrc.startsWith("data:"))
|
|
423
|
+
return image.currentSrc;
|
|
424
|
+
if (!image.complete || image.naturalWidth === 0)
|
|
425
|
+
return null;
|
|
426
|
+
try {
|
|
427
|
+
const canvas = document.createElement("canvas");
|
|
428
|
+
canvas.width = image.naturalWidth;
|
|
429
|
+
canvas.height = image.naturalHeight;
|
|
430
|
+
const ctx = canvas.getContext("2d");
|
|
431
|
+
if (!ctx)
|
|
432
|
+
return null;
|
|
433
|
+
ctx.drawImage(image, 0, 0);
|
|
434
|
+
// Throws a SecurityError on a cross-origin image without CORS. That is the
|
|
435
|
+
// expected outcome for a third-party image, not an error worth surfacing.
|
|
436
|
+
return canvas.toDataURL("image/png");
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function safeCanvasDataUrl(canvas) {
|
|
443
|
+
try {
|
|
444
|
+
return canvas.toDataURL("image/png");
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/* ── Decoding ─────────────────────────────────────────────────────────── */
|
|
451
|
+
function decodeSvg(svg) {
|
|
452
|
+
return new Promise((resolve) => {
|
|
453
|
+
const image = new Image();
|
|
454
|
+
let settled = false;
|
|
455
|
+
const settle = (value) => {
|
|
456
|
+
if (settled)
|
|
457
|
+
return;
|
|
458
|
+
settled = true;
|
|
459
|
+
window.clearTimeout(timer);
|
|
460
|
+
resolve(value);
|
|
461
|
+
};
|
|
462
|
+
// A malformed clone makes the decode fail rather than throw, and an
|
|
463
|
+
// environment that never loads anything (jsdom, a blocked document) makes
|
|
464
|
+
// it do neither - hence the timeout as well as the error handler.
|
|
465
|
+
const timer = window.setTimeout(() => settle(null), DECODE_TIMEOUT_MS);
|
|
466
|
+
image.onload = () => settle(image);
|
|
467
|
+
image.onerror = () => settle(null);
|
|
468
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* The colour behind the page. `body` is usually transparent and the paint is
|
|
473
|
+
* on `html`, or on neither, in which case the browser's canvas is white.
|
|
474
|
+
*/
|
|
475
|
+
function resolvePageBackground() {
|
|
476
|
+
for (const element of [document.documentElement, document.body]) {
|
|
477
|
+
if (!element)
|
|
478
|
+
continue;
|
|
479
|
+
const colour = window
|
|
480
|
+
.getComputedStyle(element)
|
|
481
|
+
.getPropertyValue("background-color");
|
|
482
|
+
if (colour &&
|
|
483
|
+
colour !== "transparent" &&
|
|
484
|
+
!colour.includes("rgba(0, 0, 0, 0)")) {
|
|
485
|
+
return colour;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return "#ffffff";
|
|
489
|
+
}
|
|
490
|
+
//# sourceMappingURL=rasterise.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rasterise.js","sourceRoot":"","sources":["../src/rasterise.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,cAAc;AACd,EAAE;AACF,+EAA+E;AAC/E,yEAAyE;AACzE,8EAA8E;AAC9E,2EAA2E;AAC3E,sEAAsE;AACtE,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,EAAE;AACF,6EAA6E;AAC7E,kCAAkC;AAClC,+EAA+E;AAC/E,4EAA4E;AAC5E,yEAAyE;AACzE,+EAA+E;AAC/E,gFAAgF;AAChF,4EAA4E;AAC5E,qDAAqD;AACrD,EAAE;AACF,gFAAgF;AAChF,wEAAwE;AACxE,8EAA8E;AAC9E,+BAA+B;AAE/B,8EAA8E;AAC9E,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAC3B,QAAQ;IACR,OAAO;IACP,MAAM;IACN,MAAM;IACN,OAAO;IACP,UAAU;IACV,UAAU;IACV,MAAM;CACP,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,sBAAsB,GAAG,8BAA8B,CAAC;AAE9D;;;;GAIG;AACH,MAAM,YAAY,GAAG,MAAM,CAAC;AAE5B;;;;GAIG;AACH,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC;IAC9B,SAAS;IACT,SAAS;IACT,MAAM;IACN,KAAK;IACL,KAAK;IACL,OAAO;IACP,UAAU;IACV,MAAM;IACN,OAAO;IACP,SAAS;IACT,UAAU;IACV,MAAM;IACN,MAAM;CACP,CAAC,CAAC;AAEH,2EAA2E;AAC3E,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEhC;;;GAGG;AACH,MAAM,iBAAiB,GAAG;IACxB,MAAM;IACN,SAAS;IACT,UAAU;IACV,KAAK;IACL,OAAO;IACP,QAAQ;IACR,MAAM;IACN,OAAO;IACP,OAAO;IACP,SAAS;IACT,YAAY;IACZ,OAAO;IACP,QAAQ;IACR,WAAW;IACX,YAAY;IACZ,WAAW;IACX,YAAY;IACZ,cAAc;IACd,YAAY;IACZ,cAAc;IACd,eAAe;IACf,aAAa;IACb,aAAa;IACb,eAAe;IACf,gBAAgB;IAChB,cAAc;IACd,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,SAAS;IACT,qBAAqB;IACrB,kBAAkB;IAClB,oBAAoB;IACpB,qBAAqB;IACrB,mBAAmB;IACnB,kBAAkB;IAClB,oBAAoB;IACpB,qBAAqB;IACrB,mBAAmB;IACnB,kBAAkB;IAClB,oBAAoB;IACpB,qBAAqB;IACrB,mBAAmB;IACnB,wBAAwB;IACxB,yBAAyB;IACzB,2BAA2B;IAC3B,4BAA4B;IAC5B,eAAe;IACf,eAAe;IACf,eAAe;IACf,gBAAgB;IAChB,QAAQ;IACR,kBAAkB;IAClB,kBAAkB;IAClB,iBAAiB;IACjB,qBAAqB;IACrB,mBAAmB;IACnB,iBAAiB;IACjB,mBAAmB;IACnB,YAAY;IACZ,QAAQ;IACR,gBAAgB;IAChB,WAAW;IACX,WAAW;IACX,kBAAkB;IAClB,OAAO;IACP,OAAO;IACP,aAAa;IACb,WAAW;IACX,YAAY;IACZ,aAAa;IACb,cAAc;IACd,cAAc;IACd,gBAAgB;IAChB,aAAa;IACb,YAAY;IACZ,sBAAsB;IACtB,uBAAuB;IACvB,uBAAuB;IACvB,aAAa;IACb,gBAAgB;IAChB,aAAa;IACb,aAAa;IACb,YAAY;IACZ,eAAe;IACf,gBAAgB;IAChB,eAAe;IACf,WAAW;IACX,gBAAgB;IAChB,gBAAgB;IAChB,WAAW;IACX,WAAW;IACX,aAAa;IACb,YAAY;IACZ,iBAAiB;IACjB,eAAe;IACf,cAAc;IACd,eAAe;IACf,aAAa;IACb,YAAY;IACZ,OAAO;IACP,KAAK;IACL,SAAS;IACT,YAAY;IACZ,uBAAuB;IACvB,oBAAoB;IACpB,gBAAgB;IAChB,mBAAmB;IACnB,gBAAgB;IAChB,mBAAmB;IACnB,iBAAiB;IACjB,gBAAgB;IAChB,cAAc;IACd,uBAAuB;IACvB,cAAc;IACd,iBAAiB;IACjB,gBAAgB;IAChB,cAAc;IACd,iBAAiB;IACjB,qBAAqB;IACrB,YAAY;IACZ,iBAAiB;IACjB,MAAM;IACN,QAAQ;IACR,cAAc;CACN,CAAC;AAEX;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,gBAAwB;IAExB,OAAO,eAAe,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAgB,EAChB,gBAAwB;IAExB,MAAM,IAAI,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAC7C,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACnD,OAAO,eAAe,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,KAAK,UAAU,eAAe,CAC5B,gBAAwB,EACxB,MAAsE;IAEtE,IAAI,OAAO,QAAQ,KAAK,WAAW,IAAI,CAAC,QAAQ,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEnE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CACpB,CAAC,EACD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CACtD,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CACrB,CAAC,EACD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CACxD,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IAEtE,0EAA0E;IAC1E,uEAAuE;IACvE,iBAAiB;IACjB,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChD,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IAEtB,MAAM,GAAG,GAAG,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACpD,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAE9B,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,MAAM,UAAU,GAAG,qBAAqB,EAAE,CAAC;IAC3C,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,gBAAgB,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IACtD,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;QAC7C,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;QACtD,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC;QACxD,GAAG,CAAC,SAAS,GAAG,UAAU,CAAC;QAC3B,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAChD,GAAG,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACxD,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAC9C,IAAI,OAAO,CAAC,MAAM,IAAI,gBAAgB;YAAE,OAAO,OAAO,CAAC;QACvD,KAAK,IAAI,CAAC,CAAC;IACb,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAa,EACb,MAAc;AACd;;;;;;;;;;;GAWG;AACH,SAAmC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;IAEjD,IAAI,QAAQ,CAAC,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;QAClE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAuB,CAAC;IAC/D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,yEAAyE;IACzE,2EAA2E;IAC3E,oEAAoE;IACpE,2EAA2E;IAC3E,kEAAkE;IAClE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,qBAAqB,EAAE,CAAC;IACnD,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IAChD,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;IAC7D,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;IAC3D,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;IACpD,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;IACtD,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAEvC,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAChD,SAAS,CAAC,YAAY,CACpB,OAAO,EACP;QACE,mBAAmB;QACnB,SAAS,KAAK,IAAI;QAClB,UAAU,MAAM,IAAI;QACpB,iBAAiB;QACjB,cAAc,qBAAqB,EAAE,EAAE;KACxC,CAAC,IAAI,CAAC,GAAG,CAAC,CACZ,CAAC;IACF,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAE7B,MAAM,IAAI,GAAG,IAAI,aAAa,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC9D,OAAO,CACL,kDAAkD,KAAK,aAAa,MAAM,IAAI;QAC9E,gBAAgB,KAAK,IAAI,MAAM,IAAI;QACnC,yDAAyD,IAAI,kBAAkB;QAC/E,QAAQ,CACT,CAAC;AACJ,CAAC;AAED,6EAA6E;AAE7E,SAAS,WAAW,CAAC,MAAe;IAClC,IAAI,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAClD,IAAI,MAAM,CAAC,YAAY,CAAC,sBAAsB,CAAC;QAAE,OAAO,IAAI,CAAC;IAE7D,MAAM,QAAQ,GAAG,MAAM,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACjD,6EAA6E;IAC7E,yEAAyE;IACzE,IAAI,QAAQ,CAAC,gBAAgB,CAAC,SAAS,CAAC,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IAEjE,6EAA6E;IAC7E,2EAA2E;IAC3E,2DAA2D;IAC3D,IAAI,MAAM,YAAY,UAAU,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAe,CAAC;QACtD,QAAQ,CAAC,YAAY,CAAC,OAAO,EAAE,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;QACjE,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,6EAA6E;IAC7E,MAAM,KAAK,GAAG,CACZ,MAAM,YAAY,iBAAiB;QACjC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC;QAC/B,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CACb,CAAC;IAEjB,4EAA4E;IAC5E,2EAA2E;IAC3E,4EAA4E;IAC5E,8CAA8C;IAC9C,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,KAAK,CAAC,iBAAiB,EAAE,CAAC,EAAE,CAAC;QAClD,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IAC9D,CAAC;IACD,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAE9D,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC;QACtC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;YAClD,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;gBACtC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YAC5C,CAAC;iBAAM,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,YAAY,EAAE,CAAC;gBAChD,MAAM,UAAU,GAAG,WAAW,CAAC,KAAgB,CAAC,CAAC;gBACjD,IAAI,UAAU;oBAAE,KAAK,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;YAChD,CAAC;YACD,gEAAgE;QAClE,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,cAAc,CACrB,MAAe,EACf,QAA6B;IAE7B,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,KAAK,MAAM,QAAQ,IAAI,iBAAiB,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAClD,IAAI,KAAK,KAAK,EAAE;YAAE,SAAS;QAC3B,YAAY,CAAC,IAAI,CAAC,GAAG,QAAQ,IAAI,KAAK,EAAE,CAAC,CAAC;IAC5C,CAAC;IACD,4EAA4E;IAC5E,8BAA8B;IAC9B,IAAI,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QAC1C,YAAY,CAAC,IAAI,CACf,uBAAuB,CAAC,MAAM,CAAC,UAAU,OAAO,CAAC,MAAM,CAAC,SAAS,KAAK,CACvE,CAAC;IACJ,CAAC;IACD,OAAO,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,MAAe,EAAE,KAAkB;IAC5D,IAAI,MAAM,YAAY,gBAAgB,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;QAClC,oEAAoE;QACpE,IAAI,OAAO;YAAE,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,MAAM,YAAY,iBAAiB,EAAE,CAAC;QACxC,uEAAuE;QACvE,MAAM,OAAO,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;QAC1C,IAAI,OAAO;YAAE,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IACE,MAAM,YAAY,gBAAgB;QAClC,MAAM,YAAY,mBAAmB,EACrC,CAAC;QACD,wEAAwE;QACxE,0DAA0D;QAC1D,IAAI,MAAM,YAAY,mBAAmB,EAAE,CAAC;YAC1C,KAAK,CAAC,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC;QACnC,CAAC;aAAM,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACtC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;YAC1C,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,SAAS,CAAC,KAAuB;IACxC,IAAI,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC,UAAU,CAAC;IAClE,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,YAAY,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7D,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,YAAY,CAAC;QAClC,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,aAAa,CAAC;QACpC,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,GAAG,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC3B,2EAA2E;QAC3E,0EAA0E;QAC1E,OAAO,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,MAAyB;IAClD,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,6EAA6E;AAE7E,SAAS,SAAS,CAAC,GAAW;IAC5B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,MAAM,GAAG,CAAC,KAA8B,EAAE,EAAE;YAChD,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,CAAC;QACF,oEAAoE;QACpE,0EAA0E;QAC1E,kEAAkE;QAClE,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,iBAAiB,CAAC,CAAC;QACvE,KAAK,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnC,KAAK,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACnC,KAAK,CAAC,GAAG,GAAG,oCAAoC,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;IAC5E,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,SAAS,qBAAqB;IAC5B,KAAK,MAAM,OAAO,IAAI,CAAC,QAAQ,CAAC,eAAe,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAChE,IAAI,CAAC,OAAO;YAAE,SAAS;QACvB,MAAM,MAAM,GAAG,MAAM;aAClB,gBAAgB,CAAC,OAAO,CAAC;aACzB,gBAAgB,CAAC,kBAAkB,CAAC,CAAC;QACxC,IACE,MAAM;YACN,MAAM,KAAK,aAAa;YACxB,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EACpC,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a selector that resolves back to `el`. Returns null when no selector
|
|
3
|
+
* can be built, which the caller treats as "this element cannot be pinned"
|
|
4
|
+
* rather than storing an anchor that will not resolve.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildSelector(el: Element): string | null;
|
|
7
|
+
/** Resolve a stored selector, or null when the element is no longer there. */
|
|
8
|
+
export declare function resolveSelector(selector: string): Element | null;
|
|
9
|
+
/** Where inside an element a click landed, as fractions of its box. */
|
|
10
|
+
export declare function offsetWithin(el: Element, clientX: number, clientY: number): {
|
|
11
|
+
offsetX: number;
|
|
12
|
+
offsetY: number;
|
|
13
|
+
};
|
|
14
|
+
/** Viewport position of a stored anchor, or null when it no longer resolves. */
|
|
15
|
+
export declare function locatePin(selector: string, offsetX: number, offsetY: number): {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
} | null;
|
|
19
|
+
//# sourceMappingURL=selector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selector.d.ts","sourceRoot":"","sources":["../src/selector.ts"],"names":[],"mappings":"AAiDA;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAsCxD;AAwBD,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAMhE;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAC1B,EAAE,EAAE,OAAO,EACX,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAUtC;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CASjC"}
|
package/dist/selector.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Turning an element into something that still finds it after a reload.
|
|
2
|
+
//
|
|
3
|
+
// This is the part of pinning that decides whether a pin survives, so it is a
|
|
4
|
+
// pure DOM function with no React in it and its own tests. The strategy, in
|
|
5
|
+
// order of how much it can be trusted:
|
|
6
|
+
//
|
|
7
|
+
// 1. `id`, when there is one and it looks stable.
|
|
8
|
+
// 2. `data-testid`, which a team puts on things it intends to keep.
|
|
9
|
+
// 3. A path of tag + nth-of-type steps up to the nearest anchor above.
|
|
10
|
+
//
|
|
11
|
+
// Class names are deliberately not used. A build that hashes CSS module names
|
|
12
|
+
// changes every class on every deploy, and a pin anchored to `.css-1x2y3z`
|
|
13
|
+
// would break on the next commit, which is precisely the pin that matters.
|
|
14
|
+
/** Attributes considered stable enough to anchor to, most trusted first. */
|
|
15
|
+
const STABLE_ATTRIBUTES = ["data-testid", "data-test-id", "data-qa"];
|
|
16
|
+
/** An id a bundler generated is not an anchor, however unique it is. */
|
|
17
|
+
function looksGenerated(value) {
|
|
18
|
+
// Long hex-ish or random-looking runs: React's useId (":r1:"), CSS modules,
|
|
19
|
+
// and most component libraries' generated ids.
|
|
20
|
+
return (value.length > 40 ||
|
|
21
|
+
/^:r[0-9a-z]+:$/i.test(value) ||
|
|
22
|
+
/^[0-9a-f]{16,}$/i.test(value) ||
|
|
23
|
+
/^(radix|headlessui|mui|chakra)-/i.test(value));
|
|
24
|
+
}
|
|
25
|
+
function escapeIdent(value) {
|
|
26
|
+
// CSS.escape exists everywhere the toolbar runs, but jsdom in older setups
|
|
27
|
+
// and a stray non-browser import should not throw.
|
|
28
|
+
const cssEscape = globalThis
|
|
29
|
+
.CSS?.escape;
|
|
30
|
+
return cssEscape ? cssEscape(value) : value.replace(/[^\w-]/g, "\\$&");
|
|
31
|
+
}
|
|
32
|
+
/** The step that identifies one element among its siblings. */
|
|
33
|
+
function nthOfTypeStep(el) {
|
|
34
|
+
const tag = el.tagName.toLowerCase();
|
|
35
|
+
const parent = el.parentElement;
|
|
36
|
+
if (!parent)
|
|
37
|
+
return tag;
|
|
38
|
+
const sameTag = Array.from(parent.children).filter((c) => c.tagName === el.tagName);
|
|
39
|
+
if (sameTag.length === 1)
|
|
40
|
+
return tag;
|
|
41
|
+
return `${tag}:nth-of-type(${sameTag.indexOf(el) + 1})`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build a selector that resolves back to `el`. Returns null when no selector
|
|
45
|
+
* can be built, which the caller treats as "this element cannot be pinned"
|
|
46
|
+
* rather than storing an anchor that will not resolve.
|
|
47
|
+
*/
|
|
48
|
+
export function buildSelector(el) {
|
|
49
|
+
if (el.id && !looksGenerated(el.id)) {
|
|
50
|
+
const candidate = `#${escapeIdent(el.id)}`;
|
|
51
|
+
if (resolvesUniquely(candidate, el))
|
|
52
|
+
return candidate;
|
|
53
|
+
}
|
|
54
|
+
for (const attr of STABLE_ATTRIBUTES) {
|
|
55
|
+
const value = el.getAttribute(attr);
|
|
56
|
+
if (value) {
|
|
57
|
+
const candidate = `[${attr}="${cssStringEscape(value)}"]`;
|
|
58
|
+
if (resolvesUniquely(candidate, el))
|
|
59
|
+
return candidate;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Walk up, prepending steps, and stop as soon as the path is unique. Anchor
|
|
63
|
+
// on an id or test id above if one turns up, which keeps the selector short
|
|
64
|
+
// and unaffected by anything that changes outside that subtree.
|
|
65
|
+
const steps = [];
|
|
66
|
+
let current = el;
|
|
67
|
+
while (current && current !== document.documentElement) {
|
|
68
|
+
steps.unshift(nthOfTypeStep(current));
|
|
69
|
+
const parent = current.parentElement;
|
|
70
|
+
if (parent) {
|
|
71
|
+
const anchor = anchorFor(parent);
|
|
72
|
+
if (anchor) {
|
|
73
|
+
const candidate = `${anchor} > ${steps.join(" > ")}`;
|
|
74
|
+
if (resolvesUniquely(candidate, el))
|
|
75
|
+
return candidate;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const candidate = steps.join(" > ");
|
|
79
|
+
if (resolvesUniquely(candidate, el))
|
|
80
|
+
return candidate;
|
|
81
|
+
current = parent;
|
|
82
|
+
}
|
|
83
|
+
const full = steps.join(" > ");
|
|
84
|
+
return resolvesUniquely(full, el) ? full : null;
|
|
85
|
+
}
|
|
86
|
+
function anchorFor(el) {
|
|
87
|
+
if (el.id && !looksGenerated(el.id))
|
|
88
|
+
return `#${escapeIdent(el.id)}`;
|
|
89
|
+
for (const attr of STABLE_ATTRIBUTES) {
|
|
90
|
+
const value = el.getAttribute(attr);
|
|
91
|
+
if (value)
|
|
92
|
+
return `[${attr}="${cssStringEscape(value)}"]`;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function cssStringEscape(value) {
|
|
97
|
+
return value.replace(/["\\]/g, "\\$&");
|
|
98
|
+
}
|
|
99
|
+
function resolvesUniquely(selector, el) {
|
|
100
|
+
try {
|
|
101
|
+
const found = document.querySelectorAll(selector);
|
|
102
|
+
return found.length === 1 && found[0] === el;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Resolve a stored selector, or null when the element is no longer there. */
|
|
109
|
+
export function resolveSelector(selector) {
|
|
110
|
+
try {
|
|
111
|
+
return document.querySelector(selector);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Where inside an element a click landed, as fractions of its box. */
|
|
118
|
+
export function offsetWithin(el, clientX, clientY) {
|
|
119
|
+
const rect = el.getBoundingClientRect();
|
|
120
|
+
// Fractions rather than pixels so the dot stays where the reviewer put it
|
|
121
|
+
// when the element is a different size on a different viewport.
|
|
122
|
+
const offsetX = rect.width > 0 ? (clientX - rect.left) / rect.width : 0.5;
|
|
123
|
+
const offsetY = rect.height > 0 ? (clientY - rect.top) / rect.height : 0.5;
|
|
124
|
+
return {
|
|
125
|
+
offsetX: Math.min(1, Math.max(0, offsetX)),
|
|
126
|
+
offsetY: Math.min(1, Math.max(0, offsetY)),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Viewport position of a stored anchor, or null when it no longer resolves. */
|
|
130
|
+
export function locatePin(selector, offsetX, offsetY) {
|
|
131
|
+
const el = resolveSelector(selector);
|
|
132
|
+
if (!el)
|
|
133
|
+
return null;
|
|
134
|
+
const rect = el.getBoundingClientRect();
|
|
135
|
+
if (rect.width === 0 && rect.height === 0)
|
|
136
|
+
return null;
|
|
137
|
+
return {
|
|
138
|
+
x: rect.left + rect.width * offsetX,
|
|
139
|
+
y: rect.top + rect.height * offsetY,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=selector.js.map
|