@jsenv/dom 0.10.3 → 0.10.5

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.
Files changed (2) hide show
  1. package/dist/jsenv_dom.js +365 -269
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -5419,6 +5419,31 @@ const getScrollContainerSet = (element) => {
5419
5419
  return scrollContainerSet;
5420
5420
  };
5421
5421
 
5422
+ /**
5423
+ * Rounds a CSS pixel value to the nearest physical pixel boundary for the current display.
5424
+ *
5425
+ * At zoom levels other than 100%, `devicePixelRatio` is not an integer (e.g. 1.25, 1.5),
5426
+ * so fractional CSS pixel values from `getBoundingClientRect()` may not align to the physical
5427
+ * pixel grid. Setting `top`/`left` to such values causes the browser to interpolate across
5428
+ * pixels, resulting in blurry rendering or misalignment with adjacent elements.
5429
+ *
5430
+ * Snapping to the physical grid ensures the value falls exactly on a pixel boundary.
5431
+ *
5432
+ * @param {number} value - A CSS pixel value (e.g. from getBoundingClientRect or scroll offset).
5433
+ * @returns {number} The nearest physical-pixel-aligned CSS pixel value.
5434
+ * @example
5435
+ * // At devicePixelRatio 1.25, snapToPixel(154.4) → 154.4 (already on grid)
5436
+ * // At devicePixelRatio 1.25, snapToPixel(154.3) → 154.4
5437
+ */
5438
+ const snapToPixel = (value) => {
5439
+ return Math.round(value * devicePixelRatio) / devicePixelRatio;
5440
+ };
5441
+
5442
+ // Round a CSS-pixel value to the nearest physical pixel boundary.
5443
+ // At zoom levels other than 100%, devicePixelRatio is not an integer (e.g. 1.25, 1.5),
5444
+ // so CSS pixels don't align 1:1 with physical pixels. Rounding to the physical grid
5445
+ // ensures the browser can render the element without sub-pixel blurring.
5446
+
5422
5447
  const getBorderSizes = (element) => {
5423
5448
  const {
5424
5449
  borderLeftWidth,
@@ -5426,11 +5451,12 @@ const getBorderSizes = (element) => {
5426
5451
  borderTopWidth,
5427
5452
  borderBottomWidth,
5428
5453
  } = window.getComputedStyle(element, null);
5454
+
5429
5455
  return {
5430
- left: parseFloat(borderLeftWidth),
5431
- right: parseFloat(borderRightWidth),
5432
- top: parseFloat(borderTopWidth),
5433
- bottom: parseFloat(borderBottomWidth),
5456
+ left: snapToPixel(parseFloat(borderLeftWidth)),
5457
+ right: snapToPixel(parseFloat(borderRightWidth)),
5458
+ top: snapToPixel(parseFloat(borderTopWidth)),
5459
+ bottom: snapToPixel(parseFloat(borderBottomWidth)),
5434
5460
  };
5435
5461
  };
5436
5462
 
@@ -5782,8 +5808,8 @@ const measureScrollbar = (scrollableElement) => {
5782
5808
  const scrollbarHeight = scrollDiv.offsetHeight - scrollDiv.clientHeight;
5783
5809
  scrollableElement.removeChild(scrollDiv);
5784
5810
  return [
5785
- hasXScrollbar ? scrollbarWidth : 0,
5786
- hasYScrollbar ? scrollbarHeight : 0,
5811
+ hasXScrollbar ? snapToPixel(scrollbarWidth) : 0,
5812
+ hasYScrollbar ? snapToPixel(scrollbarHeight) : 0,
5787
5813
  ];
5788
5814
  };
5789
5815
 
@@ -6096,6 +6122,18 @@ const scrollIntoViewWithStickyAwareness = (
6096
6122
  }
6097
6123
  };
6098
6124
 
6125
+ const getPaddingSizes = (element) => {
6126
+ const { paddingLeft, paddingRight, paddingTop, paddingBottom } =
6127
+ window.getComputedStyle(element, null);
6128
+
6129
+ return {
6130
+ left: snapToPixel(parseFloat(paddingLeft)),
6131
+ right: snapToPixel(parseFloat(paddingRight)),
6132
+ top: snapToPixel(parseFloat(paddingTop)),
6133
+ bottom: snapToPixel(parseFloat(paddingBottom)),
6134
+ };
6135
+ };
6136
+
6099
6137
  /**
6100
6138
  * Prevents scrolling on all scrollable containers that are ancestors of (or
6101
6139
  * siblings preceding) `element`. Used when an overlay (popover, dialog) is
@@ -6132,11 +6170,10 @@ const trapScrollInside = (element) => {
6132
6170
  return;
6133
6171
  }
6134
6172
  const [scrollbarWidth, scrollbarHeight] = measureScrollbar(el);
6135
- const paddingRight = parseInt(getStyle(el, "padding-right"), 0);
6136
- const paddingBottom = parseInt(getStyle(el, "padding-bottom"), 0);
6173
+ const { right, bottom } = getPaddingSizes(el);
6137
6174
  const removeScrollLockStyles = setStyles(el, {
6138
- "padding-right": `${paddingRight + scrollbarWidth}px`,
6139
- "padding-bottom": `${paddingBottom + scrollbarHeight}px`,
6175
+ "padding-right": `${right + scrollbarWidth}px`,
6176
+ "padding-bottom": `${bottom + scrollbarHeight}px`,
6140
6177
  "overflow": "hidden",
6141
6178
  });
6142
6179
  cleanupCallbackSet.add(removeScrollLockStyles);
@@ -6919,6 +6956,13 @@ installImportMetaCssBuild(import.meta);/**
6919
6956
  * donc juste x/y ca seras surement mieux
6920
6957
  *
6921
6958
  */
