@jsenv/dom 0.17.95 → 0.17.96

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 +165 -41
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -14846,7 +14846,8 @@ const DRAG_IGNORED_SELECTOR =
14846
14846
  * Creates a gesture controller that moves elements via drag.
14847
14847
  *
14848
14848
  * Wraps `createDragGestureController` and adds:
14849
- * - Element translation via CSS transform (translate only; other existing transforms are preserved)
14849
+ * - Element translation, on the `translate` property (or inside `transform`,
14850
+ * see `positionProperty`), added to the translate the element already had
14850
14851
  * - Auto-scroll while dragging near scroll-container edges
14851
14852
  * - Constraints (area boundaries, obstacle elements)
14852
14853
  *
@@ -14859,10 +14860,11 @@ const DRAG_IGNORED_SELECTOR =
14859
14860
  * If omitted, `element` is translated. The translate is read from `dragStyleController`
14860
14861
  * at grab time so any pre-existing translate is accumulated rather than reset.
14861
14862
  *
14862
- * A `transform` already on the moved element (rotate, scale…) is preserved and does
14863
- * not disturb the movement. `rotate` and `scale` set as individual CSS properties do:
14864
- * they apply outside `transform`, where nothing the gesture writes can reach them —
14865
- * put those on a child element instead (a warning says so in dev).
14863
+ * Carried on `translate`, the element keeps everything else it is drawn with:
14864
+ * the element's matrix is translate, then rotate, then scale, then `transform`,
14865
+ * so the hand's translate applies outside a rotation or a scale of its own,
14866
+ * whether written as properties or inside `transform`, and lands exactly where
14867
+ * the hand is.
14866
14868
  *
14867
14869
  * @param {object} [options]
14868
14870
  * @param {boolean} [options.stickyFrontiers=true]
@@ -14885,9 +14887,15 @@ const DRAG_IGNORED_SELECTOR =
14885
14887
  * Renders a visual line when the pointer deviates from the element due to constraints.
14886
14888
  * @param {boolean} [options.showDebugMarkers=false]
14887
14889
  * Renders debug markers for constraint regions.
14890
+ * @param {"translate"|"transform"} [options.positionProperty="translate"]
14891
+ * Where the hand's translate is written. `"transform"` puts it first in the
14892
+ * element's transform list, for a box whose `translate` property is already
14893
+ * spoken for — the copy `startDragTo` carries, which travels on it to the
14894
+ * hand and home. Inside `transform` it lands within a `rotate` or a `scale`
14895
+ * property and comes out rotated and scaled (a warning says so in dev).
14888
14896
  * @param {"translate"|"manual"} [options.dragPositionEffect="translate"]
14889
14897
  * Who puts the element where the hand is, while the hand is moving.
14890
- * - `"translate"`: this controller does, with a transform (default).
14898
+ * - `"translate"`: this controller does (default), on `positionProperty`.
14891
14899
  * - `"manual"`: nobody — the element stays where it was drawn, and whoever is
14892
14900
  * told the gesture draws it. For a thing whose position is state.
14893
14901
  * @param {"commit"|"cancel"|"cancel-animated"|"manual"} [options.releasePositionEffect="commit"]
@@ -14917,6 +14925,7 @@ const createDragToMoveGestureController = ({
14917
14925
  obstacleAttributeName = "data-drag-obstacle",
14918
14926
  showConstraintFeedbackLine = false,
14919
14927
  showDebugMarkers = false,
14928
+ positionProperty = "translate",
14920
14929
  dragPositionEffect = "translate",
14921
14930
  releasePositionEffect = "commit",
14922
14931
  cancelAnimationDuration = 200,
@@ -14938,10 +14947,21 @@ const createDragToMoveGestureController = ({
14938
14947
  // rather than resetting it to zero on the first drag event.
14939
14948
  // Nothing is written on the element on "manual" (the caller draws it where
14940
14949
  // it goes), so there is nothing to accumulate on and nothing to read.
14950
+ const writesPosition = dragPositionEffect !== "manual";
14951
+ const carriesOnTranslate = positionProperty === "translate";
14952
+ const translateAtGrab =
14953
+ writesPosition && carriesOnTranslate
14954
+ ? readTranslateParts(
14955
+ dragStyleController.getUnderlyingValue(
14956
+ elementImpacted,
14957
+ "translate",
14958
+ ),
14959
+ )
14960
+ : null;
14941
14961
  const transformAtGrabOrNull =
14942
- dragPositionEffect === "manual"
14943
- ? null
14944
- : dragStyleController.getUnderlyingValue(elementImpacted, "transform");
14962
+ writesPosition && !carriesOnTranslate
14963
+ ? dragStyleController.getUnderlyingValue(elementImpacted, "transform")
14964
+ : null;
14945
14965
  // A transform that cannot be read as named parts (a complex 3D matrix)
14946
14966
  // comes back undefined: the drag then starts from a zero baseline.
14947
14967
  const transformAtGrab = transformAtGrabOrNull || {};
@@ -14951,24 +14971,28 @@ const createDragToMoveGestureController = ({
14951
14971
  const cancelPosition = () => {
14952
14972
  dragStyleController.clear(elementImpacted);
14953
14973
  };
14954
- // Reading the transform on either side of the clear is what lets this work
14974
+ // Reading the property on either side of the clear is what lets this work
14955
14975
  // without knowing anything about the element: how it looked while held and
14956
- // how it looks once let go are both just computed transforms, and the
14976
+ // how it looks once let go are both just computed values, and the
14957
14977
  // animation has only to bridge the two.
14958
14978
  const cancelPositionAnimated = ({
14959
14979
  duration = cancelAnimationDuration,
14960
14980
  easing = cancelAnimationEasing,
14961
14981
  } = {}) => {
14962
- const transformWhileHeld = getComputedStyle(elementImpacted).transform;
14982
+ const valueWhileHeld =
14983
+ getComputedStyle(elementImpacted)[positionProperty];
14963
14984
  cancelPosition();
14964
- const transformAtRest = getComputedStyle(elementImpacted).transform;
14965
- if (transformWhileHeld === transformAtRest) {
14985
+ const valueAtRest = getComputedStyle(elementImpacted)[positionProperty];
14986
+ if (valueWhileHeld === valueAtRest) {
14966
14987
  return null;
14967
14988
  }
14968
- // No fill: the element already sits at its resting transform, the
14989
+ // No fill: the element already sits at its resting position, the
14969
14990
  // animation only replays the way back to it.
14970
14991
  return elementImpacted.animate(
14971
- [{ transform: transformWhileHeld }, { transform: transformAtRest }],
14992
+ [
14993
+ { [positionProperty]: valueWhileHeld },
14994
+ { [positionProperty]: valueAtRest },
14995
+ ],
14972
14996
  { duration, easing },
14973
14997
  );
14974
14998
  };
@@ -15178,6 +15202,19 @@ const createDragToMoveGestureController = ({
15178
15202
  scrollableLeft,
15179
15203
  scrollableTop,
15180
15204
  );
15205
+ if (carriesOnTranslate) {
15206
+ const { x, y, z } = translateAtGrab;
15207
+ const xMoved = direction.x
15208
+ ? addPixels(x, positionedLeft - dragGesture.gestureInfo.leftAtGrab)
15209
+ : x;
15210
+ const yMoved = direction.y
15211
+ ? addPixels(y, positionedTop - dragGesture.gestureInfo.topAtGrab)
15212
+ : y;
15213
+ dragStyleController.set(elementImpacted, {
15214
+ translate: z ? `${xMoved} ${yMoved} ${z}` : `${xMoved} ${yMoved}`,
15215
+ });
15216
+ break move;
15217
+ }
15181
15218
  // Build the transform to apply, preserving any transforms that were
15182
15219
  // already on the element before the grab (e.g. rotate from another
15183
15220
  // controller), and accumulating from the pre-grab translate baseline.
@@ -15260,6 +15297,47 @@ const createDragToMoveGestureController = ({
15260
15297
  return dragGestureController;
15261
15298
  };
15262
15299
 
15300
+ // The computed `translate` an element has of its own ("none", "10px",
15301
+ // "10px 20px", "50% 0px 4px"), as the three parts the hand's movement is added
15302
+ // to.
15303
+ const readTranslateParts = (translate) => {
15304
+ if (!translate || translate === "none") {
15305
+ return { x: "0px", y: "0px", z: "" };
15306
+ }
15307
+ // Split on the spaces between the parts, not on those inside a calc().
15308
+ const parts = [];
15309
+ let part = "";
15310
+ let depth = 0;
15311
+ for (const char of translate) {
15312
+ if (char === " " && depth === 0) {
15313
+ if (part) {
15314
+ parts.push(part);
15315
+ }
15316
+ part = "";
15317
+ continue;
15318
+ }
15319
+ if (char === "(") {
15320
+ depth++;
15321
+ } else if (char === ")") {
15322
+ depth--;
15323
+ }
15324
+ part += char;
15325
+ }
15326
+ if (part) {
15327
+ parts.push(part);
15328
+ }
15329
+ const [x = "0px", y = "0px", z = ""] = parts;
15330
+ return { x, y, z };
15331
+ };
15332
+ // A movement in pixels added to a length: a sum when it is in pixels too, a
15333
+ // calc() otherwise — a percentage is only resolved by the browser.
15334
+ const addPixels = (length, pixels) => {
15335
+ if (/^-?[\d.]+(?:e-?\d+)?px$/.test(length)) {
15336
+ return `${parseFloat(length) + pixels}px`;
15337
+ }
15338
+ return `calc(${length} + ${pixels}px)`;
15339
+ };
15340
+
15263
15341
  /**
15264
15342
  * Starts a drag, for one or more of the outcomes listed.
15265
15343
  *
@@ -15298,6 +15376,13 @@ const createDragToMoveGestureController = ({
15298
15376
  * reject. Beside a copy, the copy fades where it was let go of and comes back
15299
15377
  * if the promise rejects. Picked up and put straight back down is a cancel:
15300
15378
  * it has to have gone somewhere to be away from anything.
15379
+ * @param {(detail: {outcome: "move"|"reorder"|"land"|"toss"|"leave"|null, refused: boolean, error?: any}) => void} [options.onCarryEnd]
15380
+ * Whatever was carried is at rest: the copy gone and the original shown again,
15381
+ * or the element settled where it stays — after the answer and the journey
15382
+ * home a refusal takes. `refused` says the answer rejected, and `error` is what
15383
+ * it rejected with, so it can be said on the element now that it stands where
15384
+ * it was. Not told for a copy taken back in hand on its way home: that carry
15385
+ * goes on, and its own end is told. Told, not asked.
15301
15386
  * @param {(detail: {gestureInfo: object, x: number, y: number, outcome: "move"|"reorder"|"land"|"toss"|"leave"|null}) => void} [options.onRelease]
15302
15387
  * The hand let go — the mirror of `onDragStart`, and the one thing that is told
15303
15388
  * whatever the release meant, a cancelled gesture included. `outcome` is what
@@ -15547,6 +15632,7 @@ const startDragToMoveElement = (
15547
15632
  onMove,
15548
15633
  onLeave,
15549
15634
  onRelease,
15635
+ onCarryEnd,
15550
15636
  outsideOf,
15551
15637
  // A thing that can be let go of outside has to be able to get there. Same
15552
15638
  // reason and same shape as the default of the copy path below.
@@ -15607,7 +15693,8 @@ const startDragToMoveElement = (
15607
15693
  if (dropMeans === "cancel") {
15608
15694
  // Nothing answers it: whatever the gesture translated goes home — and
15609
15695
  // there is nothing to take back when `onMoving` had the caller draw.
15610
- gestureInfo.cancelPositionAnimated();
15696
+ await whenAnimationDone(gestureInfo.cancelPositionAnimated());
15697
+ onCarryEnd?.({ outcome: null, refused: false });
15611
15698
  return;
15612
15699
  }
15613
15700
  if (dropMeans === "leave") {
@@ -15618,10 +15705,13 @@ const startDragToMoveElement = (
15618
15705
  // it where it goes back to; travelled home on a reject.
15619
15706
  try {
15620
15707
  await onLeave?.({ gestureInfo, x: xDelta, y: yDelta });
15621
- gestureInfo.cancelPosition();
15622
- } catch {
15623
- gestureInfo.cancelPositionAnimated();
15708
+ } catch (error) {
15709
+ await whenAnimationDone(gestureInfo.cancelPositionAnimated());
15710
+ onCarryEnd?.({ outcome: dropMeans, refused: true, error });
15711
+ return;
15624
15712
  }
15713
+ gestureInfo.cancelPosition();
15714
+ onCarryEnd?.({ outcome: dropMeans, refused: false });
15625
15715
  return;
15626
15716
  }
15627
15717
  // Kept where the hand let go of it while the answer is asked — a thing
@@ -15630,17 +15720,18 @@ const startDragToMoveElement = (
15630
15720
  const layoutBeforeAnswer = readOwnLayout(draggedElement);
15631
15721
  try {
15632
15722
  await onMove?.({ gestureInfo, x: xDelta, y: yDelta });
15633
- } catch {
15634
- gestureInfo.cancelPositionAnimated();
15723
+ } catch (error) {
15724
+ await whenAnimationDone(gestureInfo.cancelPositionAnimated());
15725
+ onCarryEnd?.({ outcome: dropMeans, refused: true, error });
15635
15726
  return;
15636
15727
  }
15637
- if (onMoving) {
15638
- // Nothing was translated, so there is nothing to keep or to take back:
15639
- // who owns the position was settled when the gesture was declared,
15640
- // rather than read off the element afterwards.
15641
- return;
15728
+ // Nothing was translated under `onMoving`, so there is nothing to keep
15729
+ // or to take back: who owns the position was settled when the gesture
15730
+ // was declared, rather than read off the element afterwards.
15731
+ if (!onMoving) {
15732
+ settleMovedElement(draggedElement, gestureInfo, layoutBeforeAnswer);
15642
15733
  }
15643
- settleMovedElement(draggedElement, gestureInfo, layoutBeforeAnswer);
15734
+ onCarryEnd?.({ outcome: dropMeans, refused: false });
15644
15735
  });
15645
15736
  return dragGesture;
15646
15737
  },
@@ -15806,6 +15897,7 @@ const startDragToCarryCopy = (
15806
15897
  onToss,
15807
15898
  onLeave,
15808
15899
  onRelease,
15900
+ onCarryEnd,
15809
15901
  outsideOf,
15810
15902
  tossDistance,
15811
15903
  tossSpeed,
@@ -15884,6 +15976,10 @@ const startDragToCarryCopy = (
15884
15976
 
15885
15977
  const gestureController = createDragToMoveGestureController({
15886
15978
  direction,
15979
+ // The wrapper's own `translate` carries its flights — to the hand
15980
+ // that pressed elsewhere, away when thrown, home when refused — and
15981
+ // nothing rotates or scales it: the hand goes in its transform.
15982
+ positionProperty: "transform",
15887
15983
  releasePositionEffect: "manual",
15888
15984
  areaConstraint,
15889
15985
  ...options,
@@ -16154,6 +16250,9 @@ const startDragToCarryCopy = (
16154
16250
  }),
16155
16251
  );
16156
16252
 
16253
+ // What the answer said no with, once the copy is home: told with the
16254
+ // end of the carry, where the original stands again.
16255
+ let refusal = null;
16157
16256
  // The copy stops where the hand left it, and the answer is given a way to
16158
16257
  // take it the rest of the way — synchronously, inside a view transition, so
16159
16258
  // it is captured where it lands rather than where it was let go of.
@@ -16178,11 +16277,12 @@ const startDragToCarryCopy = (
16178
16277
  };
16179
16278
  try {
16180
16279
  await answer(syncCloneWithDropTarget);
16181
- } catch {
16280
+ } catch (error) {
16182
16281
  // Coming down on a place can be a QUESTION, and the answer may be
16183
16282
  // no. Refused, the thing is still where it was and the screen has
16184
16283
  // to say so: the copy takes the same journey home a refused throw
16185
16284
  // takes — from wherever it got to, which is where it visibly is.
16285
+ refusal = { error };
16186
16286
  await settleCloneBack(cloneWrapper, draggedElement);
16187
16287
  }
16188
16288
  };
@@ -16192,18 +16292,24 @@ const startDragToCarryCopy = (
16192
16292
  // there rather than from where the clone was declared.
16193
16293
  setCloneViewportRect(cloneWrapper, cloneWrapper);
16194
16294
  gestureInfo.cancelPosition();
16195
- const gone = await tossCloneAway(cloneWrapper, gestureInfo, onToss);
16196
- if (!gone) {
16295
+ const tossed = await tossCloneAway(
16296
+ cloneWrapper,
16297
+ gestureInfo,
16298
+ onToss,
16299
+ );
16300
+ if (!tossed.gone) {
16197
16301
  // It still exists, so the screen has to say so: the copy comes back
16198
16302
  // over the original, and taking it away then reveals the row in
16199
16303
  // place.
16304
+ refusal = { error: tossed.error };
16200
16305
  await settleCloneBack(cloneWrapper, draggedElement);
16201
16306
  }
16202
16307
  } else if (dropMeans === "leave") {
16203
16308
  setCloneViewportRect(cloneWrapper, cloneWrapper);
16204
16309
  gestureInfo.cancelPosition();
16205
- const gone = await letCloneGo(cloneWrapper, gestureInfo, onLeave);
16206
- if (!gone) {
16310
+ const left = await letCloneGo(cloneWrapper, gestureInfo, onLeave);
16311
+ if (!left.gone) {
16312
+ refusal = { error: left.error };
16207
16313
  await settleCloneBack(cloneWrapper, draggedElement);
16208
16314
  }
16209
16315
  } else if (dropMeans === "land") {
@@ -16235,6 +16341,11 @@ const startDragToCarryCopy = (
16235
16341
  }
16236
16342
  draggedElement.removeAttribute("navi-drag-clone-source");
16237
16343
  cloneWrapper.remove();
16344
+ onCarryEnd?.({
16345
+ outcome: dropMeans === "cancel" ? null : dropMeans,
16346
+ refused: Boolean(refusal),
16347
+ error: refusal?.error,
16348
+ });
16238
16349
  });
16239
16350
 
16240
16351
  return dragGesture;
@@ -16505,7 +16616,8 @@ const findPlaceOnNearestLine = (dragElementRect, items, flow) => {
16505
16616
 
16506
16617
  /**
16507
16618
  * The copy leaves the screen the way it was thrown, and the caller says what that
16508
- * meant. Resolves true when it is really gone.
16619
+ * meant. Resolves `{ gone: true }` when it is really gone, and with what the
16620
+ * answer refused with otherwise.
16509
16621
  *
16510
16622
  * The answer is asked for WHILE it flies rather than after: the thing is already
16511
16623
  * far away by the time the request lands, which is the whole point of a gesture
@@ -16520,15 +16632,15 @@ const tossCloneAway = async (cloneWrapper, gestureInfo, onToss) => {
16520
16632
  }px`;
16521
16633
  try {
16522
16634
  await onToss?.({ gestureInfo });
16523
- return true;
16524
- } catch {
16525
- return false;
16635
+ return { gone: true };
16636
+ } catch (error) {
16637
+ return { gone: false, error };
16526
16638
  }
16527
16639
  };
16528
16640
 
16529
16641
  /**
16530
16642
  * The copy fades where the hand let go of it, and the caller says what being let
16531
- * go of there meant. Resolves true when it is really gone.
16643
+ * go of there meant. Resolves like tossCloneAway.
16532
16644
  *
16533
16645
  * No flight, unlike a throw: nothing was thrown. The thing was put down over
16534
16646
  * nothing, and it goes from there — the same attribute as a throw, so that a
@@ -16539,9 +16651,9 @@ const letCloneGo = async (cloneWrapper, gestureInfo, onLeave) => {
16539
16651
  try {
16540
16652
  const { xDelta, yDelta } = gestureInfo.layout;
16541
16653
  await onLeave?.({ gestureInfo, x: xDelta, y: yDelta });
16542
- return true;
16543
- } catch {
16544
- return false;
16654
+ return { gone: true };
16655
+ } catch (error) {
16656
+ return { gone: false, error };
16545
16657
  }
16546
16658
  };
16547
16659
 
@@ -16599,6 +16711,18 @@ const settleCloneBack = (cloneWrapper, sourceElement) => {
16599
16711
  });
16600
16712
  };
16601
16713
 
16714
+ // The way home an element travels on its own (cancelPositionAnimated), waited
16715
+ // out: what is said about it after is said where it stands again.
16716
+ const whenAnimationDone = (animation) => {
16717
+ if (!animation) {
16718
+ return Promise.resolve();
16719
+ }
16720
+ return animation.finished.then(
16721
+ () => {},
16722
+ () => {},
16723
+ );
16724
+ };
16725
+
16602
16726
  /**
16603
16727
  * The copy is let go of, and it is still there: flying home, coming down on a
16604
16728
  * place, waiting on an answer. It is still the object for all that time — a hand
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.95",
3
+ "version": "0.17.96",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {