@jsenv/dom 0.17.81 → 0.17.83

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 +272 -37
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -10147,6 +10147,11 @@ const dragSourceThatStoodDown = (pressEvent) => {
10147
10147
  return pressEvent[STOOD_DOWN] || null;
10148
10148
  };
10149
10149
 
10150
+ // How far (px) a press travels before it means drag. Also how far a carry that
10151
+ // was handed over must go before it has gone anywhere (see handedOver in
10152
+ // drag_to.js): that is the same question, asked of the hand holding it.
10153
+ const DRAG_INTENT_THRESHOLD = 5;
10154
+
10150
10155
  /**
10151
10156
  * Waits for the user to mean it, then starts a drag gesture.
10152
10157
  *
@@ -10183,12 +10188,16 @@ const dragSourceThatStoodDown = (pressEvent) => {
10183
10188
  * drag_gesture.js). `"manual"` leaves the press to the browser until the
10184
10189
  * caller's own gesture says otherwise. A long press waits without refusing
10185
10190
  * anything either way (see the stylesheet above).
10191
+ * @param {boolean} [options.handedOver=false]
10192
+ * The press was held on something else, which handed it to this drag (see
10193
+ * `handedOver` in drag_to.js): the hold that did so established the intent,
10194
+ * so the gesture starts at once, as it does from a dedicated handle.
10186
10195
  */