6959
+ const css$3 = /* css */`
6960
+ .navi_drag_gesture_backdrop {
6961
+ position: fixed;
6962
+ inset: 0;
6963
+ user-select: none;
6964
+ }
6965
+ `;
6922
6966
  const createDragGestureController = (options = {}) => {
6923
6967
  const {
6924
6968
  name,
@@ -7065,6 +7109,7 @@ const createDragGestureController = (options = {}) => {
7065
7109
 
7066
7110
  // 2. VISUAL CONTROL: Backdrop for consistent cursor and pointer event blocking
7067
7111
  if (backdrop) {
7112
+ import.meta.css = [css$3, "@jsenv/dom/src/interaction/drag/drag_gesture.js"];
7068
7113
  const backdropElement = document.createElement("div");
7069
7114
  backdropElement.className = "navi_drag_gesture_backdrop";
7070
7115
  backdropElement.ariaHidden = "true";
@@ -7421,15 +7466,24 @@ const definePropertyAsReadOnly = (object, propertyName) => {
7421
7466
  value: object[propertyName]
7422
7467
  });
7423
7468
  };
7424
- import.meta.css = [/* css */`
7425
- .navi_drag_gesture_backdrop {
7469
+
7470
+ installImportMetaCssBuild(import.meta);const css$2 = /* css */`
7471
+ .navi_constraint_feedback_line {
7426
7472
  position: fixed;
7427
- inset: 0;
7428
- user-select: none;
7473
+ z-index: 9998;
7474
+ border-top: 2px dotted rgba(59, 130, 246, 0.7);
7475
+ visibility: hidden;
7476
+ transform-origin: left center;
7477
+ transition: opacity 0.15s ease;
7478
+ pointer-events: none;
7429
7479
  }
7430
- `, "@jsenv/dom/src/interaction/drag/drag_gesture.js"];
7431
7480
 
7432
- installImportMetaCssBuild(import.meta);const setupConstraintFeedbackLine = () => {
7481
+ .navi_constraint_feedback_line[data-visible] {
7482
+ visibility: visible;
7483
+ }
7484
+ `;
7485
+ const setupConstraintFeedbackLine = () => {
7486
+ import.meta.css = [css$2, "@jsenv/dom/src/interaction/drag/constraint_feedback_line.js"];
7433
7487
  const constraintFeedbackLine = createConstraintFeedbackLine();
7434
7488
 
7435
7489
  // Track last known mouse position for constraint feedback line during scroll
@@ -7502,21 +7556,6 @@ const createConstraintFeedbackLine = () => {
7502
7556
  document.body.appendChild(line);
7503
7557
  return line;
7504
7558
  };
7505
- import.meta.css = [/* css */`
7506
- .navi_constraint_feedback_line {
7507
- position: fixed;
7508
- z-index: 9998;
7509
- border-top: 2px dotted rgba(59, 130, 246, 0.7);
7510
- visibility: hidden;
7511
- transform-origin: left center;
7512
- transition: opacity 0.15s ease;
7513
- pointer-events: none;
7514
- }
7515
-
7516
- .navi_constraint_feedback_line[data-visible] {
7517
- visibility: visible;
7518
- }
7519
- `, "@jsenv/dom/src/interaction/drag/constraint_feedback_line.js"];
7520
7559
 
7521
7560
  installImportMetaCssBuild(import.meta);// Keep visual markers (debug markers, obstacle markers, constraint feedback line) in DOM after drag ends
7522
7561
  const MARKER_SIZE = 12;
@@ -7524,9 +7563,197 @@ let currentDebugMarkers = [];
7524
7563
  let currentConstraintMarkers = [];
7525
7564
  let currentReferenceElementMarker = null;
7526
7565
  let currentElementMarker = null;
7566
+ const css$1 = /* css */`
7567
+ .navi_debug_markers_container {
7568
+ position: fixed;
7569
+ top: 0;
7570
+ left: 0;
7571
+ z-index: 999998;
7572
+ width: 100vw;
7573
+ height: 100vh;
7574
+ pointer-events: none;
7575
+ overflow: hidden;
7576
+ --marker-size: ${MARKER_SIZE}px;
7577
+ }
7578
+
7579
+ .navi_debug_marker {
7580
+ position: absolute;
7581
+ pointer-events: none;
7582
+ }
7583
+
7584
+ /* Markers based on side rather than orientation */
7585
+ .navi_debug_marker[data-left],
7586
+ .navi_debug_marker[data-right] {
7587
+ width: var(--marker-size);
7588
+ height: 100vh;
7589
+ }
7590
+
7591
+ .navi_debug_marker[data-top],
7592
+ .navi_debug_marker[data-bottom] {
7593
+ width: 100vw;
7594
+ height: var(--marker-size);
7595
+ }
7596
+
7597
+ /* Gradient directions based on side, using CSS custom properties for color */
7598
+ .navi_debug_marker[data-left] {
7599
+ background: linear-gradient(
7600
+ to right,
7601
+ rgba(from var(--marker-color) r g b / 0.9) 0%,
7602
+ rgba(from var(--marker-color) r g b / 0.7) 30%,
7603
+ rgba(from var(--marker-color) r g b / 0.3) 70%,
7604
+ rgba(from var(--marker-color) r g b / 0) 100%
7605
+ );
7606
+ }
7607
+
7608
+ .navi_debug_marker[data-right] {
7609
+ background: linear-gradient(
7610
+ to left,
7611
+ rgba(from var(--marker-color) r g b / 0.9) 0%,
7612
+ rgba(from var(--marker-color) r g b / 0.7) 30%,
7613
+ rgba(from var(--marker-color) r g b / 0.3) 70%,
7614
+ rgba(from var(--marker-color) r g b / 0) 100%
7615
+ );
7616
+ }
7617
+
7618
+ .navi_debug_marker[data-top] {
7619
+ background: linear-gradient(
7620
+ to bottom,
7621
+ rgba(from var(--marker-color) r g b / 0.9) 0%,
7622
+ rgba(from var(--marker-color) r g b / 0.7) 30%,
7623
+ rgba(from var(--marker-color) r g b / 0.3) 70%,
7624
+ rgba(from var(--marker-color) r g b / 0) 100%
7625
+ );
7626
+ }
7627
+
7628
+ .navi_debug_marker[data-bottom] {
7629
+ background: linear-gradient(
7630
+ to top,
7631
+ rgba(from var(--marker-color) r g b / 0.9) 0%,
7632
+ rgba(from var(--marker-color) r g b / 0.7) 30%,
7633
+ rgba(from var(--marker-color) r g b / 0.3) 70%,
7634
+ rgba(from var(--marker-color) r g b / 0) 100%
7635
+ );
7636
+ }
7637
+
7638
+ .navi_debug_marker_label {
7639
+ position: absolute;
7640
+ padding: 2px 6px;
7641
+ color: rgb(from var(--marker-color) r g b / 1);
7642
+ font-weight: bold;
7643
+ font-size: 12px;
7644
+ white-space: nowrap;
7645
+ background: rgba(255, 255, 255, 0.9);
7646
+ border: 1px solid;
7647
+ border-color: rgb(from var(--marker-color) r g b / 1);
7648
+ border-radius: 3px;
7649
+ pointer-events: none;
7650
+ }
7651
+
7652
+ /* Label positioning based on side data attributes */
7653
+
7654
+ /* Left side markers - vertical with 90° rotation */
7655
+ .navi_debug_marker[data-left] .navi_debug_marker_label {
7656
+ top: 20px;
7657
+ left: 10px;
7658
+ transform: rotate(90deg);
7659
+ transform-origin: left center;
7660
+ }
7661
+
7662
+ /* Right side markers - vertical with -90° rotation */
7663
+ .navi_debug_marker[data-right] .navi_debug_marker_label {
7664
+ top: 20px;
7665
+ right: 10px;
7666
+ left: auto;
7667
+ transform: rotate(-90deg);
7668
+ transform-origin: right center;
7669
+ }
7670
+
7671
+ /* Top side markers - horizontal, label on the line */
7672
+ .navi_debug_marker[data-top] .navi_debug_marker_label {
7673
+ top: 0px;
7674
+ left: 20px;
7675
+ }
7676
+
7677
+ /* Bottom side markers - horizontal, label on the line */
7678
+ .navi_debug_marker[data-bottom] .navi_debug_marker_label {
7679
+ top: auto;
7680
+ bottom: 0px;
7681
+ left: 20px;
7682
+ }
7683
+
7684
+ .navi_obstacle_marker {
7685
+ position: absolute;
7686
+ z-index: 9999;
7687
+ background-color: orange;
7688
+ opacity: 0.6;
7689
+ pointer-events: none;
7690
+ }
7691
+
7692
+ .navi_obstacle_marker_label {
7693
+ position: absolute;
7694
+ top: 50%;
7695
+ left: 50%;
7696
+ color: white;
7697
+ font-weight: bold;
7698
+ font-size: 12px;
7699
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
7700
+ transform: translate(-50%, -50%);
7701
+ pointer-events: none;
7702
+ }
7703
+
7704
+ .navi_element_marker {
7705
+ position: absolute;
7706
+ z-index: 9997;
7707
+ background-color: var(--element-color-alpha, rgba(255, 0, 150, 0.3));
7708
+ border: 2px solid var(--element-color, rgb(255, 0, 150));
7709
+ opacity: 0.9;
7710
+ pointer-events: none;
7711
+ }
7712
+
7713
+ .navi_element_marker_label {
7714
+ position: absolute;
7715
+ top: -25px;
7716
+ right: 0;
7717
+ padding: 2px 6px;
7718
+ color: var(--element-color, rgb(255, 0, 150));
7719
+ font-weight: bold;
7720
+ font-size: 11px;
7721
+ white-space: nowrap;
7722
+ background: rgba(255, 255, 255, 0.9);
7723
+ border: 1px solid var(--element-color, rgb(255, 0, 150));
7724
+ border-radius: 3px;
7725
+ pointer-events: none;
7726
+ }
7727
+
7728
+ .navi_reference_element_marker {
7729
+ position: absolute;
7730
+ z-index: 9998;
7731
+ background-color: rgba(0, 150, 255, 0.3);
7732
+ border: 2px dashed rgba(0, 150, 255, 0.7);
7733
+ opacity: 0.8;
7734
+ pointer-events: none;
7735
+ }
7736
+
7737
+ .navi_reference_element_marker_label {
7738
+ position: absolute;
7739
+ top: -25px;
7740
+ left: 0;
7741
+ padding: 2px 6px;
7742
+ color: rgba(0, 150, 255, 1);
7743
+ font-weight: bold;
7744
+ font-size: 11px;
7745
+ white-space: nowrap;
7746
+ background: rgba(255, 255, 255, 0.9);
7747
+ border: 1px solid rgba(0, 150, 255, 0.7);
7748
+ border-radius: 3px;
7749
+ pointer-events: none;
7750
+ }
7751
+ `;
7527
7752
  const setupDragDebugMarkers = (dragGesture, {
7528
7753
  referenceElement
7529
7754
  }) => {
7755
+ import.meta.css = [css$1, "@jsenv/dom/src/interaction/drag/drag_debug_markers.js"];
7756
+
7530
7757
  // Clean up any existing persistent markers from previous drag gestures
7531
7758
  {
7532
7759
  // Remove any existing markers from previous gestures
@@ -7913,192 +8140,6 @@ const createReferenceElementMarker = ({
7913
8140
  container.appendChild(marker);
7914
8141
  return marker;
7915
8142
  };
7916
- import.meta.css = [/* css */`
7917
- .navi_debug_markers_container {
7918
- position: fixed;
7919
- top: 0;
7920
- left: 0;
7921
- z-index: 999998;
7922
- width: 100vw;
7923
- height: 100vh;
7924
- pointer-events: none;
7925
- overflow: hidden;
7926
- --marker-size: ${MARKER_SIZE}px;
7927
- }
7928
-
7929
- .navi_debug_marker {
7930
- position: absolute;
7931
- pointer-events: none;
7932
- }
7933
-
7934
- /* Markers based on side rather than orientation */
7935
- .navi_debug_marker[data-left],
7936
- .navi_debug_marker[data-right] {
7937
- width: var(--marker-size);
7938
- height: 100vh;
7939
- }
7940
-
7941
- .navi_debug_marker[data-top],
7942
- .navi_debug_marker[data-bottom] {
7943
- width: 100vw;
7944
- height: var(--marker-size);
7945
- }
7946
-
7947
- /* Gradient directions based on side, using CSS custom properties for color */
7948
- .navi_debug_marker[data-left] {
7949
- background: linear-gradient(
7950
- to right,
7951
- rgba(from var(--marker-color) r g b / 0.9) 0%,
7952
- rgba(from var(--marker-color) r g b / 0.7) 30%,
7953
- rgba(from var(--marker-color) r g b / 0.3) 70%,
7954
- rgba(from var(--marker-color) r g b / 0) 100%
7955
- );
7956
- }
7957
-
7958
- .navi_debug_marker[data-right] {
7959
- background: linear-gradient(
7960
- to left,
7961
- rgba(from var(--marker-color) r g b / 0.9) 0%,
7962
- rgba(from var(--marker-color) r g b / 0.7) 30%,
7963
- rgba(from var(--marker-color) r g b / 0.3) 70%,
7964
- rgba(from var(--marker-color) r g b / 0) 100%
7965
- );
7966
- }
7967
-
7968
- .navi_debug_marker[data-top] {
7969
- background: linear-gradient(
7970
- to bottom,
7971
- rgba(from var(--marker-color) r g b / 0.9) 0%,
7972
- rgba(from var(--marker-color) r g b / 0.7) 30%,
7973
- rgba(from var(--marker-color) r g b / 0.3) 70%,
7974
- rgba(from var(--marker-color) r g b / 0) 100%
7975
- );
7976
- }
7977
-
7978
- .navi_debug_marker[data-bottom] {
7979
- background: linear-gradient(
7980
- to top,
7981
- rgba(from var(--marker-color) r g b / 0.9) 0%,
7982
- rgba(from var(--marker-color) r g b / 0.7) 30%,
7983
- rgba(from var(--marker-color) r g b / 0.3) 70%,
7984
- rgba(from var(--marker-color) r g b / 0) 100%
7985
- );
7986
- }
7987
-
7988
- .navi_debug_marker_label {
7989
- position: absolute;
7990
- padding: 2px 6px;
7991
- color: rgb(from var(--marker-color) r g b / 1);
7992
- font-weight: bold;
7993
- font-size: 12px;
7994
- white-space: nowrap;
7995
- background: rgba(255, 255, 255, 0.9);
7996
- border: 1px solid;
7997
- border-color: rgb(from var(--marker-color) r g b / 1);
7998
- border-radius: 3px;
7999
- pointer-events: none;
8000
- }
8001
-
8002
- /* Label positioning based on side data attributes */
8003
-
8004
- /* Left side markers - vertical with 90° rotation */
8005
- .navi_debug_marker[data-left] .navi_debug_marker_label {
8006
- top: 20px;
8007
- left: 10px;
8008
- transform: rotate(90deg);
8009
- transform-origin: left center;
8010
- }
8011
-
8012
- /* Right side markers - vertical with -90° rotation */
8013
- .navi_debug_marker[data-right] .navi_debug_marker_label {
8014
- top: 20px;
8015
- right: 10px;
8016
- left: auto;
8017
- transform: rotate(-90deg);
8018
- transform-origin: right center;
8019
- }
8020
-
8021
- /* Top side markers - horizontal, label on the line */
8022
- .navi_debug_marker[data-top] .navi_debug_marker_label {
8023
- top: 0px;
8024
- left: 20px;
8025
- }
8026
-
8027
- /* Bottom side markers - horizontal, label on the line */
8028
- .navi_debug_marker[data-bottom] .navi_debug_marker_label {
8029
- top: auto;
8030
- bottom: 0px;
8031
- left: 20px;
8032
- }
8033
-
8034
- .navi_obstacle_marker {
8035
- position: absolute;
8036
- z-index: 9999;
8037
- background-color: orange;
8038
- opacity: 0.6;
8039
- pointer-events: none;
8040
- }
8041
-
8042
- .navi_obstacle_marker_label {
8043
- position: absolute;
8044
- top: 50%;
8045
- left: 50%;
8046
- color: white;
8047
- font-weight: bold;
8048
- font-size: 12px;
8049
- text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
8050
- transform: translate(-50%, -50%);
8051
- pointer-events: none;
8052
- }
8053
-
8054
- .navi_element_marker {
8055
- position: absolute;
8056
- z-index: 9997;
8057
- background-color: var(--element-color-alpha, rgba(255, 0, 150, 0.3));
8058
- border: 2px solid var(--element-color, rgb(255, 0, 150));
8059
- opacity: 0.9;
8060
- pointer-events: none;
8061
- }
8062
-
8063
- .navi_element_marker_label {
8064
- position: absolute;
8065
- top: -25px;
8066
- right: 0;
8067
- padding: 2px 6px;
8068
- color: var(--element-color, rgb(255, 0, 150));
8069
- font-weight: bold;
8070
- font-size: 11px;
8071
- white-space: nowrap;
8072
- background: rgba(255, 255, 255, 0.9);
8073
- border: 1px solid var(--element-color, rgb(255, 0, 150));
8074
- border-radius: 3px;
8075
- pointer-events: none;
8076
- }
8077
-
8078
- .navi_reference_element_marker {
8079
- position: absolute;
8080
- z-index: 9998;
8081
- background-color: rgba(0, 150, 255, 0.3);
8082
- border: 2px dashed rgba(0, 150, 255, 0.7);
8083
- opacity: 0.8;
8084
- pointer-events: none;
8085
- }
8086
-
8087
- .navi_reference_element_marker_label {
8088
- position: absolute;
8089
- top: -25px;
8090
- left: 0;
8091
- padding: 2px 6px;
8092
- color: rgba(0, 150, 255, 1);
8093
- font-weight: bold;
8094
- font-size: 11px;
8095
- white-space: nowrap;
8096
- background: rgba(255, 255, 255, 0.9);
8097
- border: 1px solid rgba(0, 150, 255, 0.7);
8098
- border-radius: 3px;
8099
- pointer-events: none;
8100
- }
8101
- `, "@jsenv/dom/src/interaction/drag/drag_debug_markers.js"];
8102
8143
 
8103
8144
  const initDragConstraints = (
8104
8145
  dragGesture,
@@ -8730,6 +8771,7 @@ const createStickyFrontierOnAxis = (
8730
8771
  const dragStyleController = createStyleController("drag_to_move");
8731
8772
 
8732
8773
  const createDragToMoveGestureController = ({
8774
+ cloneOnDrag = false,
8733
8775
  stickyFrontiers = true,
8734
8776
  // Padding to reduce the area used to autoscroll by this amount (applied after sticky frontiers)
8735
8777
  // This creates an invisible space around the area where elements cannot be dragged
@@ -8743,8 +8785,8 @@ const createDragToMoveGestureController = ({
8743
8785
  // position due to obstacles, boundaries, or other constraints. The line originates from where the mouse
8744
8786
  // initially grabbed the element, but moves with the element to show the current anchor position.
8745
8787
  // It becomes visible when there's a significant distance between mouse and grab point.
8746
- showConstraintFeedbackLine = true,
8747
- showDebugMarkers = true,
8788
+ showConstraintFeedbackLine = false,
8789
+ showDebugMarkers = false,
8748
8790
  resetPositionAfterRelease = false,
8749
8791
  ...options
8750
8792
  } = {}) => {
@@ -8752,6 +8794,18 @@ const createDragToMoveGestureController = ({
8752
8794
  dragGesture,
8753
8795
  { element, referenceElement, elementToMove, convertScrollablePosition },
8754
8796
  ) => {
8797
+ if (cloneOnDrag) {
8798
+ const { grabEvent } = dragGesture.gestureInfo;
8799
+ const ghostData = createDragGhost(element, {
8800
+ clientX: grabEvent.clientX,
8801
+ clientY: grabEvent.clientY,
8802
+ });
8803
+ elementToMove = ghostData.ghostWrapper;
8804
+ dragGesture.gestureInfo.elementImpacted = ghostData.ghostWrapper;
8805
+ dragGesture.addReleaseCallback(() => {
8806
+ ghostData.remove();
8807
+ });
8808
+ }
8755
8809
  const direction = dragGesture.gestureInfo.direction;
8756
8810
  // const dragGestureName = dragGesture.gestureInfo.name;
8757
8811
  const scrollContainer = dragGesture.gestureInfo.scrollContainer;
@@ -9021,6 +9075,31 @@ const createDragToMoveGestureController = ({
9021
9075
  return dragGestureController;
9022
9076
  };
9023
9077
 
9078
+ const createDragGhost = (element, pointerEvent) => {
9079
+ const rect = element.getBoundingClientRect();
9080
+
9081
+ const ghost = element.cloneNode(true);
9082
+ ghost.dataset.dragging = "";
9083
+ ghost.style.pointerEvents = "none";
9084
+ // transform-origin set to pointer position within the element for natural scale expansion
9085
+ const originX = pointerEvent.clientX - rect.left;
9086
+ const originY = pointerEvent.clientY - rect.top;
9087
+ ghost.style.transformOrigin = `${originX}px ${originY}px`;
9088
+
9089
+ const ghostWrapper = document.createElement("div");
9090
+ ghostWrapper.style.cssText = `position: absolute; pointer-events: none; z-index: 9999; top: ${rect.top + window.scrollY}px; left: ${rect.left + window.scrollX}px; width: ${rect.width}px;`;
9091
+ ghostWrapper.appendChild(ghost);
9092
+ document.body.appendChild(ghostWrapper);
9093
+
9094
+ return {
9095
+ ghost,
9096
+ ghostWrapper,
9097
+ remove: () => {
9098
+ ghostWrapper.remove();
9099
+ },
9100
+ };
9101
+ };
9102
+
9024
9103
  const startDragToResizeGesture = (
9025
9104
  pointerdownEvent,
9026
9105
  { onDragStart, onDrag, onRelease, ...options },
@@ -9096,7 +9175,7 @@ const getResizeDirection = (element) => {
9096
9175
  * @returns {Object|null} Drop target info with elementSide or null if no valid target found
9097
9176
  */
9098
9177
  const getDropTargetInfo = (gestureInfo, targetElements) => {
9099
- const dragElement = gestureInfo.element;
9178
+ const dragElement = gestureInfo.elementImpacted || gestureInfo.element;
9100
9179
  const dragElementRect = dragElement.getBoundingClientRect();
9101
9180
  const intersectingTargets = [];
9102
9181
  let someTargetIsCol;
@@ -9141,12 +9220,13 @@ const getDropTargetInfo = (gestureInfo, targetElements) => {
9141
9220
  const elementsUnderDragElement = document.elementsFromPoint(clientX, clientY);
9142
9221
  let targetElement = null;
9143
9222
  let targetIndex = -1;
9223
+ let intersectingIndex = -1;
9144
9224
  for (const element of elementsUnderDragElement) {
9145
9225
  // First, check if the element itself is a target
9146
9226
  const directIndex = intersectingTargets.indexOf(element);
9147
9227
  if (directIndex !== -1) {
9148
9228
  targetElement = element;
9149
- targetIndex = directIndex;
9229
+ intersectingIndex = directIndex;
9150
9230
  break;
9151
9231
  }
9152
9232
  // Special case: if element is <td> or <th> and not in targets,
@@ -9167,7 +9247,7 @@ const getDropTargetInfo = (gestureInfo, targetElements) => {
9167
9247
  break try_col;
9168
9248
  }
9169
9249
  targetElement = tableCellCol;
9170
- targetIndex = colIndex;
9250
+ intersectingIndex = colIndex;
9171
9251
  break;
9172
9252
  }
9173
9253
  try_tr: {
@@ -9180,26 +9260,31 @@ const getDropTargetInfo = (gestureInfo, targetElements) => {
9180
9260
  break try_tr;
9181
9261
  }
9182
9262
  targetElement = tableRow;
9183
- targetIndex = rowIndex;
9263
+ intersectingIndex = intersectingTargets.indexOf(tableRow);
9184
9264
  break;
9185
9265
  }
9186
9266
  }
9187
9267
  if (!targetElement) {
9188
9268
  targetElement = intersectingTargets[0];
9189
- targetIndex = 0;
9269
+ intersectingIndex = 0;
9190
9270
  }
9271
+ targetIndex = targetElements.indexOf(targetElement);
9191
9272
 
9192
9273
  // Determine position within the target for both axes
9193
9274
  const targetRect = targetElement.getBoundingClientRect();
9194
9275
  const targetCenterX = targetRect.left + targetRect.width / 2;
9195
9276
  const targetCenterY = targetRect.top + targetRect.height / 2;
9196
9277
  const result = {
9278
+ // Index of the target element within the original targetElements array
9197
9279
  index: targetIndex,
9198
9280
  element: targetElement,
9199
9281
  elementSide: {
9200
9282
  x: dragElementRect.left < targetCenterX ? "start" : "end",
9201
9283
  y: dragElementRect.top < targetCenterY ? "start" : "end",
9202
9284
  },
9285
+ // Index within the intersecting subset — could be useful to know how many
9286
+ // elements were overlapping, but rarely needed in practice
9287
+ intersectingIndex,
9203
9288
  intersecting: intersectingTargets,
9204
9289
  };
9205
9290
  return result;
@@ -9284,15 +9369,16 @@ installImportMetaCssBuild(import.meta);/**
9284
9369
  *
9285
9370
  * The element should have a CSS "top" value specified (e.g., top: 10px).
9286
9371
  */
9287
- import.meta.css = [/* css */`
9372
+ const css = /* css */`
9288
9373
  [data-position-sticky-placeholder] {
9289
9374
  position: static !important;
9290
9375
  width: auto !important;
9291
9376
  height: auto !important;
9292
9377
  opacity: 0 !important;
9293
9378
  }
9294
- `, "@jsenv/dom/src/position/position_sticky.js"];
9379
+ `;
9295
9380
  const initPositionSticky = element => {
9381
+ import.meta.css = [css, "@jsenv/dom/src/position/position_sticky.js"];
9296
9382
  const computedStyle = getComputedStyle(element);
9297
9383
  const topCssValue = computedStyle.top;
9298
9384
  const top = parseFloat(topCssValue);
@@ -9930,6 +10016,8 @@ const pickPositionRelativeTo = (
9930
10016
  positionYFixed,
9931
10017
  alignToViewportEdgeWhenAnchorNearEdge = 0,
9932
10018
  minLeft = 0,
10019
+ spacing = 0,
10020
+ viewportSpacing = 0,
9933
10021
  } = {},
9934
10022
  ) => {
9935
10023
 
@@ -9944,12 +10032,10 @@ const pickPositionRelativeTo = (
9944
10032
  top: elementTop,
9945
10033
  bottom: elementBottom,
9946
10034
  } = elementRect;
9947
- const {
9948
- left: anchorLeft,
9949
- right: anchorRight,
9950
- top: anchorTop,
9951
- bottom: anchorBottom,
9952
- } = anchorRect;
10035
+ const anchorLeft = snapToPixel(anchorRect.left);
10036
+ const anchorTop = snapToPixel(anchorRect.top);
10037
+ const anchorRight = snapToPixel(anchorRect.right);
10038
+ const anchorBottom = snapToPixel(anchorRect.bottom);
9953
10039
  const elementWidth = elementRight - elementLeft;
9954
10040
  const elementHeight = elementBottom - elementTop;
9955
10041
  const anchorWidth = anchorRight - anchorLeft;
@@ -9992,16 +10078,16 @@ const pickPositionRelativeTo = (
9992
10078
  // Compute effective space for a given Y value
9993
10079
  const spaceFor = (y) => {
9994
10080
  if (y === "above") {
9995
- return spaceAbove;
10081
+ return spaceAbove - spacing - viewportSpacing;
9996
10082
  }
9997
10083
  if (y === "above-overlap") {
9998
- return spaceAbove + anchorHeight;
10084
+ return spaceAbove + anchorHeight - viewportSpacing;
9999
10085
  }
10000
10086
  if (y === "below") {
10001
- return spaceBelow;
10087
+ return spaceBelow - spacing - viewportSpacing;
10002
10088
  }
10003
10089
  if (y === "below-overlap") {
10004
- return spaceBelow + anchorHeight;
10090
+ return spaceBelow + anchorHeight - viewportSpacing;
10005
10091
  }
10006
10092
  return Infinity; // center
10007
10093
  };
@@ -10049,16 +10135,16 @@ const pickPositionRelativeTo = (
10049
10135
  // Compute effective space for a given X value
10050
10136
  const spaceFor = (x) => {
10051
10137
  if (x === "to-the-left") {
10052
- return spaceLeft;
10138
+ return spaceLeft - spacing - viewportSpacing;
10053
10139
  }
10054
10140
  if (x === "left-aligned") {
10055
- return viewportWidth - anchorLeft;
10141
+ return viewportWidth - anchorLeft - viewportSpacing;
10056
10142
  }
10057
10143
  if (x === "right-aligned") {
10058
- return anchorRight;
10144
+ return anchorRight - viewportSpacing;
10059
10145
  }
10060
10146
  if (x === "to-the-right") {
10061
- return spaceRight;
10147
+ return spaceRight - spacing - viewportSpacing;
10062
10148
  }
10063
10149
  return Infinity; // center
10064
10150
  };
@@ -10095,7 +10181,7 @@ const pickPositionRelativeTo = (
10095
10181
  let elementPositionLeft;
10096
10182
  {
10097
10183
  if (finalX === "to-the-left") {
10098
- elementPositionLeft = anchorLeft - elementWidth;
10184
+ elementPositionLeft = anchorLeft - elementWidth - spacing;
10099
10185
  } else if (finalX === "left-aligned") {
10100
10186
  elementPositionLeft = anchorLeft;
10101
10187
  } else if (finalX === "center") {
@@ -10132,13 +10218,16 @@ const pickPositionRelativeTo = (
10132
10218
  elementPositionLeft = anchorRight - elementWidth;
10133
10219
  } else {
10134
10220
  // "to-the-right"
10135
- elementPositionLeft = anchorRight;
10221
+ elementPositionLeft = anchorRight + spacing;
10136
10222
  }
10137
- // Constrain horizontal position to viewport boundaries
10138
- if (elementPositionLeft < 0) {
10139
- elementPositionLeft = 0;
10140
- } else if (elementPositionLeft + elementWidth > viewportWidth) {
10141
- elementPositionLeft = viewportWidth - elementWidth;
10223
+ // Constrain horizontal position to viewport boundaries (with viewportSpacing margin)
10224
+ if (elementPositionLeft < viewportSpacing) {
10225
+ elementPositionLeft = viewportSpacing;
10226
+ } else if (
10227
+ elementPositionLeft + elementWidth >
10228
+ viewportWidth - viewportSpacing
10229
+ ) {
10230
+ elementPositionLeft = viewportWidth - viewportSpacing - elementWidth;
10142
10231
  }
10143
10232
  }
10144
10233
 
@@ -10146,11 +10235,14 @@ const pickPositionRelativeTo = (
10146
10235
  let elementPositionTop;
10147
10236
  {
10148
10237
  if (finalY === "above") {
10149
- const idealTop = anchorTop - elementHeight;
10150
- elementPositionTop = idealTop < 0 ? 0 : idealTop;
10238
+ // top is always anchorTop - elementHeight - spacing — max-height truncates if needed.
10239
+ const idealTop = anchorTop - elementHeight - spacing;
10240
+ elementPositionTop =
10241
+ idealTop < viewportSpacing ? viewportSpacing : idealTop;
10151
10242
  } else if (finalY === "above-overlap") {
10152
10243
  const idealTop = anchorBottom - elementHeight;
10153
- elementPositionTop = idealTop < 0 ? 0 : idealTop;
10244
+ elementPositionTop =
10245
+ idealTop < viewportSpacing ? viewportSpacing : idealTop;
10154
10246
  } else if (finalY === "center") {
10155
10247
  elementPositionTop = anchorTop + anchorHeight / 2 - elementHeight / 2;
10156
10248
  } else if (finalY === "below-overlap") {
@@ -10159,7 +10251,9 @@ const pickPositionRelativeTo = (
10159
10251
  idealTop % 1 === 0 ? idealTop : Math.floor(idealTop) + 1;
10160
10252
  } else {
10161
10253
  // "below"
10162
- const idealTop = anchorBottom;
10254
+ // top is always anchorBottom + spacing — max-height (via --space-available) truncates
10255
+ // the element height so it doesn't overflow the viewport bottom.
10256
+ const idealTop = anchorBottom + spacing;
10163
10257
  elementPositionTop =
10164
10258
  idealTop % 1 === 0 ? idealTop : Math.floor(idealTop) + 1;
10165
10259
  }
@@ -10176,13 +10270,26 @@ const pickPositionRelativeTo = (
10176
10270
 
10177
10271
  // Get document scroll for final coordinate conversion
10178
10272
  const { scrollLeft, scrollTop } = document.documentElement;
10179
- const elementDocumentLeft = elementPositionLeft + scrollLeft;
10180
- const elementDocumentTop = elementPositionTop + scrollTop;
10273
+ const elementDocumentLeft = snapToPixel(elementPositionLeft + scrollLeft);
10274
+ const elementDocumentTop = snapToPixel(elementPositionTop + scrollTop);
10181
10275
  const anchorDocumentLeft = anchorLeft + scrollLeft;
10182
10276
  const anchorDocumentTop = anchorTop + scrollTop;
10183
10277
  const anchorDocumentRight = anchorRight + scrollLeft;
10184
10278
  const anchorDocumentBottom = anchorBottom + scrollTop;
10185
10279
 
10280
+ // For overlap variants the element starts at the anchor edge (not past it),
10281
+ // so the usable space includes the anchor dimension.
10282
+ // spacing (gap between anchor and element) and viewportSpacing are subtracted
10283
+ // so callers get the net usable space directly.
10284
+ const effectiveSpaceAbove =
10285
+ (finalY === "above-overlap" ? spaceAbove + anchorHeight : spaceAbove) -
10286
+ (finalY === "above" ? spacing : 0) -
10287
+ viewportSpacing;
10288
+ const effectiveSpaceBelow =
10289
+ (finalY === "below-overlap" ? spaceBelow + anchorHeight : spaceBelow) -
10290
+ (finalY === "below" ? spacing : 0) -
10291
+ viewportSpacing;
10292
+
10186
10293
  return {
10187
10294
  positionX: finalX,
10188
10295
  positionY: finalY,
@@ -10194,10 +10301,10 @@ const pickPositionRelativeTo = (
10194
10301
  anchorTop: anchorDocumentTop,
10195
10302
  anchorRight: anchorDocumentRight,
10196
10303
  anchorBottom: anchorDocumentBottom,
10197
- spaceLeft,
10198
- spaceRight,
10199
- spaceAbove,
10200
- spaceBelow,
10304
+ spaceLeft: spaceLeft - viewportSpacing,
10305
+ spaceRight: spaceRight - viewportSpacing,
10306
+ spaceAbove: effectiveSpaceAbove,
10307
+ spaceBelow: effectiveSpaceBelow,
10201
10308
  };
10202
10309
  };
10203
10310
 
@@ -11973,17 +12080,6 @@ const getWidthWithoutTransition = (element) =>
11973
12080
  const getHeightWithoutTransition = (element) =>
11974
12081
  getHeight$1(element, transitionStyleController);
11975
12082
 
11976
- const getPaddingSizes = (element) => {
11977
- const { paddingLeft, paddingRight, paddingTop, paddingBottom } =
11978
- window.getComputedStyle(element, null);
11979
- return {
11980
- left: parseFloat(paddingLeft),
11981
- right: parseFloat(paddingRight),
11982
- top: parseFloat(paddingTop),
11983
- bottom: parseFloat(paddingBottom),
11984
- };
11985
- };
11986
-
11987
12083
  const getInnerHeight = (element) => {
11988
12084
  // Always subtract paddings and borders to get the content height
11989
12085
  const paddingSizes = getPaddingSizes(element);
@@ -13324,4 +13420,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
13324
13420
  };
13325
13421
  };
13326
13422
 
13327
- export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isSameColor, isScrollable, measureScrollbar, mergeOneStyle, mergeTwoStyles, normalizeStyles, parseStyle, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
13423
+ export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isSameColor, isScrollable, measureScrollbar, mergeOneStyle, mergeTwoStyles, normalizeStyle, normalizeStyles, parseStyle, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, snapToPixel, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.10.3",
3
+ "version": "0.10.5",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {