react-grab 0.0.25 → 0.0.26

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { render, createComponent, memo, template, effect, style, setStyleProperty, insert, use } from 'solid-js/web';
1
+ import { render, createComponent, memo, template, effect, style, insert, setStyleProperty, use } from 'solid-js/web';
2
2
  import { createRoot, createSignal, createMemo, createEffect, on, onCleanup, Show, For, onMount } from 'solid-js';
3
3
  import { instrument, _fiberRoots, getFiberFromHostInstance } from 'bippy';
4
4
  import { getOwnerStack, getSourcesFromStack } from 'bippy/dist/source';
@@ -110,11 +110,12 @@ var SelectionBox = (props) => {
110
110
  const [opacity, setOpacity] = createSignal(1);
111
111
  let hasBeenRenderedOnce = false;
112
112
  let animationFrameId = null;
113
+ let fadeTimerId = null;
113
114
  let targetBounds = props.bounds;
114
115
  let isAnimating = false;
115
116
  const lerpFactor = () => {
116
117
  if (props.lerpFactor !== void 0) return props.lerpFactor;
117
- if (props.variant === "drag") return 0.9;
118
+ if (props.variant === "drag") return 0.7;
118
119
  return SELECTION_LERP_FACTOR;
119
120
  };
120
121
  const startAnimation = () => {
@@ -151,11 +152,22 @@ var SelectionBox = (props) => {
151
152
  }
152
153
  startAnimation();
153
154
  }));
155
+ createEffect(() => {
156
+ if (props.variant === "grabbed" && props.createdAt) {
157
+ fadeTimerId = window.setTimeout(() => {
158
+ setOpacity(0);
159
+ }, 1500);
160
+ }
161
+ });
154
162
  onCleanup(() => {
155
163
  if (animationFrameId !== null) {
156
164
  cancelAnimationFrame(animationFrameId);
157
165
  animationFrameId = null;
158
166
  }
167
+ if (fadeTimerId !== null) {
168
+ window.clearTimeout(fadeTimerId);
169
+ fadeTimerId = null;
170
+ }
159
171
  isAnimating = false;
160
172
  });
161
173
  const baseStyle = {
@@ -167,8 +179,8 @@ var SelectionBox = (props) => {
167
179
  const variantStyle = () => {
168
180
  if (props.variant === "drag") {
169
181
  return {
170
- border: "1px dashed rgb(210, 57, 192)",
171
- "background-color": "rgba(210, 57, 192, 0.15)",
182
+ border: "1px dashed rgba(210, 57, 192, 0.4)",
183
+ "background-color": "rgba(210, 57, 192, 0.05)",
172
184
  "will-change": "transform, width, height",
173
185
  contain: "layout paint size",
174
186
  cursor: "crosshair"
@@ -243,11 +255,47 @@ var getClampedElementPosition = (positionLeft, positionTop, elementWidth, elemen
243
255
 
244
256
  // src/components/label.tsx
245
257
  var _tmpl$3 = /* @__PURE__ */ template(`<span style=display:inline-block;margin-right:4px;font-weight:600>\u2713`);
246
- var _tmpl$22 = /* @__PURE__ */ template(`<div style=margin-right:4px>Grabbed`);
247
- var _tmpl$32 = /* @__PURE__ */ template(`<div style="position:fixed;padding:2px 6px;background-color:#fde7f7;color:#b21c8e;border:1px solid #f7c5ec;border-radius:4px;font-size:11px;font-weight:500;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;pointer-events:none;transition:opacity 0.2s ease-in-out;display:flex;align-items:center;max-width:calc(100vw - (16px + env(safe-area-inset-left) + env(safe-area-inset-right)));overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><span style="font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;font-variant-numeric:tabular-nums">`);
258
+ var _tmpl$22 = /* @__PURE__ */ template(`<div style=margin-right:4px>Copied`);
259
+ var _tmpl$32 = /* @__PURE__ */ template(`<div style=margin-left:4px>to clipboard`);
260
+ var _tmpl$4 = /* @__PURE__ */ template(`<div style="position:fixed;padding:2px 6px;background-color:#fde7f7;color:#b21c8e;border:1px solid #f7c5ec;border-radius:4px;font-size:11px;font-weight:500;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;pointer-events:none;transition:opacity 0.2s ease-in-out;display:flex;align-items:center;max-width:calc(100vw - (16px + env(safe-area-inset-left) + env(safe-area-inset-right)));overflow:hidden;text-overflow:ellipsis;white-space:nowrap">`);
261
+ var _tmpl$5 = /* @__PURE__ */ template(`<span style="font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;font-variant-numeric:tabular-nums">`);
248
262
  var Label = (props) => {
249
263
  const [opacity, setOpacity] = createSignal(0);
264
+ const [positionTick, setPositionTick] = createSignal(0);
250
265
  let labelRef;
266
+ let currentX = props.x;
267
+ let currentY = props.y;
268
+ let targetX = props.x;
269
+ let targetY = props.y;
270
+ let animationFrameId = null;
271
+ let hasBeenRenderedOnce = false;
272
+ const animate = () => {
273
+ currentX = lerp(currentX, targetX, 0.3);
274
+ currentY = lerp(currentY, targetY, 0.3);
275
+ setPositionTick((tick) => tick + 1);
276
+ const hasConvergedToTarget = Math.abs(currentX - targetX) < 0.5 && Math.abs(currentY - targetY) < 0.5;
277
+ if (!hasConvergedToTarget) {
278
+ animationFrameId = requestAnimationFrame(animate);
279
+ } else {
280
+ animationFrameId = null;
281
+ }
282
+ };
283
+ const startAnimation = () => {
284
+ if (animationFrameId !== null) return;
285
+ animationFrameId = requestAnimationFrame(animate);
286
+ };
287
+ const updateTarget = () => {
288
+ targetX = props.x;
289
+ targetY = props.y;
290
+ if (!hasBeenRenderedOnce) {
291
+ currentX = targetX;
292
+ currentY = targetY;
293
+ hasBeenRenderedOnce = true;
294
+ setPositionTick((tick) => tick + 1);
295
+ return;
296
+ }
297
+ startAnimation();
298
+ };
251
299
  createEffect(on(() => props.visible, (visible) => {
252
300
  if (visible !== false) {
253
301
  requestAnimationFrame(() => {
@@ -264,16 +312,26 @@ var Label = (props) => {
264
312
  onCleanup(() => clearTimeout(fadeOutTimer));
265
313
  }
266
314
  }));
315
+ createEffect(() => {
316
+ updateTarget();
317
+ });
318
+ onCleanup(() => {
319
+ if (animationFrameId !== null) {
320
+ cancelAnimationFrame(animationFrameId);
321
+ animationFrameId = null;
322
+ }
323
+ });
267
324
  const labelBoundingRect = () => labelRef?.getBoundingClientRect();
268
325
  const computedPosition = () => {
326
+ positionTick();
269
327
  const boundingRect = labelBoundingRect();
270
328
  if (!boundingRect) return {
271
- left: props.x,
272
- top: props.y
329
+ left: currentX,
330
+ top: currentY
273
331
  };
274
332
  if (props.variant === "success") {
275
- const indicatorLeft = Math.round(props.x);
276
- const indicatorTop = Math.round(props.y) - boundingRect.height - 6;
333
+ const indicatorLeft = Math.round(currentX);
334
+ const indicatorTop = Math.round(currentY) - boundingRect.height - 6;
277
335
  const willClampLeft = indicatorLeft < VIEWPORT_MARGIN_PX;
278
336
  const willClampTop = indicatorTop < VIEWPORT_MARGIN_PX;
279
337
  const isClamped = willClampLeft || willClampTop;
@@ -288,17 +346,17 @@ var Label = (props) => {
288
346
  const viewportWidth = window.innerWidth;
289
347
  const viewportHeight = window.innerHeight;
290
348
  const quadrants = [{
291
- left: Math.round(props.x) + CROSSHAIR_OFFSET,
292
- top: Math.round(props.y) + CROSSHAIR_OFFSET
349
+ left: Math.round(currentX) + CROSSHAIR_OFFSET,
350
+ top: Math.round(currentY) + CROSSHAIR_OFFSET
293
351
  }, {
294
- left: Math.round(props.x) - boundingRect.width - CROSSHAIR_OFFSET,
295
- top: Math.round(props.y) + CROSSHAIR_OFFSET
352
+ left: Math.round(currentX) - boundingRect.width - CROSSHAIR_OFFSET,
353
+ top: Math.round(currentY) + CROSSHAIR_OFFSET
296
354
  }, {
297
- left: Math.round(props.x) + CROSSHAIR_OFFSET,
298
- top: Math.round(props.y) - boundingRect.height - CROSSHAIR_OFFSET
355
+ left: Math.round(currentX) + CROSSHAIR_OFFSET,
356
+ top: Math.round(currentY) - boundingRect.height - CROSSHAIR_OFFSET
299
357
  }, {
300
- left: Math.round(props.x) - boundingRect.width - CROSSHAIR_OFFSET,
301
- top: Math.round(props.y) - boundingRect.height - CROSSHAIR_OFFSET
358
+ left: Math.round(currentX) - boundingRect.width - CROSSHAIR_OFFSET,
359
+ top: Math.round(currentY) - boundingRect.height - CROSSHAIR_OFFSET
302
360
  }];
303
361
  for (const position of quadrants) {
304
362
  const fitsHorizontally = position.left >= VIEWPORT_MARGIN_PX && position.left + boundingRect.width <= viewportWidth - VIEWPORT_MARGIN_PX;
@@ -317,7 +375,7 @@ var Label = (props) => {
317
375
  return props.visible !== false;
318
376
  },
319
377
  get children() {
320
- var _el$ = _tmpl$32(), _el$4 = _el$.firstChild;
378
+ var _el$ = _tmpl$4();
321
379
  var _ref$ = labelRef;
322
380
  typeof _ref$ === "function" ? use(_ref$, _el$) : labelRef = _el$;
323
381
  insert(_el$, createComponent(Show, {
@@ -327,7 +385,7 @@ var Label = (props) => {
327
385
  get children() {
328
386
  return createComponent(Spinner, {});
329
387
  }
330
- }), _el$4);
388
+ }), null);
331
389
  insert(_el$, createComponent(Show, {
332
390
  get when() {
333
391
  return props.variant === "success";
@@ -335,7 +393,7 @@ var Label = (props) => {
335
393
  get children() {
336
394
  return _tmpl$3();
337
395
  }
338
- }), _el$4);
396
+ }), null);
339
397
  insert(_el$, createComponent(Show, {
340
398
  get when() {
341
399
  return props.variant === "success";
@@ -343,14 +401,36 @@ var Label = (props) => {
343
401
  get children() {
344
402
  return _tmpl$22();
345
403
  }
346
- }), _el$4);
404
+ }), null);
347
405
  insert(_el$, createComponent(Show, {
348
406
  get when() {
349
407
  return props.variant === "processing";
350
408
  },
351
409
  children: "Grabbing\u2026"
352
- }), _el$4);
353
- insert(_el$4, () => props.text);
410
+ }), null);
411
+ insert(_el$, createComponent(Show, {
412
+ get when() {
413
+ return props.text.startsWith("(");
414
+ },
415
+ get fallback() {
416
+ return (() => {
417
+ var _el$5 = _tmpl$5();
418
+ insert(_el$5, () => props.text);
419
+ return _el$5;
420
+ })();
421
+ },
422
+ get children() {
423
+ return props.text;
424
+ }
425
+ }), null);
426
+ insert(_el$, createComponent(Show, {
427
+ get when() {
428
+ return props.variant === "success";
429
+ },
430
+ get children() {
431
+ return _tmpl$32();
432
+ }
433
+ }), null);
354
434
  effect((_p$) => {
355
435
  var _v$ = `${computedPosition().top}px`, _v$2 = `${computedPosition().left}px`, _v$3 = props.zIndex?.toString() ?? "2147483647", _v$4 = opacity();
356
436
  _v$ !== _p$.e && setStyleProperty(_el$, "top", _p$.e = _v$);
@@ -368,7 +448,7 @@ var Label = (props) => {
368
448
  }
369
449
  });
370
450
  };
371
- var _tmpl$4 = /* @__PURE__ */ template(`<div style="position:fixed;z-index:2147483647;pointer-events:none;transition:opacity 0.1s ease-in-out"><div style="width:32px;height:2px;background-color:rgba(178, 28, 142, 0.2);border-radius:1px;overflow:hidden;position:relative"><div style="height:100%;background-color:#b21c8e;border-radius:1px;transition:width 0.05s cubic-bezier(0.165, 0.84, 0.44, 1)">`);
451
+ var _tmpl$6 = /* @__PURE__ */ template(`<div style="position:fixed;z-index:2147483647;pointer-events:none;transition:opacity 0.1s ease-in-out"><div style="width:32px;height:2px;background-color:rgba(178, 28, 142, 0.2);border-radius:1px;overflow:hidden;position:relative"><div style="height:100%;background-color:#b21c8e;border-radius:1px;transition:width 0.05s cubic-bezier(0.165, 0.84, 0.44, 1)">`);
372
452
  var useFadeInOut = (visible) => {
373
453
  const [opacity, setOpacity] = createSignal(0);
374
454
  createEffect(on(() => visible, (isVisible) => {
@@ -401,7 +481,7 @@ var ProgressIndicator = (props) => {
401
481
  return props.visible !== false;
402
482
  },
403
483
  get children() {
404
- var _el$ = _tmpl$4(), _el$2 = _el$.firstChild, _el$3 = _el$2.firstChild;
484
+ var _el$ = _tmpl$6(), _el$2 = _el$.firstChild, _el$3 = _el$2.firstChild;
405
485
  var _ref$ = progressIndicatorRef;
406
486
  typeof _ref$ === "function" ? use(_ref$, _el$) : progressIndicatorRef = _el$;
407
487
  effect((_p$) => {
@@ -421,66 +501,99 @@ var ProgressIndicator = (props) => {
421
501
  }
422
502
  });
423
503
  };
424
- var _tmpl$5 = /* @__PURE__ */ template(`<div style=position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:2147483645><div style="position:absolute;top:0;width:1px;height:100%;background-color:rgba(210, 57, 192, 0.5);will-change:transform"></div><div style="position:absolute;left:0;width:100%;height:1px;background-color:rgba(210, 57, 192, 0.5);will-change:transform">`);
504
+ var _tmpl$7 = /* @__PURE__ */ template(`<canvas style=position:fixed;top:0;left:0;pointer-events:none;z-index:2147483645>`);
425
505
  var Crosshair = (props) => {
426
- const [currentX, setCurrentX] = createSignal(props.mouseX);
427
- const [currentY, setCurrentY] = createSignal(props.mouseY);
428
- let hasBeenRenderedOnce = false;
429
- let animationFrameId = null;
506
+ let canvasRef;
507
+ let context = null;
508
+ let width = 0;
509
+ let height = 0;
510
+ let dpr = 1;
511
+ let currentX = props.mouseX;
512
+ let currentY = props.mouseY;
430
513
  let targetX = props.mouseX;
431
514
  let targetY = props.mouseY;
432
- let isAnimating = false;
515
+ let animationFrameId = null;
516
+ let hasBeenRenderedOnce = false;
517
+ const setupCanvas = () => {
518
+ if (!canvasRef) return;
519
+ dpr = Math.max(window.devicePixelRatio || 1, 2);
520
+ width = window.innerWidth;
521
+ height = window.innerHeight;
522
+ canvasRef.width = width * dpr;
523
+ canvasRef.height = height * dpr;
524
+ canvasRef.style.width = `${width}px`;
525
+ canvasRef.style.height = `${height}px`;
526
+ context = canvasRef.getContext("2d");
527
+ if (context) {
528
+ context.scale(dpr, dpr);
529
+ }
530
+ };
531
+ const render2 = () => {
532
+ if (!context) return;
533
+ context.clearRect(0, 0, width, height);
534
+ context.strokeStyle = "rgba(210, 57, 192)";
535
+ context.lineWidth = 1;
536
+ context.beginPath();
537
+ context.moveTo(currentX, 0);
538
+ context.lineTo(currentX, height);
539
+ context.moveTo(0, currentY);
540
+ context.lineTo(width, currentY);
541
+ context.stroke();
542
+ };
543
+ const animate = () => {
544
+ currentX = lerp(currentX, targetX, 0.3);
545
+ currentY = lerp(currentY, targetY, 0.3);
546
+ render2();
547
+ const hasConvergedToTarget = Math.abs(currentX - targetX) < 0.5 && Math.abs(currentY - targetY) < 0.5;
548
+ if (!hasConvergedToTarget) {
549
+ animationFrameId = requestAnimationFrame(animate);
550
+ } else {
551
+ animationFrameId = null;
552
+ }
553
+ };
433
554
  const startAnimation = () => {
434
- if (isAnimating) return;
435
- isAnimating = true;
436
- const animate = () => {
437
- const interpolatedX = lerp(currentX(), targetX, 0.3);
438
- const interpolatedY = lerp(currentY(), targetY, 0.3);
439
- setCurrentX(interpolatedX);
440
- setCurrentY(interpolatedY);
441
- const hasConvergedToTarget = Math.abs(interpolatedX - targetX) < 0.5 && Math.abs(interpolatedY - targetY) < 0.5;
442
- if (!hasConvergedToTarget) {
443
- animationFrameId = requestAnimationFrame(animate);
444
- } else {
445
- animationFrameId = null;
446
- isAnimating = false;
447
- }
448
- };
555
+ if (animationFrameId !== null) return;
449
556
  animationFrameId = requestAnimationFrame(animate);
450
557
  };
451
- createEffect(on(() => [props.mouseX, props.mouseY], ([newMouseX, newMouseY]) => {
452
- targetX = newMouseX;
453
- targetY = newMouseY;
558
+ const updateTarget = () => {
559
+ targetX = props.mouseX;
560
+ targetY = props.mouseY;
454
561
  if (!hasBeenRenderedOnce) {
455
- setCurrentX(targetX);
456
- setCurrentY(targetY);
562
+ currentX = targetX;
563
+ currentY = targetY;
457
564
  hasBeenRenderedOnce = true;
565
+ render2();
458
566
  return;
459
567
  }
460
568
  startAnimation();
461
- }));
462
- onCleanup(() => {
463
- if (animationFrameId !== null) {
464
- cancelAnimationFrame(animationFrameId);
465
- animationFrameId = null;
466
- }
467
- isAnimating = false;
569
+ };
570
+ createEffect(() => {
571
+ setupCanvas();
572
+ render2();
573
+ const handleResize = () => {
574
+ setupCanvas();
575
+ render2();
576
+ };
577
+ window.addEventListener("resize", handleResize);
578
+ onCleanup(() => {
579
+ window.removeEventListener("resize", handleResize);
580
+ if (animationFrameId !== null) {
581
+ cancelAnimationFrame(animationFrameId);
582
+ animationFrameId = null;
583
+ }
584
+ });
585
+ });
586
+ createEffect(() => {
587
+ updateTarget();
468
588
  });
469
589
  return createComponent(Show, {
470
590
  get when() {
471
591
  return props.visible !== false;
472
592
  },
473
593
  get children() {
474
- var _el$ = _tmpl$5(), _el$2 = _el$.firstChild, _el$3 = _el$2.nextSibling;
475
- effect((_p$) => {
476
- var _v$ = `${currentX()}px`, _v$2 = `${currentY()}px`;
477
- _v$ !== _p$.e && setStyleProperty(_el$2, "left", _p$.e = _v$);
478
- _v$2 !== _p$.t && setStyleProperty(_el$3, "top", _p$.t = _v$2);
479
- return _p$;
480
- }, {
481
- e: void 0,
482
- t: void 0
483
- });
594
+ var _el$ = _tmpl$7();
595
+ var _ref$ = canvasRef;
596
+ typeof _ref$ === "function" ? use(_ref$, _el$) : canvasRef = _el$;
484
597
  return _el$;
485
598
  }
486
599
  });
@@ -541,6 +654,9 @@ var ReactGrabRenderer = (props) => {
541
654
  variant: "grabbed",
542
655
  get bounds() {
543
656
  return box.bounds;
657
+ },
658
+ get createdAt() {
659
+ return box.createdAt;
544
660
  }
545
661
  })
546
662
  }), createComponent(For, {
@@ -971,11 +1087,65 @@ var filterElementsInDrag = (dragRect, isValidGrabbableElement2, shouldCheckCover
971
1087
  }
972
1088
  return elements;
973
1089
  };
1090
+ var removeNestedElements = (elements) => {
1091
+ return elements.filter((element) => {
1092
+ return !elements.some(
1093
+ (otherElement) => otherElement !== element && otherElement.contains(element)
1094
+ );
1095
+ });
1096
+ };
1097
+ var findBestParentElement = (elements, dragRect, isValidGrabbableElement2) => {
1098
+ if (elements.length <= 1) return null;
1099
+ const dragLeft = dragRect.x;
1100
+ const dragTop = dragRect.y;
1101
+ const dragRight = dragRect.x + dragRect.width;
1102
+ const dragBottom = dragRect.y + dragRect.height;
1103
+ let currentParent = elements[0];
1104
+ while (currentParent) {
1105
+ const parent = currentParent.parentElement;
1106
+ if (!parent) break;
1107
+ const parentRect = parent.getBoundingClientRect();
1108
+ const intersectionLeft = Math.max(dragLeft, parentRect.left);
1109
+ const intersectionTop = Math.max(dragTop, parentRect.top);
1110
+ const intersectionRight = Math.min(dragRight, parentRect.left + parentRect.width);
1111
+ const intersectionBottom = Math.min(dragBottom, parentRect.top + parentRect.height);
1112
+ const intersectionWidth = Math.max(0, intersectionRight - intersectionLeft);
1113
+ const intersectionHeight = Math.max(0, intersectionBottom - intersectionTop);
1114
+ const intersectionArea = intersectionWidth * intersectionHeight;
1115
+ const parentArea = Math.max(0, parentRect.width * parentRect.height);
1116
+ const hasMajorityCoverage = parentArea > 0 && intersectionArea / parentArea >= DRAG_COVERAGE_THRESHOLD;
1117
+ if (!hasMajorityCoverage) break;
1118
+ if (!isValidGrabbableElement2(parent)) {
1119
+ currentParent = parent;
1120
+ continue;
1121
+ }
1122
+ const allChildrenInParent = elements.every(
1123
+ (element) => parent.contains(element)
1124
+ );
1125
+ if (allChildrenInParent) {
1126
+ return parent;
1127
+ }
1128
+ currentParent = parent;
1129
+ }
1130
+ return null;
1131
+ };
974
1132
  var getElementsInDrag = (dragRect, isValidGrabbableElement2) => {
975
- return filterElementsInDrag(dragRect, isValidGrabbableElement2, true);
1133
+ const elements = filterElementsInDrag(dragRect, isValidGrabbableElement2, true);
1134
+ const uniqueElements = removeNestedElements(elements);
1135
+ const bestParent = findBestParentElement(uniqueElements, dragRect, isValidGrabbableElement2);
1136
+ if (bestParent) {
1137
+ return [bestParent];
1138
+ }
1139
+ return uniqueElements;
976
1140
  };
977
1141
  var getElementsInDragLoose = (dragRect, isValidGrabbableElement2) => {
978
- return filterElementsInDrag(dragRect, isValidGrabbableElement2, false);
1142
+ const elements = filterElementsInDrag(dragRect, isValidGrabbableElement2, false);
1143
+ const uniqueElements = removeNestedElements(elements);
1144
+ const bestParent = findBestParentElement(uniqueElements, dragRect, isValidGrabbableElement2);
1145
+ if (bestParent) {
1146
+ return [bestParent];
1147
+ }
1148
+ return uniqueElements;
979
1149
  };
980
1150
 
981
1151
  // src/utils/create-element-bounds.ts
@@ -1020,22 +1190,26 @@ var init = (rawOptions) => {
1020
1190
  const [successLabels, setSuccessLabels] = createSignal([]);
1021
1191
  const [isActivated, setIsActivated] = createSignal(false);
1022
1192
  const [showProgressIndicator, setShowProgressIndicator] = createSignal(false);
1023
- const [grabMouseX, setGrabMouseX] = createSignal(null);
1024
- const [grabMouseY, setGrabMouseY] = createSignal(null);
1025
1193
  let holdTimerId = null;
1026
1194
  let progressAnimationId = null;
1027
1195
  let progressDelayTimerId = null;
1196
+ let keydownSpamTimerId = null;
1028
1197
  const isRendererActive = createMemo(() => isActivated() && !isCopying());
1029
1198
  const hasValidMousePosition = createMemo(() => mouseX() > OFFSCREEN_POSITION && mouseY() > OFFSCREEN_POSITION);
1030
1199
  const isTargetKeyCombination = (event) => (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "c";
1031
1200
  const addGrabbedBox = (bounds) => {
1032
1201
  const boxId = `grabbed-${Date.now()}-${Math.random()}`;
1202
+ const createdAt = Date.now();
1033
1203
  const newBox = {
1034
1204
  id: boxId,
1035
- bounds
1205
+ bounds,
1206
+ createdAt
1036
1207
  };
1037
1208
  const currentBoxes = grabbedBoxes();
1038
1209
  setGrabbedBoxes([...currentBoxes, newBox]);
1210
+ setTimeout(() => {
1211
+ setGrabbedBoxes((previousBoxes) => previousBoxes.filter((box) => box.id !== boxId));
1212
+ }, SUCCESS_LABEL_DURATION_MS);
1039
1213
  };
1040
1214
  const addSuccessLabel = (text, positionX, positionY) => {
1041
1215
  const labelId = `success-${Date.now()}-${Math.random()}`;
@@ -1058,6 +1232,9 @@ var init = (rawOptions) => {
1058
1232
  return ` ${functionName} - ${fileName}:${lineNumber}:${columnNumber}`;
1059
1233
  }).join("\n");
1060
1234
  };
1235
+ const wrapContextInXmlTags = (context) => {
1236
+ return `<selected_element>${context}</selected_element>`;
1237
+ };
1061
1238
  const getElementContentWithTrace = async (element) => {
1062
1239
  const elementHtml = getHTMLSnippet(element);
1063
1240
  const componentStackTrace = await getSourceTrace(element);
@@ -1074,12 +1251,10 @@ ${formattedStackTrace}`;
1074
1251
  const handleCopy = async (targetElement2) => {
1075
1252
  const elementBounds = targetElement2.getBoundingClientRect();
1076
1253
  const tagName = getElementTagName(targetElement2);
1077
- setGrabMouseX(mouseX());
1078
- setGrabMouseY(mouseY());
1079
1254
  addGrabbedBox(createElementBounds(targetElement2));
1080
1255
  try {
1081
1256
  const content = await getElementContentWithTrace(targetElement2);
1082
- await copyContent(content);
1257
+ await copyContent(wrapContextInXmlTags(content));
1083
1258
  } catch {
1084
1259
  }
1085
1260
  addSuccessLabel(tagName ? `<${tagName}>` : "<element>", elementBounds.left, elementBounds.top);
@@ -1088,8 +1263,6 @@ ${formattedStackTrace}`;
1088
1263
  if (targetElements.length === 0) return;
1089
1264
  let minPositionX = Infinity;
1090
1265
  let minPositionY = Infinity;
1091
- setGrabMouseX(mouseX());
1092
- setGrabMouseY(mouseY());
1093
1266
  for (const element of targetElements) {
1094
1267
  const elementBounds = element.getBoundingClientRect();
1095
1268
  minPositionX = Math.min(minPositionX, elementBounds.left);
@@ -1099,7 +1272,7 @@ ${formattedStackTrace}`;
1099
1272
  try {
1100
1273
  const elementSnippets = await Promise.all(targetElements.map((element) => getElementContentWithTrace(element)));
1101
1274
  const combinedContent = elementSnippets.join("\n\n---\n\n");
1102
- await copyContent(combinedContent);
1275
+ await copyContent(wrapContextInXmlTags(combinedContent));
1103
1276
  } catch {
1104
1277
  }
1105
1278
  addSuccessLabel(`${targetElements.length} elements`, minPositionX, minPositionY);
@@ -1122,7 +1295,7 @@ ${formattedStackTrace}`;
1122
1295
  y: elementBounds.top
1123
1296
  };
1124
1297
  });
1125
- const DRAG_THRESHOLD_PX = 5;
1298
+ const DRAG_THRESHOLD_PX = 2;
1126
1299
  const getDragDistance = (endX, endY) => ({
1127
1300
  x: Math.abs(endX - dragStartX()),
1128
1301
  y: Math.abs(endY - dragStartY())
@@ -1158,7 +1331,7 @@ ${formattedStackTrace}`;
1158
1331
  });
1159
1332
  const labelText = createMemo(() => {
1160
1333
  const element = targetElement();
1161
- if (!element) return "";
1334
+ if (!element) return "(click or drag to select element(s))";
1162
1335
  const tagName = getElementTagName(element);
1163
1336
  return tagName ? `<${tagName}>` : "<element>";
1164
1337
  });
@@ -1178,18 +1351,6 @@ ${formattedStackTrace}`;
1178
1351
  setLastGrabbedElement(null);
1179
1352
  }
1180
1353
  }));
1181
- createEffect(on(() => [mouseX(), mouseY(), grabMouseX(), grabMouseY()], ([currentMouseX, currentMouseY, initialGrabMouseX, initialGrabMouseY]) => {
1182
- if (initialGrabMouseX === null || initialGrabMouseY === null) return;
1183
- if (grabbedBoxes().length === 0) return;
1184
- const MOUSE_MOVE_THRESHOLD_PX = 5;
1185
- const distanceX = Math.abs(currentMouseX - initialGrabMouseX);
1186
- const distanceY = Math.abs(currentMouseY - initialGrabMouseY);
1187
- if (distanceX > MOUSE_MOVE_THRESHOLD_PX || distanceY > MOUSE_MOVE_THRESHOLD_PX) {
1188
- setGrabbedBoxes([]);
1189
- setGrabMouseX(null);
1190
- setGrabMouseY(null);
1191
- }
1192
- }));
1193
1354
  const progress = createMemo(() => {
1194
1355
  const startTime = progressStartTime();
1195
1356
  progressTick();
@@ -1231,43 +1392,55 @@ ${formattedStackTrace}`;
1231
1392
  setIsActivated(true);
1232
1393
  document.body.style.cursor = "crosshair";
1233
1394
  };
1395
+ const deactivateRenderer = () => {
1396
+ setIsHoldingKeys(false);
1397
+ setIsActivated(false);
1398
+ document.body.style.cursor = "";
1399
+ if (isDragging()) {
1400
+ setIsDragging(false);
1401
+ document.body.style.userSelect = "";
1402
+ }
1403
+ if (holdTimerId) window.clearTimeout(holdTimerId);
1404
+ if (keydownSpamTimerId) window.clearTimeout(keydownSpamTimerId);
1405
+ stopProgressAnimation();
1406
+ };
1234
1407
  const abortController = new AbortController();
1235
1408
  const eventListenerSignal = abortController.signal;
1236
1409
  window.addEventListener("keydown", (event) => {
1237
1410
  if (event.key === "Escape" && isHoldingKeys()) {
1238
- setIsHoldingKeys(false);
1239
- setIsActivated(false);
1240
- document.body.style.cursor = "";
1241
- if (isDragging()) {
1242
- setIsDragging(false);
1243
- document.body.style.userSelect = "";
1244
- }
1245
- if (holdTimerId) window.clearTimeout(holdTimerId);
1246
- stopProgressAnimation();
1411
+ deactivateRenderer();
1247
1412
  return;
1248
1413
  }
1249
1414
  if (isKeyboardEventTriggeredByInput(event)) return;
1250
- if (isTargetKeyCombination(event) && !isHoldingKeys()) {
1251
- setIsHoldingKeys(true);
1252
- startProgressAnimation();
1253
- holdTimerId = window.setTimeout(() => {
1254
- activateRenderer();
1255
- options.onActivate?.();
1256
- }, options.keyHoldDuration);
1415
+ if (isTargetKeyCombination(event)) {
1416
+ if (!isHoldingKeys()) {
1417
+ setIsHoldingKeys(true);
1418
+ startProgressAnimation();
1419
+ holdTimerId = window.setTimeout(() => {
1420
+ activateRenderer();
1421
+ options.onActivate?.();
1422
+ }, options.keyHoldDuration);
1423
+ }
1424
+ if (isActivated()) {
1425
+ if (keydownSpamTimerId) window.clearTimeout(keydownSpamTimerId);
1426
+ keydownSpamTimerId = window.setTimeout(() => {
1427
+ deactivateRenderer();
1428
+ }, 200);
1429
+ }
1257
1430
  }
1258
1431
  }, {
1259
1432
  signal: eventListenerSignal
1260
1433
  });
1261
1434
  window.addEventListener("keyup", (event) => {
1262
- if (isHoldingKeys() && (!isTargetKeyCombination(event) || event.key.toLowerCase() === "c")) {
1263
- setIsHoldingKeys(false);
1264
- setIsActivated(false);
1265
- document.body.style.cursor = "";
1266
- if (holdTimerId) window.clearTimeout(holdTimerId);
1267
- stopProgressAnimation();
1435
+ if (!isHoldingKeys() && !isActivated()) return;
1436
+ const isReleasingC = event.key.toLowerCase() === "c";
1437
+ const isReleasingModifier = !event.metaKey && !event.ctrlKey;
1438
+ if (isReleasingC || isReleasingModifier) {
1439
+ deactivateRenderer();
1268
1440
  }
1269
1441
  }, {
1270
- signal: eventListenerSignal
1442
+ signal: eventListenerSignal,
1443
+ capture: true
1271
1444
  });
1272
1445
  window.addEventListener("mousemove", (event) => {
1273
1446
  setMouseX(event.clientX);
@@ -1330,6 +1503,7 @@ ${formattedStackTrace}`;
1330
1503
  onCleanup(() => {
1331
1504
  abortController.abort();
1332
1505
  if (holdTimerId) window.clearTimeout(holdTimerId);
1506
+ if (keydownSpamTimerId) window.clearTimeout(keydownSpamTimerId);
1333
1507
  stopProgressAnimation();
1334
1508
  document.body.style.userSelect = "";
1335
1509
  document.body.style.cursor = "";
@@ -1338,7 +1512,7 @@ ${formattedStackTrace}`;
1338
1512
  const selectionVisible = createMemo(() => false);
1339
1513
  const dragVisible = createMemo(() => isRendererActive() && isDraggingBeyondThreshold());
1340
1514
  const labelVariant = createMemo(() => isCopying() ? "processing" : "hover");
1341
- const labelVisible = createMemo(() => isRendererActive() && !isDragging() && !!targetElement() && !isSameAsLast() || isCopying());
1515
+ const labelVisible = createMemo(() => isRendererActive() && !isDragging() && (!!targetElement() && !isSameAsLast() || !targetElement()) || isCopying());
1342
1516
  const progressVisible = createMemo(() => isHoldingKeys() && showProgressIndicator() && hasValidMousePosition());
1343
1517
  const crosshairVisible = createMemo(() => isRendererActive() && !isDragging());
1344
1518
  render(() => createComponent(ReactGrabRenderer, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-grab",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "react",