craftdriver 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/cli/snapshot.js +31 -4
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/lib/bidi/index.d.ts +4 -5
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +18 -8
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts +12 -1
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +20 -8
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/browser.d.ts +6 -9
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +6 -4
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/by.d.ts +18 -0
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +141 -17
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/locator.d.ts +28 -0
- package/dist/lib/locator.d.ts.map +1 -1
- package/dist/lib/locator.js +28 -0
- package/dist/lib/locator.js.map +1 -1
- package/docs/browser-api.md +4 -2
- package/docs/browser-logs.md +8 -23
- package/docs/public/examples/a11y.html +44 -0
- package/docs/public/examples/clock.html +83 -0
- package/docs/public/examples/console-errors.html +344 -0
- package/docs/public/examples/dialogs.html +42 -0
- package/docs/public/examples/download.html +31 -0
- package/docs/public/examples/dynamic.html +131 -0
- package/docs/public/examples/emulate.html +127 -0
- package/docs/public/examples/evaluate.html +28 -0
- package/docs/public/examples/hover-select.html +295 -0
- package/docs/public/examples/iframe-child.html +51 -0
- package/docs/public/examples/iframes.html +35 -0
- package/docs/public/examples/keyboard.html +195 -0
- package/docs/public/examples/locator.html +131 -0
- package/docs/public/examples/login.html +116 -0
- package/docs/public/examples/mobile.html +270 -0
- package/docs/public/examples/mouse.html +505 -0
- package/docs/public/examples/network.html +293 -0
- package/docs/public/examples/popup-target.html +26 -0
- package/docs/public/examples/popup.html +50 -0
- package/docs/public/examples/screenshot.html +99 -0
- package/docs/public/examples/selectors.html +150 -0
- package/docs/public/examples/session.html +514 -0
- package/docs/public/examples/upload.html +36 -0
- package/docs/recipes/accessibility-gate.md +24 -40
- package/docs/recipes/console-error-gate.md +27 -39
- package/docs/recipes/file-upload-download.md +24 -44
- package/docs/recipes/find-elements.md +142 -0
- package/docs/recipes/login-once-reuse-session.md +22 -45
- package/docs/recipes/mobile-flow-with-network-and-logs.md +22 -41
- package/docs/recipes/mock-api-and-assert-network.md +22 -35
- package/docs/recipes/multi-user-contexts.md +32 -43
- package/docs/recipes/page-objects.md +52 -0
- package/docs/recipes/trace-failing-test.md +31 -44
- package/docs/recipes/virtual-clock-time-sensitive-ui.md +24 -40
- package/docs/recipes/vitest-browser-lifecycle.md +15 -15
- package/docs/recipes.md +20 -1
- package/docs/selectors.md +291 -229
- package/docs/tracing.md +2 -2
- package/package.json +5 -1
- package/skills/craftdriver/SKILL.md +3 -2
- package/skills/craftdriver/cheatsheet.md +2 -1
- package/skills/craftdriver/patterns.md +2 -5
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Mouse Actions Playground</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0f1115;
|
|
10
|
+
--panel: #151925;
|
|
11
|
+
--accent: #4f86ff;
|
|
12
|
+
--success: #18a558;
|
|
13
|
+
--fail: #e63946;
|
|
14
|
+
--text: #e6edf3;
|
|
15
|
+
--muted: #9aa5b1;
|
|
16
|
+
}
|
|
17
|
+
* {
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
}
|
|
20
|
+
html,
|
|
21
|
+
body {
|
|
22
|
+
height: 100%;
|
|
23
|
+
}
|
|
24
|
+
body {
|
|
25
|
+
margin: 0;
|
|
26
|
+
font-family:
|
|
27
|
+
ui-sans-serif,
|
|
28
|
+
system-ui,
|
|
29
|
+
-apple-system,
|
|
30
|
+
Segoe UI,
|
|
31
|
+
Roboto,
|
|
32
|
+
Ubuntu,
|
|
33
|
+
Cantarell,
|
|
34
|
+
Noto Sans,
|
|
35
|
+
Helvetica Neue,
|
|
36
|
+
Arial,
|
|
37
|
+
'Apple Color Emoji',
|
|
38
|
+
'Segoe UI Emoji';
|
|
39
|
+
background: var(--bg);
|
|
40
|
+
color: var(--text);
|
|
41
|
+
display: grid;
|
|
42
|
+
grid-template-columns: 1fr 360px;
|
|
43
|
+
grid-template-rows: auto 1fr;
|
|
44
|
+
gap: 12px;
|
|
45
|
+
padding: 12px;
|
|
46
|
+
}
|
|
47
|
+
header {
|
|
48
|
+
grid-column: 1 / -1;
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: 12px;
|
|
52
|
+
}
|
|
53
|
+
header h1 {
|
|
54
|
+
font-size: 18px;
|
|
55
|
+
margin: 0;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
}
|
|
58
|
+
header .hint {
|
|
59
|
+
color: var(--muted);
|
|
60
|
+
font-size: 12px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.board {
|
|
64
|
+
background: var(--panel);
|
|
65
|
+
border: 1px solid #222839;
|
|
66
|
+
border-radius: 10px;
|
|
67
|
+
padding: 12px;
|
|
68
|
+
position: relative;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
min-height: 520px;
|
|
71
|
+
}
|
|
72
|
+
.board .grid {
|
|
73
|
+
position: absolute;
|
|
74
|
+
inset: 0;
|
|
75
|
+
pointer-events: none;
|
|
76
|
+
opacity: 0.15;
|
|
77
|
+
background-image:
|
|
78
|
+
linear-gradient(#fff2 1px, transparent 1px),
|
|
79
|
+
linear-gradient(90deg, #fff2 1px, transparent 1px);
|
|
80
|
+
background-size: 24px 24px;
|
|
81
|
+
}
|
|
82
|
+
.row {
|
|
83
|
+
display: grid;
|
|
84
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
85
|
+
gap: 12px;
|
|
86
|
+
}
|
|
87
|
+
.card {
|
|
88
|
+
background: #0e1320;
|
|
89
|
+
border: 1px solid #1e263a;
|
|
90
|
+
border-radius: 10px;
|
|
91
|
+
padding: 10px;
|
|
92
|
+
}
|
|
93
|
+
.card h2 {
|
|
94
|
+
font-size: 14px;
|
|
95
|
+
margin: 0 0 8px;
|
|
96
|
+
font-weight: 600;
|
|
97
|
+
}
|
|
98
|
+
.sep {
|
|
99
|
+
height: 1px;
|
|
100
|
+
background: #1e263a;
|
|
101
|
+
margin: 8px 0;
|
|
102
|
+
}
|
|
103
|
+
.pill {
|
|
104
|
+
display: inline-flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: 6px;
|
|
107
|
+
padding: 4px 8px;
|
|
108
|
+
border-radius: 999px;
|
|
109
|
+
background: #101628;
|
|
110
|
+
border: 1px solid #1f2740;
|
|
111
|
+
color: var(--muted);
|
|
112
|
+
font-size: 12px;
|
|
113
|
+
}
|
|
114
|
+
button {
|
|
115
|
+
background: #14203a;
|
|
116
|
+
color: var(--text);
|
|
117
|
+
border: 1px solid #243257;
|
|
118
|
+
border-radius: 6px;
|
|
119
|
+
padding: 6px 10px;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
}
|
|
122
|
+
button:hover {
|
|
123
|
+
background: #19294a;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Drag & Drop area */
|
|
127
|
+
.stage {
|
|
128
|
+
position: relative;
|
|
129
|
+
height: 260px;
|
|
130
|
+
background: #0b1120;
|
|
131
|
+
border: 1px dashed #2a3555;
|
|
132
|
+
border-radius: 8px;
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
}
|
|
135
|
+
#drag-source {
|
|
136
|
+
position: absolute;
|
|
137
|
+
left: 24px;
|
|
138
|
+
top: 24px;
|
|
139
|
+
width: 72px;
|
|
140
|
+
height: 72px;
|
|
141
|
+
border-radius: 10px;
|
|
142
|
+
background: linear-gradient(145deg, #5a7aff, #3e5be8);
|
|
143
|
+
border: 1px solid #2f49c0;
|
|
144
|
+
box-shadow: 0 6px 16px #3e5be880;
|
|
145
|
+
display: grid;
|
|
146
|
+
place-items: center;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
user-select: none;
|
|
149
|
+
touch-action: none;
|
|
150
|
+
color: #fff;
|
|
151
|
+
}
|
|
152
|
+
#drag-target {
|
|
153
|
+
position: absolute;
|
|
154
|
+
right: 24px;
|
|
155
|
+
bottom: 24px;
|
|
156
|
+
width: 160px;
|
|
157
|
+
height: 120px;
|
|
158
|
+
border-radius: 12px;
|
|
159
|
+
border: 2px dashed #3b4f7d;
|
|
160
|
+
background: #0f1730;
|
|
161
|
+
color: var(--muted);
|
|
162
|
+
display: grid;
|
|
163
|
+
place-items: center;
|
|
164
|
+
text-align: center;
|
|
165
|
+
padding: 8px;
|
|
166
|
+
}
|
|
167
|
+
#drag-target.success {
|
|
168
|
+
border-color: var(--success);
|
|
169
|
+
color: var(--success);
|
|
170
|
+
box-shadow: 0 0 0 2px #18a55833 inset;
|
|
171
|
+
}
|
|
172
|
+
#drag-target.fail {
|
|
173
|
+
border-color: var(--fail);
|
|
174
|
+
color: var(--fail);
|
|
175
|
+
box-shadow: 0 0 0 2px #e6394633 inset;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Interaction boxes */
|
|
179
|
+
.box {
|
|
180
|
+
height: 90px;
|
|
181
|
+
border-radius: 8px;
|
|
182
|
+
border: 1px solid #28314a;
|
|
183
|
+
background: #0c1426;
|
|
184
|
+
display: grid;
|
|
185
|
+
place-items: center;
|
|
186
|
+
text-align: center;
|
|
187
|
+
padding: 8px;
|
|
188
|
+
user-select: none;
|
|
189
|
+
}
|
|
190
|
+
.box strong {
|
|
191
|
+
color: #c7d2fe;
|
|
192
|
+
}
|
|
193
|
+
#wheel-box {
|
|
194
|
+
height: 240px;
|
|
195
|
+
overflow: auto;
|
|
196
|
+
}
|
|
197
|
+
#wheel-content {
|
|
198
|
+
height: 600px;
|
|
199
|
+
width: 100%;
|
|
200
|
+
background: linear-gradient(#0c1426, #0f172a);
|
|
201
|
+
display: grid;
|
|
202
|
+
place-items: center;
|
|
203
|
+
color: var(--muted);
|
|
204
|
+
}
|
|
205
|
+
#hover-tip {
|
|
206
|
+
display: none;
|
|
207
|
+
position: absolute;
|
|
208
|
+
transform: translateY(60px);
|
|
209
|
+
padding: 6px 10px;
|
|
210
|
+
font-size: 12px;
|
|
211
|
+
background: #111a2e;
|
|
212
|
+
border: 1px solid #233154;
|
|
213
|
+
border-radius: 6px;
|
|
214
|
+
color: var(--muted);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* Sidebar */
|
|
218
|
+
.sidebar {
|
|
219
|
+
background: var(--panel);
|
|
220
|
+
border: 1px solid #222839;
|
|
221
|
+
border-radius: 10px;
|
|
222
|
+
padding: 10px;
|
|
223
|
+
display: grid;
|
|
224
|
+
grid-template-rows: auto auto 1fr;
|
|
225
|
+
gap: 10px;
|
|
226
|
+
}
|
|
227
|
+
.log {
|
|
228
|
+
background: #0b1120;
|
|
229
|
+
border: 1px solid #1b2340;
|
|
230
|
+
border-radius: 8px;
|
|
231
|
+
padding: 8px;
|
|
232
|
+
height: 320px;
|
|
233
|
+
overflow: auto;
|
|
234
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
235
|
+
font-size: 12px;
|
|
236
|
+
}
|
|
237
|
+
.log .item {
|
|
238
|
+
color: #d1d5db;
|
|
239
|
+
}
|
|
240
|
+
.log .time {
|
|
241
|
+
color: #7aa2ff;
|
|
242
|
+
}
|
|
243
|
+
.status {
|
|
244
|
+
display: grid;
|
|
245
|
+
grid-template-columns: auto 1fr;
|
|
246
|
+
gap: 6px 10px;
|
|
247
|
+
align-items: center;
|
|
248
|
+
}
|
|
249
|
+
.status .value {
|
|
250
|
+
font-weight: 600;
|
|
251
|
+
}
|
|
252
|
+
.ok {
|
|
253
|
+
color: var(--success);
|
|
254
|
+
}
|
|
255
|
+
.bad {
|
|
256
|
+
color: var(--fail);
|
|
257
|
+
}
|
|
258
|
+
.coords {
|
|
259
|
+
color: var(--muted);
|
|
260
|
+
font-size: 12px;
|
|
261
|
+
}
|
|
262
|
+
</style>
|
|
263
|
+
</head>
|
|
264
|
+
<body>
|
|
265
|
+
<header>
|
|
266
|
+
<h1>Mouse Actions Playground</h1>
|
|
267
|
+
<span class="hint"
|
|
268
|
+
>Try press, release, move, click, double-click, right-click, wheel, and drag & drop.</span
|
|
269
|
+
>
|
|
270
|
+
<span class="pill" id="coords">x: 0, y: 0</span>
|
|
271
|
+
<button id="reset-btn" title="Reset positions and log">Reset</button>
|
|
272
|
+
</header>
|
|
273
|
+
|
|
274
|
+
<main class="board" id="board">
|
|
275
|
+
<div class="grid"></div>
|
|
276
|
+
|
|
277
|
+
<section class="card">
|
|
278
|
+
<h2>Drag & Drop (success when fully inside the target)</h2>
|
|
279
|
+
<div class="stage" id="stage">
|
|
280
|
+
<div id="drag-source" aria-label="drag-source">DRAG</div>
|
|
281
|
+
<div id="drag-target" aria-label="drag-target">
|
|
282
|
+
Drop here
|
|
283
|
+
<div style="font-size: 11px; color: var(--muted)">needs to be fully inside</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</section>
|
|
287
|
+
|
|
288
|
+
<div class="row" style="margin-top: 12px">
|
|
289
|
+
<section class="card">
|
|
290
|
+
<h2>Press & Release</h2>
|
|
291
|
+
<div class="box" id="press-box">
|
|
292
|
+
<div><strong>press-box</strong><br /><span>pointerdown / pointerup</span></div>
|
|
293
|
+
</div>
|
|
294
|
+
</section>
|
|
295
|
+
<section class="card">
|
|
296
|
+
<h2>Double click</h2>
|
|
297
|
+
<div class="box" id="dblclick-box">
|
|
298
|
+
<div>
|
|
299
|
+
<strong>dblclick-box</strong><br /><span
|
|
300
|
+
>dblclick counter: <span id="dblcount">0</span></span
|
|
301
|
+
>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</section>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="row" style="margin-top: 12px">
|
|
308
|
+
<section class="card">
|
|
309
|
+
<h2>Right click / Context menu</h2>
|
|
310
|
+
<div class="box" id="context-box">
|
|
311
|
+
<div><strong>context-box</strong><br /><span>right-click here</span></div>
|
|
312
|
+
</div>
|
|
313
|
+
</section>
|
|
314
|
+
<section class="card" style="position: relative">
|
|
315
|
+
<h2>Hover</h2>
|
|
316
|
+
<div class="box" id="hover-box" style="position: relative">
|
|
317
|
+
<div><strong>hover-box</strong><br /><span>shows a tip on hover</span></div>
|
|
318
|
+
<div id="hover-tip">Hello from hover!</div>
|
|
319
|
+
</div>
|
|
320
|
+
</section>
|
|
321
|
+
</div>
|
|
322
|
+
</main>
|
|
323
|
+
|
|
324
|
+
<aside class="sidebar">
|
|
325
|
+
<div class="status">
|
|
326
|
+
<div>Drag result:</div>
|
|
327
|
+
<div id="status" class="value">n/a</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="sep"></div>
|
|
330
|
+
<section class="card">
|
|
331
|
+
<h2>Wheel / Scroll</h2>
|
|
332
|
+
<div class="box" id="wheel-box">
|
|
333
|
+
<div id="wheel-content">Scroll in this box (it overflows)</div>
|
|
334
|
+
</div>
|
|
335
|
+
</section>
|
|
336
|
+
<div id="log" class="log" aria-live="polite"></div>
|
|
337
|
+
</aside>
|
|
338
|
+
|
|
339
|
+
<script>
|
|
340
|
+
(function () {
|
|
341
|
+
const $ = (sel) => document.querySelector(sel);
|
|
342
|
+
const logEl = $('#log');
|
|
343
|
+
const status = $('#status');
|
|
344
|
+
|
|
345
|
+
const coords = $('#coords');
|
|
346
|
+
const stage = $('#stage');
|
|
347
|
+
const src = $('#drag-source');
|
|
348
|
+
const tgt = $('#drag-target');
|
|
349
|
+
|
|
350
|
+
const resetBtn = $('#reset-btn');
|
|
351
|
+
|
|
352
|
+
const fmtTime = () => new Date().toLocaleTimeString();
|
|
353
|
+
function log(msg) {
|
|
354
|
+
const div = document.createElement('div');
|
|
355
|
+
div.className = 'item';
|
|
356
|
+
div.innerHTML = '<span class="time">[' + fmtTime() + ']</span> ' + msg;
|
|
357
|
+
logEl.appendChild(div);
|
|
358
|
+
while (logEl.children.length > 200) logEl.removeChild(logEl.firstChild);
|
|
359
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// coords display
|
|
363
|
+
document.addEventListener(
|
|
364
|
+
'pointermove',
|
|
365
|
+
(e) => {
|
|
366
|
+
coords.textContent = 'x: ' + Math.round(e.clientX) + ', y: ' + Math.round(e.clientY);
|
|
367
|
+
},
|
|
368
|
+
{ passive: true }
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// Press / Release box
|
|
372
|
+
const pressBox = $('#press-box');
|
|
373
|
+
pressBox.addEventListener('pointerdown', (e) => {
|
|
374
|
+
log('press-box: pointerdown button=' + e.button);
|
|
375
|
+
});
|
|
376
|
+
pressBox.addEventListener('pointerup', (e) => {
|
|
377
|
+
log('press-box: pointerup button=' + e.button);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Double-click box
|
|
381
|
+
const dblBox = $('#dblclick-box');
|
|
382
|
+
const dblCount = $('#dblcount');
|
|
383
|
+
dblBox.addEventListener('dblclick', () => {
|
|
384
|
+
dblCount.textContent = String(Number(dblCount.textContent || '0') + 1);
|
|
385
|
+
log('dblclick-box: dblclick');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Context menu (right-click)
|
|
389
|
+
const ctxBox = $('#context-box');
|
|
390
|
+
ctxBox.addEventListener('contextmenu', (e) => {
|
|
391
|
+
e.preventDefault();
|
|
392
|
+
log('context-box: contextmenu (right click)');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Hover tip
|
|
396
|
+
const hoverBox = $('#hover-box');
|
|
397
|
+
const tip = $('#hover-tip');
|
|
398
|
+
hoverBox.addEventListener('pointerenter', () => {
|
|
399
|
+
tip.style.display = 'block';
|
|
400
|
+
log('hover-box: enter');
|
|
401
|
+
});
|
|
402
|
+
hoverBox.addEventListener('pointerleave', () => {
|
|
403
|
+
tip.style.display = 'none';
|
|
404
|
+
log('hover-box: leave');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Wheel box
|
|
408
|
+
const wheelBox = $('#wheel-box');
|
|
409
|
+
wheelBox.addEventListener(
|
|
410
|
+
'wheel',
|
|
411
|
+
(e) => {
|
|
412
|
+
log('wheel-box: wheel dx=' + e.deltaX + ' dy=' + e.deltaY);
|
|
413
|
+
},
|
|
414
|
+
{ passive: true }
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// Drag logic (pointer events, works with WebDriver pointer actions)
|
|
418
|
+
let dragging = false;
|
|
419
|
+
let offsetX = 0;
|
|
420
|
+
let offsetY = 0;
|
|
421
|
+
let startPos = { left: 24, top: 24 };
|
|
422
|
+
|
|
423
|
+
function setSrcPos(left, top) {
|
|
424
|
+
src.style.left = left + 'px';
|
|
425
|
+
src.style.top = top + 'px';
|
|
426
|
+
}
|
|
427
|
+
setSrcPos(startPos.left, startPos.top);
|
|
428
|
+
|
|
429
|
+
src.addEventListener('pointerdown', (e) => {
|
|
430
|
+
dragging = true;
|
|
431
|
+
src.setPointerCapture(e.pointerId);
|
|
432
|
+
const r = src.getBoundingClientRect();
|
|
433
|
+
offsetX = e.clientX - r.left;
|
|
434
|
+
offsetY = e.clientY - r.top;
|
|
435
|
+
log('drag: pointerdown at ' + e.clientX + ',' + e.clientY);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
document.addEventListener('pointermove', (e) => {
|
|
439
|
+
if (!dragging) return;
|
|
440
|
+
const stageR = stage.getBoundingClientRect();
|
|
441
|
+
let left = e.clientX - stageR.left - offsetX;
|
|
442
|
+
let top = e.clientY - stageR.top - offsetY;
|
|
443
|
+
// keep inside stage bounds
|
|
444
|
+
const r = src.getBoundingClientRect();
|
|
445
|
+
const w = r.width,
|
|
446
|
+
h = r.height;
|
|
447
|
+
left = Math.max(0, Math.min(left, stageR.width - w));
|
|
448
|
+
top = Math.max(0, Math.min(top, stageR.height - h));
|
|
449
|
+
setSrcPos(left, top);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
function isFullyInside(inner, outer) {
|
|
453
|
+
const a = inner.getBoundingClientRect();
|
|
454
|
+
const b = outer.getBoundingClientRect();
|
|
455
|
+
return a.left >= b.left && a.top >= b.top && a.right <= b.right && a.bottom <= b.bottom;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function updateDropStatus() {
|
|
459
|
+
const inside1 = isFullyInside(src, tgt);
|
|
460
|
+
status.textContent = inside1 ? 'success' : 'fail';
|
|
461
|
+
status.className = 'value ' + (inside1 ? 'ok' : 'bad');
|
|
462
|
+
tgt.classList.toggle('success', inside1);
|
|
463
|
+
tgt.classList.toggle('fail', !inside1);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
document.addEventListener('pointerup', (e) => {
|
|
467
|
+
if (!dragging) return;
|
|
468
|
+
dragging = false;
|
|
469
|
+
try {
|
|
470
|
+
src.releasePointerCapture(e.pointerId);
|
|
471
|
+
} catch {}
|
|
472
|
+
updateDropStatus();
|
|
473
|
+
const inside1 = isFullyInside(src, tgt);
|
|
474
|
+
const mods =
|
|
475
|
+
[
|
|
476
|
+
e.shiftKey ? 'Shift' : '',
|
|
477
|
+
e.ctrlKey ? 'Ctrl' : '',
|
|
478
|
+
e.altKey ? 'Alt' : '',
|
|
479
|
+
e.metaKey ? 'Meta' : '',
|
|
480
|
+
]
|
|
481
|
+
.filter(Boolean)
|
|
482
|
+
.join('+') || 'none';
|
|
483
|
+
log(
|
|
484
|
+
'drag: drop at ' +
|
|
485
|
+
e.clientX +
|
|
486
|
+
',' +
|
|
487
|
+
e.clientY +
|
|
488
|
+
' — target=' +
|
|
489
|
+
(inside1 ? 'success' : 'fail') +
|
|
490
|
+
', modifiers=' +
|
|
491
|
+
mods
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
resetBtn.addEventListener('click', () => {
|
|
496
|
+
setSrcPos(startPos.left, startPos.top);
|
|
497
|
+
status.textContent = 'n/a';
|
|
498
|
+
status.className = 'value';
|
|
499
|
+
tgt.classList.remove('success', 'fail');
|
|
500
|
+
log('reset');
|
|
501
|
+
});
|
|
502
|
+
})();
|
|
503
|
+
</script>
|
|
504
|
+
</body>
|
|
505
|
+
</html>
|