10187
10196
  const dragAfterIntent = (
10188
10197
  grabEvent,
10189
10198
  dragGestureInitializer,
10190
10199
  {
10191
- threshold = 5,
10200
+ threshold = DRAG_INTENT_THRESHOLD,
10192
10201
  longPress = "if-touch",
10193
10202
  longPressDelay = 400,
10194
10203
  longPressSlop = 8,
@@ -10196,6 +10205,7 @@ const dragAfterIntent = (
10196
10205
  onPressCancel,
10197
10206
  onPress,
10198
10207
  selection = "auto",
10208
+ handedOver = false,
10199
10209
  } = {},
10200
10210
  ) => {
10201
10211
  if (!isPrimaryButtonEvent(grabEvent)) {
@@ -10204,7 +10214,7 @@ const dragAfterIntent = (
10204
10214
  const target = grabEvent.target;
10205
10215
  const isDedicatedHandle =
10206
10216
  target.closest && target.closest("[data-drag-handle]");
10207
- if (isDedicatedHandle) {
10217
+ if (isDedicatedHandle || handedOver) {
10208
10218
  startDragGesture(dragGestureInitializer);
10209
10219
  return;
10210
10220
  }
@@ -10836,6 +10846,82 @@ const getDragCoordinates = (
10836
10846
  return [leftRelativeToScrollContainer, topRelativeToScrollContainer];
10837
10847
  };
10838
10848
 
10849
+ /**
10850
+ * Which presses are still down, and where each one is now.
10851
+ *
10852
+ * A press is read by whoever it landed on, from its `pointerdown` to its
10853
+ * `pointerup`. Some are handed over halfway: a hold opens a popup, and
10854
+ * something inside it takes the press the finger is still making (see
10855
+ * `handedOver` in drag_to.js). Whoever receives it was not listening when it
10856
+ * began, so it cannot know whether the pointer has let go since, nor where it
10857
+ * is now — the `pointerdown` it is handed says where the finger WAS. Listened
10858
+ * for from the start instead, on the window in capture so that nothing lower
10859
+ * can hide a press from it.
10860
+ *
10861
+ * Keyed by the `pointerdown` itself and not by the pointer: the same finger
10862
+ * lifted and put down again is another press, and a press handed over late
10863
+ * must not be answered by the next one.
10864
+ */
10865
+
10866
+ const pressDownByPointerId = new Map();
10867
+
10868
+ /**
10869
+ * Where the pointer of `pressEvent` is now, while that press is still down.
10870
+ *
10871
+ * @param {PointerEvent} pressEvent A `pointerdown`.
10872
+ * @returns {{ clientX: number, clientY: number } | null} `null` once that press
10873
+ * has ended — let go of, or cancelled by the browser.
10874
+ */
10875
+ const readPressDown = (pressEvent) => {
10876
+ const pressDown = pressDownByPointerId.get(pressEvent.pointerId);
10877
+ if (!pressDown || pressDown.pressEvent !== pressEvent) {
10878
+ return null;
10879
+ }
10880
+ return { clientX: pressDown.clientX, clientY: pressDown.clientY };
10881
+ };
10882
+
10883
+ const onPointerMove = (pointerMoveEvent) => {
10884
+ const pressDown = pressDownByPointerId.get(pointerMoveEvent.pointerId);
10885
+ if (!pressDown) {
10886
+ return;
10887
+ }
10888
+ pressDown.clientX = pointerMoveEvent.clientX;
10889
+ pressDown.clientY = pointerMoveEvent.clientY;
10890
+ };
10891
+ const onPointerEnd = (pointerEndEvent) => {
10892
+ pressDownByPointerId.delete(pointerEndEvent.pointerId);
10893
+ if (pressDownByPointerId.size === 0) {
10894
+ // A mouse moves all the time without being pressed: nothing is read from
10895
+ // it between presses.
10896
+ window.removeEventListener("pointermove", onPointerMove, true);
10897
+ }
10898
+ };
10899
+ window.addEventListener(
10900
+ "pointerdown",
10901
+ (pointerDownEvent) => {
10902
+ if (pressDownByPointerId.size === 0) {
10903
+ window.addEventListener("pointermove", onPointerMove, {
10904
+ capture: true,
10905
+ passive: true,
10906
+ });
10907
+ }
10908
+ pressDownByPointerId.set(pointerDownEvent.pointerId, {
10909
+ pressEvent: pointerDownEvent,
10910
+ clientX: pointerDownEvent.clientX,
10911
+ clientY: pointerDownEvent.clientY,
10912
+ });
10913
+ },
10914
+ { capture: true, passive: true },
10915
+ );
10916
+ window.addEventListener("pointerup", onPointerEnd, {
10917
+ capture: true,
10918
+ passive: true,
10919
+ });
10920
+ window.addEventListener("pointercancel", onPointerEnd, {
10921
+ capture: true,
10922
+ passive: true,
10923
+ });
10924
+
10839
10925
  installImportMetaCssBuild(import.meta);
10840
10926
 
10841
10927
  const css$4 = /* css */ `.navi_constraint_feedback_line {
@@ -14324,6 +14410,16 @@ const css$1 = /* css */ `.navi_drop_hint {
14324
14410
  opacity: 0;
14325
14411
  }
14326
14412
 
14413
+ [navi-drag-clone-wrapper][data-reaching] {
14414
+ transition: translate .2s ease-out;
14415
+ }
14416
+
14417
+ @starting-style {
14418
+ [navi-drag-clone-wrapper][data-reaching] {
14419
+ translate: var(--clone-reach-from);
14420
+ }
14421
+ }
14422
+
14327
14423
  [navi-drag-clone] {
14328
14424
  box-shadow: var(--drag-clone-shadow, 0 12px 28px #00000038);
14329
14425
  transform: scale(var(--drag-clone-scale, 1.03));
@@ -14740,6 +14836,11 @@ const createDragToMoveGestureController = ({
14740
14836
  element,
14741
14837
  referenceElement,
14742
14838
  elementToMove,
14839
+ // How far from `element` what moves starts, when it does not start over it
14840
+ // (a copy brought to a hand that pressed elsewhere, see handedOver). The
14841
+ // layout is then the box the hand holds, so constraints and auto-scroll
14842
+ // read what is seen; the translate is unaffected, it counts from the grab.
14843
+ grabOffset,
14743
14844
  event,
14744
14845
  ...rest
14745
14846
  } = {}) => {
@@ -14752,8 +14853,12 @@ const createDragToMoveGestureController = ({
14752
14853
  const dragGesture = grab({
14753
14854
  element,
14754
14855
  scrollContainer,
14755
- layoutScrollableLeft: elementScrollableLeft,
14756
- layoutScrollableTop: elementScrollableTop,
14856
+ layoutScrollableLeft: grabOffset
14857
+ ? elementScrollableLeft + grabOffset.x
14858
+ : elementScrollableLeft,
14859
+ layoutScrollableTop: grabOffset
14860
+ ? elementScrollableTop + grabOffset.y
14861
+ : elementScrollableTop,
14757
14862
  event,
14758
14863
  ...rest,
14759
14864
  });
@@ -14847,6 +14952,27 @@ const createDragToMoveGestureController = ({
14847
14952
  * for: one without the other is moving the thing while hesitating, and nothing is
14848
14953
  * thrown away on a hesitation. A throw is judged before any landing; a release
14849
14954
  * with no place under it is `leave`'s, not a toss — there is no speed to it.
14955
+ * @param {boolean} [options.handedOver=false]
14956
+ * `event` is a press held on something ELSE, handed to this element: a hold
14957
+ * opened a popup, and what the popup holds is to be carried by the finger
14958
+ * still down (see HANDED OVER below). Nothing happens when that press has
14959
+ * ended in the meantime.
14960
+ *
14961
+ * HANDED OVER. The press did not land on the element, and the element may not
14962
+ * have existed when it did, so the three things a press on it would have said
14963
+ * are said otherwise. Where it landed is none of the element's business (no
14964
+ * opted-out area is looked for). Whether the hand means it was proved by the
14965
+ * hold that handed it over, so the carry starts at once. And where the hand is
14966
+ * has nothing to do with where the element stands, so the copy is brought to
14967
+ * the hand — centred on the finger, flying there from the element's box —
14968
+ * rather than following it from a distance. Where it was brought then stands
14969
+ * for the element's own place: over whatever the finger happened to be on, so
14970
+ * nothing counts as under it until the hand has taken it further than
14971
+ * `threshold`, and a release before that is a hand that changed its mind. The
14972
+ * pointer is taken on the element, away from what the finger first touched.
14973
+ * Only a copy can be brought
14974
+ * like that: `move` carries the element itself, which would leave its place
14975
+ * just by being handed a press, and is not started.
14850
14976
  *
14851
14977
  * Everything else is forwarded to `createDragToMoveGestureController`
14852
14978
  * (`areaConstraint`, `autoScrollAreaPadding`, `direction`…) and to `dragAfterIntent`
@@ -14868,8 +14994,9 @@ const startDragTo = (
14868
14994
  { draggedElement = event.currentTarget, ...options } = {},
14869
14995
  ) => {
14870
14996
  // An area that opted out of dragging (a text one wants to select, a control that
14871
- // owns the gesture, a callout): the press there is none of our business.
14872
- if (isPressIgnored(event.target, draggedElement)) {
14997
+ // owns the gesture, a callout): the press there is none of our business. A
14998
+ // press handed over landed outside the element by construction.
14999
+ if (!options.handedOver && isPressIgnored(event.target, draggedElement)) {
14873
15000
  return undefined;
14874
15001
  }
14875
15002
  // A secondary button (right click and friends) is a context menu, not a grab.
@@ -14896,6 +15023,9 @@ const startDragTo = (
14896
15023
  ...options,
14897
15024
  });
14898
15025
  }
15026
+ if (options.handedOver) {
15027
+ return undefined;
15028
+ }
14899
15029
  return startDragToMoveElement(event, {
14900
15030
  draggedElement,
14901
15031
  canMove,
@@ -15195,6 +15325,10 @@ const resolveDropMeaning = ({
15195
15325
  canLeave,
15196
15326
  tossDistance = TOSS_DISTANCE_TO_COMMIT,
15197
15327
  tossSpeed = TOSS_SPEED_TO_COMMIT,
15328
+ // How far it may have been taken and still have been taken nowhere: not at
15329
+ // all for a carry that starts over its own place, the drag threshold for one
15330
+ // brought to a hand (see handedOver).
15331
+ stillWithin = 0,
15198
15332
  }) => {
15199
15333
  if (gestureInfo.cancelled) {
15200
15334
  // Nobody let go of anything: the gesture was taken away mid-air (the
@@ -15220,7 +15354,7 @@ const resolveDropMeaning = ({
15220
15354
  // It has to have gone somewhere to be put anywhere, or away from anything:
15221
15355
  // picked up and put straight back down is a hand that changed its mind.
15222
15356
  const { xDelta, yDelta } = gestureInfo.layout;
15223
- if (!xDelta && !yDelta) {
15357
+ if (Math.hypot(xDelta, yDelta) <= stillWithin) {
15224
15358
  return "cancel";
15225
15359
  }
15226
15360
  if (canLeave && releasedOutside) {
@@ -15278,30 +15412,44 @@ const startDragToCarryCopy = (
15278
15412
  direction = canLand || canLeave
15279
15413
  ? { x: true, y: true }
15280
15414
  : { x: false, y: true },
15281
- threshold,
15415
+ // Defaulted here rather than left to dragAfterIntent: a carry handed over
15416
+ // reads it too (see stillWithin).
15417
+ threshold = DRAG_INTENT_THRESHOLD,
15282
15418
  longPress,
15283
15419
  longPressDelay,
15284
15420
  longPressSlop,
15285
15421
  onPressStart,
15286
15422
  onPressCancel,
15287
15423
  onPress,
15424
+ handedOver,
15288
15425
  ...options
15289
15426
  },
15290
15427
  ) => {
15291
15428
  // An area that opted out of dragging (a text one wants to select, a control
15292
15429
  // that owns the gesture, a callout): the press there is none of our business.
15293
- if (isPressIgnored(event.target, draggedElement)) {
15430
+ if (!handedOver && isPressIgnored(event.target, draggedElement)) {
15294
15431
  return undefined;
15295
15432
  }
15296
15433
  // A secondary button (right click and friends) is a context menu, not a grab.
15297
15434
  if (!isPrimaryButtonEvent(event)) {
15298
15435
  return undefined;
15299
15436
  }
15437
+ // Where the hand is, for a press handed over: its `pointerdown` says where it
15438
+ // WAS, before the hold and whatever the hold opened. Let go of meanwhile, it
15439
+ // is not picked up after the fact.
15440
+ const handedPointer = handedOver ? readPressDown(event) : null;
15441
+ if (handedOver && !handedPointer) {
15442
+ return undefined;
15443
+ }
15300
15444
  // One press, one carry — and the same carry over again when the hand comes back
15301
15445
  // for the copy while it is still flying home (see settleCloneBack). Nothing is
15302
15446
  // made twice in that case: it is the same copy, taken in hand again where it
15303
- // had got to.
15304
- const startCarry = (pointerEvent, cloneWrapperCaught, onCarryStart) => {
15447
+ // had got to — by a press ON it, so never a press handed over.
15448
+ const startCarry = (
15449
+ pointerEvent,
15450
+ { cloneWrapperCaught, onCarryStart } = {},
15451
+ ) => {
15452
+ const handedTo = cloneWrapperCaught ? null : handedPointer;
15305
15453
  pointerEvent.preventDefault();
15306
15454
  return dragAfterIntent(
15307
15455
  pointerEvent,
@@ -15310,8 +15458,21 @@ const startDragToCarryCopy = (
15310
15458
  // one moment both ways in (a finger held still, a mouse travelled)
15311
15459
  // agree on.
15312
15460
  onCarryStart?.();
15461
+ // From the element's centre to the finger: the copy is brought that far,
15462
+ // since the hand that pressed elsewhere has nothing to follow it from.
15463
+ const reach = handedTo
15464
+ ? measureReach(draggedElement, handedTo)
15465
+ : undefined;
15466
+ // …and has been taken nowhere until it is further from there than a
15467
+ // press travels to mean drag.
15468
+ const stillWithin = handedTo ? threshold : 0;
15469
+ const hasNotLeftHand = (gestureInfo) =>
15470
+ Boolean(handedTo) &&
15471
+ Math.hypot(gestureInfo.layout.xDelta, gestureInfo.layout.yDelta) <=
15472
+ stillWithin;
15313
15473
  const cloneWrapper =
15314
- cloneWrapperCaught || createDragClone(draggedElement, pointerEvent);
15474
+ cloneWrapperCaught ||
15475
+ createDragClone(draggedElement, handedTo || pointerEvent, reach);
15315
15476
  if (cloneWrapperCaught) {
15316
15477
  liftDragClone(cloneWrapperCaught, pointerEvent);
15317
15478
  }
@@ -15323,10 +15484,26 @@ const startDragToCarryCopy = (
15323
15484
  areaConstraint,
15324
15485
  ...options,
15325
15486
  });
15326
- const dragGesture = gestureController.grabViaPointer(pointerEvent, {
15327
- element: draggedElement,
15328
- elementToMove: cloneWrapper,
15329
- });
15487
+ const dragGesture = gestureController.grabViaPointer(
15488
+ pointerEvent,
15489
+ handedTo
15490
+ ? {
15491
+ element: draggedElement,
15492
+ elementToMove: cloneWrapper,
15493
+ // Counted from where the finger is now, so a release that
15494
+ // never moved it is still a hand that changed its mind.
15495
+ grabX: handedTo.clientX,
15496
+ grabY: handedTo.clientY,
15497
+ grabOffset: reach,
15498
+ // Off what the finger first touched, which the popup now
15499
+ // covers: the pointer is the carry's from here.
15500
+ pointerCaptureElement: draggedElement,
15501
+ }
15502
+ : {
15503
+ element: draggedElement,
15504
+ elementToMove: cloneWrapper,
15505
+ },
15506
+ );
15330
15507
  // getDropTargetInfo uses gestureInfo.elementImpacted to compute the dragged rect.
15331
15508
  // Point it at the clone so drop detection tracks the clone's current position.
15332
15509
  dragGesture.gestureInfo.elementImpacted = cloneWrapper;
@@ -15385,6 +15562,16 @@ const startDragToCarryCopy = (
15385
15562
  if (!dropHintEl) {
15386
15563
  return;
15387
15564
  }
15565
+ // Brought to the hand, the copy starts over whatever the finger is on,
15566
+ // and a place found there is one it was never taken to — the release
15567
+ // frame included, which is how a hold let go of without moving would
15568
+ // land. Around where it was brought is its own place, as the element's
15569
+ // box is for a carry that starts there: nothing is under it yet.
15570
+ if (hasNotLeftHand(gestureInfo)) {
15571
+ clearDropHint();
15572
+ gestureInfo.dropTargetInfo = null;
15573
+ return;
15574
+ }
15388
15575
  const allItems = [];
15389
15576
  const items = [];
15390
15577
  for (const el of containerElement.querySelectorAll(itemSelector)) {
@@ -15500,6 +15687,10 @@ const startDragToCarryCopy = (
15500
15687
  dragGesture.addReleaseCallback(async (gestureInfo) => {
15501
15688
  clearDropHintDOM();
15502
15689
  dropHintEl?.remove();
15690
+ // Let go of on its way to the hand: it is where the hand is, and from
15691
+ // there it goes wherever the answer sends it — on a translate of its
15692
+ // own, which the flight to the hand must not be holding.
15693
+ cloneWrapper.removeAttribute("data-reaching");
15503
15694
 
15504
15695
  // What THIS release means, from what the element said it can answer. A
15505
15696
  // throw is asked about first: it is the more insistent of the two, and a
@@ -15527,6 +15718,7 @@ const startDragToCarryCopy = (
15527
15718
  canLeave,
15528
15719
  tossDistance,
15529
15720
  tossSpeed,
15721
+ stillWithin,
15530
15722
  });
15531
15723
  // The hand has let go, and what that means is already known — the
15532
15724
  // answer to it has not run yet. Told, not asked: nothing here waits on
@@ -15544,7 +15736,10 @@ const startDragToCarryCopy = (
15544
15736
  const copyLetGoOf = letCopyBeCaught(
15545
15737
  cloneWrapper,
15546
15738
  (pointerDownEvent, whenCarried) =>
15547
- startCarry(pointerDownEvent, cloneWrapper, whenCarried),
15739
+ startCarry(pointerDownEvent, {
15740
+ cloneWrapperCaught: cloneWrapper,
15741
+ onCarryStart: whenCarried,
15742
+ }),
15548
15743
  );
15549
15744
 
15550
15745
  // The copy stops where the hand left it, and the answer is given a way to
@@ -15648,6 +15843,7 @@ const startDragToCarryCopy = (
15648
15843
  onPressStart,
15649
15844
  onPressCancel,
15650
15845
  onPress,
15846
+ handedOver: Boolean(handedTo),
15651
15847
  },
15652
15848
  );
15653
15849
  };
@@ -15951,9 +16147,23 @@ const liftDragClone = (cloneWrapper, pointerEvent) => {
15951
16147
  cloneWrapper.firstElementChild.setAttribute("navi-drag-clone", "");
15952
16148
  };
15953
16149
 
16150
+ // From the centre of the element to the pointer, for a copy brought to a hand
16151
+ // that pressed elsewhere (see handedOver): centred rather than held at the point
16152
+ // the finger had on what it pressed, since that was another box.
16153
+ const measureReach = (element, pointer) => {
16154
+ const rect = element.getBoundingClientRect();
16155
+ return {
16156
+ x: pointer.clientX - (rect.left + rect.width / 2),
16157
+ y: pointer.clientY - (rect.top + rect.height / 2),
16158
+ };
16159
+ };
16160
+
15954
16161
  let dragCloneCount = 0;
15955
- const createDragClone = (element, pointerEvent) => {
16162
+ const createDragClone = (element, grabPoint, reach) => {
15956
16163
  const rect = element.getBoundingClientRect();
16164
+ // Over the element, or shifted to where the hand is (see measureReach).
16165
+ const left = reach ? rect.left + reach.x : rect.left;
16166
+ const top = reach ? rect.top + reach.y : rect.top;
15957
16167
 
15958
16168
  const wrapper = document.createElement("div");
15959
16169
  wrapper.setAttribute("navi-drag-clone-wrapper", "");
@@ -15965,13 +16175,26 @@ const createDragClone = (element, pointerEvent) => {
15965
16175
  // Manual: it is opened and closed with the drag, and must survive an Escape
15966
16176
  // or a click elsewhere (light dismiss would take it away mid-gesture).
15967
16177
  wrapper.setAttribute("popover", "manual");
15968
- setCloneViewportRect(wrapper, element);
15969
- // Grab point within the element — used as transform-origin so the
16178
+ wrapper.style.setProperty("--clone-top", `${top}px`);
16179
+ wrapper.style.setProperty("--clone-left", `${left}px`);
16180
+ wrapper.style.setProperty("--clone-width", `${rect.width}px`);
16181
+ wrapper.style.setProperty("--clone-height", `${rect.height}px`);
16182
+ if (reach) {
16183
+ // It flies there from the element's box, so the eye follows what was
16184
+ // picked up rather than seeing it appear under the finger (see the
16185
+ // stylesheet).
16186
+ wrapper.style.setProperty(
16187
+ "--clone-reach-from",
16188
+ `${-reach.x}px ${-reach.y}px`,
16189
+ );
16190
+ wrapper.setAttribute("data-reaching", "");
16191
+ }
16192
+ // Grab point within the copy — used as transform-origin so the
15970
16193
  // scale expands from where the user clicked, not the element center.
15971
- // These offsets are element-relative so viewport coords are correct here.
16194
+ // These offsets are copy-relative so viewport coords are correct here.
15972
16195
  wrapper.style.setProperty(
15973
16196
  "--drag-origin",
15974
- `${pointerEvent.clientX - rect.left}px ${pointerEvent.clientY - rect.top}px`,
16197
+ `${grabPoint.clientX - left}px ${grabPoint.clientY - top}px`,
15975
16198
  );
15976
16199
  const elementClone = element.cloneNode(true);
15977
16200
  // A deep copy copies the ids too, and two elements answering to one id is a
@@ -21996,33 +22219,45 @@ const measureWidestChildRow = (el) => {
21996
22219
  const borderLeft = parseFloat(containerStyle.borderLeftWidth);
21997
22220
  const borderRight = parseFloat(containerStyle.borderRightWidth);
21998
22221
 
21999
- // Group children by row using their top position
22000
- const rowsByTop = new Map();
22222
+ // Rows are found by vertical overlap, not by top: in an "align-items: center"
22223
+ // row, a child taller than its neighbours (a 1px border is enough) starts
22224
+ // higher than them and would open a row of its own.
22225
+ const rows = [];
22001
22226
  for (const child of children) {
22002
22227
  const rect = child.getBoundingClientRect();
22003
22228
  if (rect.width === 0 && rect.height === 0) {
22004
22229
  continue;
22005
22230
  }
22006
- const top = Math.round(rect.top);
22007
- const existing = rowsByTop.get(top);
22008
- if (existing === undefined) {
22009
- rowsByTop.set(top, { left: rect.left, right: rect.right });
22010
- } else {
22011
- if (rect.left < existing.left) {
22012
- existing.left = rect.left;
22013
- }
22014
- if (rect.right > existing.right) {
22015
- existing.right = rect.right;
22016
- }
22231
+ const row = rows.find((candidate) => sharesLine(candidate, rect));
22232
+ if (row === undefined) {
22233
+ rows.push({
22234
+ top: rect.top,
22235
+ bottom: rect.bottom,
22236
+ left: rect.left,
22237
+ right: rect.right,
22238
+ });
22239
+ continue;
22240
+ }
22241
+ if (rect.top < row.top) {
22242
+ row.top = rect.top;
22243
+ }
22244
+ if (rect.bottom > row.bottom) {
22245
+ row.bottom = rect.bottom;
22246
+ }
22247
+ if (rect.left < row.left) {
22248
+ row.left = rect.left;
22249
+ }
22250
+ if (rect.right > row.right) {
22251
+ row.right = rect.right;
22017
22252
  }
22018
22253
  }
22019
22254
 
22020
- if (rowsByTop.size <= 1) {
22255
+ if (rows.length <= 1) {
22021
22256
  return null;
22022
22257
  }
22023
22258
 
22024
22259
  let widestRowWidth = 0;
22025
- for (const { left, right } of rowsByTop.values()) {
22260
+ for (const { left, right } of rows) {
22026
22261
  const rowWidth = right - left;
22027
22262
  if (rowWidth > widestRowWidth) {
22028
22263
  widestRowWidth = rowWidth;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.81",
3
+ "version": "0.17.83",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {