@jsenv/dom 0.17.92 → 0.17.94

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 +221 -54
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -12491,17 +12491,9 @@ const roundForConstraints = (value) => {
12491
12491
  *
12492
12492
  * @param {Object} gestureInfo - Gesture information
12493
12493
  * @param {Element[]} targetElements - Array of potential drop target elements
12494
- * @param {object} [options]
12495
- * @param {boolean} [options.fallbackToEdge=false] - When true and the drag element does
12496
- * not intersect any target, falls back to the first item (if above all items) or the
12497
- * last item (if below all items) so there is always a valid drop target at list edges.
12498
12494
  * @returns {Object|null} Drop target info with elementSide or null if no valid target found
12499
12495
  */
12500
- const getDropTargetInfo = (
12501
- gestureInfo,
12502
- targetElements,
12503
- { fallbackToEdge = false } = {},
12504
- ) => {
12496
+ const getDropTargetInfo = (gestureInfo, targetElements) => {
12505
12497
  const dragElement = gestureInfo.elementImpacted || gestureInfo.element;
12506
12498
  const dragElementRect = dragElement.getBoundingClientRect();
12507
12499
  const intersectingTargets = [];
@@ -12522,38 +12514,6 @@ const getDropTargetInfo = (
12522
12514
  }
12523
12515
 
12524
12516
  if (intersectingTargets.length === 0) {
12525
- if (fallbackToEdge) {
12526
- const dragElement = gestureInfo.elementImpacted || gestureInfo.element;
12527
- const dragElementRect = dragElement.getBoundingClientRect();
12528
- const firstItem = targetElements[0];
12529
- const lastItem = targetElements[targetElements.length - 1];
12530
- if (
12531
- firstItem &&
12532
- dragElementRect.bottom < firstItem.getBoundingClientRect().top
12533
- ) {
12534
- // Drag element is above all items → treat as hovering the first item from the top.
12535
- return {
12536
- element: firstItem,
12537
- elementSide: { x: "start", y: "start" },
12538
- index: 0,
12539
- intersectingIndex: 0,
12540
- intersecting: [firstItem],
12541
- };
12542
- }
12543
- if (
12544
- lastItem &&
12545
- dragElementRect.top > lastItem.getBoundingClientRect().bottom
12546
- ) {
12547
- // Drag element is below all items → treat as hovering the last item from the bottom.
12548
- return {
12549
- element: lastItem,
12550
- elementSide: { x: "start", y: "end" },
12551
- index: targetElements.length - 1,
12552
- intersectingIndex: 0,
12553
- intersecting: [lastItem],
12554
- };
12555
- }
12556
- }
12557
12517
  return null;
12558
12518
  }
12559
12519
 
@@ -14668,6 +14628,22 @@ const css$1 = /* css */ `.navi_drop_hint {
14668
14628
  --drop-hint-y: calc(var(--drop-target-bottom) + var(--drop-hint-margin-y, 0px));
14669
14629
  }
14670
14630
 
14631
+ .navi_drop_hint[data-drop-edge="left"], .navi_drop_hint[data-drop-edge="right"] {
14632
+ top: calc(var(--drop-target-top) + var(--drop-hint-margin-y, 0px));
14633
+ left: var(--drop-hint-x);
14634
+ width: var(--drop-hint-size, 3px);
14635
+ height: calc(var(--drop-target-height) - 2 * var(--drop-hint-margin-y, 0px));
14636
+ transform: translateX(-50%);
14637
+ }
14638
+
14639
+ .navi_drop_hint[data-drop-edge="left"] {
14640
+ --drop-hint-x: calc(var(--drop-target-left) - var(--drop-hint-margin-x, 0px));
14641
+ }
14642
+
14643
+ .navi_drop_hint[data-drop-edge="right"] {
14644
+ --drop-hint-x: calc(var(--drop-target-right) + var(--drop-hint-margin-x, 0px));
14645
+ }
14646
+
14671
14647
  .navi_drop_hint_cap {
14672
14648
  color: var(--drop-hint-background-color, #4476ff);
14673
14649
  display: flex;
@@ -14691,6 +14667,23 @@ const css$1 = /* css */ `.navi_drop_hint {
14691
14667
  rotate: 90deg;
14692
14668
  }
14693
14669
 
14670
+ .navi_drop_hint:is([data-drop-edge="left"], [data-drop-edge="right"]) .navi_drop_hint_cap {
14671
+ left: 50%;
14672
+ right: auto;
14673
+ translate: -50%;
14674
+ }
14675
+
14676
+ .navi_drop_hint:is([data-drop-edge="left"], [data-drop-edge="right"]) .navi_drop_hint_cap[data-side="start"] {
14677
+ top: calc(-1 * var(--drop-hint-arrow-size, 11px));
14678
+ rotate: none;
14679
+ }
14680
+
14681
+ .navi_drop_hint:is([data-drop-edge="left"], [data-drop-edge="right"]) .navi_drop_hint_cap[data-side="end"] {
14682
+ top: auto;
14683
+ bottom: calc(-1 * var(--drop-hint-arrow-size, 11px));
14684
+ rotate: 180deg;
14685
+ }
14686
+
14694
14687
  .navi_drop_surface {
14695
14688
  inset: auto;
14696
14689
  top: var(--drop-target-top);
@@ -15294,6 +15287,12 @@ const createDragToMoveGestureController = ({
15294
15287
  * @param {string} [options.itemSelector] What matches the items of the list.
15295
15288
  * Left out, nothing is a place: no hint is drawn and no landing can be
15296
15289
  * answered, which is what a drag that only ever throws the thing away asks for.
15290
+ * @param {"column"|"row"|"wrap"} [options.itemFlow] How the items of a
15291
+ * reordered list are laid out: one under the other, side by side on one line,
15292
+ * or side by side over as many lines as it takes (inline-blocks, a flex-wrap,
15293
+ * a grid). A row runs in the writing direction of `containerElement`. It
15294
+ * decides which side of an item the copy is read on and where the hint is
15295
+ * drawn. Left out, it is read from where the items stand (see readItemFlow).
15297
15296
  * @param {function} [options.getItemId] `getItemId(element) → id`. An id rather
15298
15297
  * than a DOM index, because the two do not match: a list draws fewer rows than
15299
15298
  * it has, a search reorders them, and the structure may have holes.
@@ -15733,6 +15732,15 @@ const resolveDropMeaning = ({
15733
15732
  return "cancel";
15734
15733
  };
15735
15734
 
15735
+ // Which edge of its anchor the reorder hint is drawn at, for each way the items
15736
+ // can follow each other (see readItemFlow): before an item, the edge the flow
15737
+ // comes in by; after the last one, the edge it leaves by.
15738
+ const DROP_HINT_EDGES = {
15739
+ down: { before: "top", after: "bottom" },
15740
+ right: { before: "left", after: "right" },
15741
+ left: { before: "right", after: "left" },
15742
+ };
15743
+
15736
15744
  /**
15737
15745
  * A COPY of the element is carried, and the original keeps its place in the
15738
15746
  * layout — which is what makes a reorder possible at all: nothing else moves
@@ -15762,6 +15770,7 @@ const startDragToCarryCopy = (
15762
15770
  areaConstraint = canToss || canLeave ? "none" : undefined,
15763
15771
  containerElement = draggedElement.parentElement,
15764
15772
  itemSelector,
15773
+ itemFlow,
15765
15774
  getItemId,
15766
15775
  onReorder,
15767
15776
  onLand,
@@ -15799,6 +15808,7 @@ const startDragToCarryCopy = (
15799
15808
  if (!isPrimaryButtonEvent(event)) {
15800
15809
  return undefined;
15801
15810
  }
15811
+ const flowSaid = resolveItemFlow(itemFlow, containerElement);
15802
15812
  // Where the hand is, for a press handed over: its `pointerdown` says where it
15803
15813
  // WAS, before the hold and whatever the hold opened. Let go of meanwhile, it
15804
15814
  // is not picked up after the fact.
@@ -15913,6 +15923,7 @@ const startDragToCarryCopy = (
15913
15923
  dropHintEl.style.removeProperty("--drop-target-top");
15914
15924
  dropHintEl.style.removeProperty("--drop-target-bottom");
15915
15925
  dropHintEl.style.removeProperty("--drop-target-left");
15926
+ dropHintEl.style.removeProperty("--drop-target-right");
15916
15927
  dropHintEl.style.removeProperty("--drop-target-width");
15917
15928
  dropHintEl.style.removeProperty("--drop-target-height");
15918
15929
  };
@@ -15954,12 +15965,10 @@ const startDragToCarryCopy = (
15954
15965
  }
15955
15966
  }
15956
15967
 
15957
- const dropTargetInfo = getDropTargetInfo(gestureInfo, items, {
15958
- // The edges of a LIST: above the first row means the top of it, below
15959
- // the last one means the end of it. A board has no such reading — away
15960
- // from every place is away from every place.
15961
- fallbackToEdge: !canLand,
15962
- });
15968
+ const flow = canLand ? undefined : flowSaid || readItemFlow(allItems);
15969
+ const dropTargetInfo = canLand
15970
+ ? getDropTargetInfo(gestureInfo, items)
15971
+ : findPlaceInList(gestureInfo, items, flow);
15963
15972
  gestureInfo.dropTargetInfo = dropTargetInfo || null;
15964
15973
  if (!dropTargetInfo) {
15965
15974
  clearDropHint();
@@ -15993,14 +16002,13 @@ const startDragToCarryCopy = (
15993
16002
  );
15994
16003
  return;
15995
16004
  }
15996
- // Convert {element, edge} to a beforeElement using the items array
16005
+ // Convert {element, side} to a beforeElement using the items array
15997
16006
  // (not nextElementSibling, which breaks if non-item elements exist between items).
15998
- // edge "start" → insert before the hovered element
15999
- // edge "end" → insert before the next item (null = append at end)
16000
- const edge = dropTargetInfo.elementSide.y;
16007
+ // side "start" → insert before the hovered element
16008
+ // side "end" → insert before the next item (null = append at end)
16001
16009
  const hoveredIndex = items.indexOf(dropTargetInfo.element);
16002
16010
  const beforeElement =
16003
- edge === "start"
16011
+ dropTargetInfo.side === "start"
16004
16012
  ? dropTargetInfo.element
16005
16013
  : (items[hoveredIndex + 1] ?? null);
16006
16014
  // Detect no-op: result would leave the grabbed element in the same position.
@@ -16023,9 +16031,11 @@ const startDragToCarryCopy = (
16023
16031
  currentReleaseElement = releaseElement;
16024
16032
  // Update drop hint CSS vars.
16025
16033
  // beforeElement = null → insert at end (hint after last item)
16026
- // beforeElement = X → insert before X (hint at top edge of X)
16034
+ // beforeElement = X → insert before X (hint at the edge of X the
16035
+ // flow comes in by)
16027
16036
  const anchorEl = beforeElement || items[items.length - 1];
16028
- const anchorEdge = beforeElement !== null ? "top" : "bottom";
16037
+ const anchorEdge =
16038
+ DROP_HINT_EDGES[flow][beforeElement !== null ? "before" : "after"];
16029
16039
  // Viewport coordinates, straight from the anchor row: the hint is fixed
16030
16040
  // in the page (see its CSS), so there is no container box to be relative
16031
16041
  // to and no scroll offset to add back.
@@ -16043,10 +16053,18 @@ const startDragToCarryCopy = (
16043
16053
  "--drop-target-left",
16044
16054
  `${anchorRect.left}px`,
16045
16055
  );
16056
+ dropHintEl.style.setProperty(
16057
+ "--drop-target-right",
16058
+ `${anchorRect.right}px`,
16059
+ );
16046
16060
  dropHintEl.style.setProperty(
16047
16061
  "--drop-target-width",
16048
16062
  `${anchorRect.width}px`,
16049
16063
  );
16064
+ dropHintEl.style.setProperty(
16065
+ "--drop-target-height",
16066
+ `${anchorRect.height}px`,
16067
+ );
16050
16068
  });
16051
16069
 
16052
16070
  dragGesture.addReleaseCallback(async (gestureInfo) => {
@@ -16307,6 +16325,155 @@ const createDropSurface = () => {
16307
16325
  return div;
16308
16326
  };
16309
16327
 
16328
+ /**
16329
+ * The way the items follow each other, from how the caller said they are laid
16330
+ * out (the `itemFlow` of startDragTo). A row runs the way the text of its list
16331
+ * does: from right to left under `direction: rtl`.
16332
+ */
16333
+ const resolveItemFlow = (itemFlow, containerElement) => {
16334
+ if (itemFlow === undefined) {
16335
+ return undefined;
16336
+ }
16337
+ if (itemFlow === "column") {
16338
+ return "down";
16339
+ }
16340
+ if (itemFlow === "row" || itemFlow === "wrap") {
16341
+ return getComputedStyle(containerElement).direction === "rtl"
16342
+ ? "left"
16343
+ : "right";
16344
+ }
16345
+ return undefined;
16346
+ };
16347
+
16348
+ /**
16349
+ * Which way the items of a list follow each other on screen: "down", or along a
16350
+ * line — a row, a wrapped row, a grid — "right", or "left" when it runs from
16351
+ * right to left. Read from where the items stand rather than declared: the
16352
+ * first item level with the one after it makes the list run sideways, and which
16353
+ * of the two is on the left says which way. Asked of the whole list, not of the
16354
+ * item under the copy: alone on the last line of a wrapped row, an item is level
16355
+ * with nothing and still part of a row.
16356
+ */
16357
+ const readItemFlow = (items) => {
16358
+ let previousRect;
16359
+ for (const item of items) {
16360
+ const rect = item.getBoundingClientRect();
16361
+ // A box that takes no room stands on no line.
16362
+ if (rect.width === 0 && rect.height === 0) {
16363
+ continue;
16364
+ }
16365
+ if (previousRect && areLevel(previousRect, rect)) {
16366
+ return rect.left < previousRect.left ? "left" : "right";
16367
+ }
16368
+ previousRect = rect;
16369
+ }
16370
+ return "down";
16371
+ };
16372
+
16373
+ // The middle of one within the height of the other. Overlapping is not enough:
16374
+ // items stacked with a negative margin (a border shared between two) overlap by
16375
+ // a pixel and are still one under the other.
16376
+ const areLevel = (a, b) => {
16377
+ const aMiddle = a.top + a.height / 2;
16378
+ const bMiddle = b.top + b.height / 2;
16379
+ return (
16380
+ (bMiddle > a.top && bMiddle < a.bottom) ||
16381
+ (aMiddle > b.top && aMiddle < b.bottom)
16382
+ );
16383
+ };
16384
+
16385
+ /**
16386
+ * The item the copy is at, and its `side` in the order of the list: "start" for
16387
+ * before it, "end" for after it.
16388
+ *
16389
+ * Down a list, the item under the copy, read by the copy's leading edge (see
16390
+ * getDropTargetInfo); over no item, above the first one is the start of the
16391
+ * list and below the last one its end.
16392
+ *
16393
+ * Along a row, the copy's middle decides, the way a click puts the caret in a
16394
+ * text: on the nearest line, before the first item whose middle it has not
16395
+ * reached. How high the copy is says which line and nothing more, its size
16396
+ * nothing at all — the leading edge assumes a copy as large as what it passes
16397
+ * over, and items of a row are as wide as their content: read by its edge, one
16398
+ * wider than its neighbour could never be put after it.
16399
+ */
16400
+ const findPlaceInList = (gestureInfo, items, flow) => {
16401
+ const dragElementRect = gestureInfo.elementImpacted.getBoundingClientRect();
16402
+ if (flow !== "down") {
16403
+ return findPlaceOnNearestLine(dragElementRect, items, flow);
16404
+ }
16405
+ const dropTargetInfo = getDropTargetInfo(gestureInfo, items);
16406
+ if (dropTargetInfo) {
16407
+ return {
16408
+ element: dropTargetInfo.element,
16409
+ side: dropTargetInfo.elementSide.y,
16410
+ };
16411
+ }
16412
+ const firstItem = items[0];
16413
+ if (!firstItem) {
16414
+ return null;
16415
+ }
16416
+ const lastItem = items[items.length - 1];
16417
+ if (dragElementRect.bottom < firstItem.getBoundingClientRect().top) {
16418
+ return { element: firstItem, side: "start" };
16419
+ }
16420
+ if (dragElementRect.top > lastItem.getBoundingClientRect().bottom) {
16421
+ return { element: lastItem, side: "end" };
16422
+ }
16423
+ return null;
16424
+ };
16425
+
16426
+ const findPlaceOnNearestLine = (dragElementRect, items, flow) => {
16427
+ const lines = [];
16428
+ let previousRect;
16429
+ for (const item of items) {
16430
+ const rect = item.getBoundingClientRect();
16431
+ // A box that takes no room stands on no line (see readItemFlow).
16432
+ if (rect.width === 0 && rect.height === 0) {
16433
+ continue;
16434
+ }
16435
+ if (!previousRect || !areLevel(previousRect, rect)) {
16436
+ lines.push({ top: rect.top, bottom: rect.bottom, entries: [] });
16437
+ }
16438
+ const line = lines[lines.length - 1];
16439
+ if (rect.top < line.top) {
16440
+ line.top = rect.top;
16441
+ }
16442
+ if (rect.bottom > line.bottom) {
16443
+ line.bottom = rect.bottom;
16444
+ }
16445
+ line.entries.push({ item, middle: rect.left + rect.width / 2 });
16446
+ previousRect = rect;
16447
+ }
16448
+ const dragMiddleY = dragElementRect.top + dragElementRect.height / 2;
16449
+ let nearestLine;
16450
+ let nearestDistance;
16451
+ for (const line of lines) {
16452
+ const distance =
16453
+ dragMiddleY < line.top
16454
+ ? line.top - dragMiddleY
16455
+ : dragMiddleY > line.bottom
16456
+ ? dragMiddleY - line.bottom
16457
+ : 0;
16458
+ if (nearestLine === undefined || distance < nearestDistance) {
16459
+ nearestLine = line;
16460
+ nearestDistance = distance;
16461
+ }
16462
+ }
16463
+ if (!nearestLine) {
16464
+ return null;
16465
+ }
16466
+ const dragMiddleX = dragElementRect.left + dragElementRect.width / 2;
16467
+ for (const { item, middle } of nearestLine.entries) {
16468
+ const notReached =
16469
+ flow === "right" ? middle > dragMiddleX : middle < dragMiddleX;
16470
+ if (notReached) {
16471
+ return { element: item, side: "start" };
16472
+ }
16473
+ }
16474
+ return { element: nearestLine.entries.at(-1).item, side: "end" };
16475
+ };
16476
+
16310
16477
  /**
16311
16478
  * The copy leaves the screen the way it was thrown, and the caller says what that
16312
16479
  * meant. Resolves true when it is really gone.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.92",
3
+ "version": "0.17.94",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {