@jsenv/dom 0.17.10 → 0.17.12

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 +335 -149
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -8623,6 +8623,72 @@ const createDragGestureController = (options = {}) => {
8623
8623
  };
8624
8624
  dragGesture.dragViaPointer = dragViaPointer;
8625
8625
  dragGesture.releaseViaPointer = releaseViaPointer;
8626
+ /*
8627
+ * A press that starts a drag is not a press that starts a selection: the
8628
+ * browser sees a pointer going down on text and moving, and that is its
8629
+ * own gesture — the words under the finger turn blue while the element
8630
+ * travels, and the selection outlives the release.
8631
+ *
8632
+ * Refused from the grab, before any threshold: whether the press becomes a
8633
+ * drag is decided a few pixels later, but the selection is decided at the
8634
+ * FIRST move, and by then it is too late to say no.
8635
+ *
8636
+ * `user-select: none` would say it in CSS, but it would say it to
8637
+ * everybody: the element would stop being selectable even when nobody is
8638
+ * dragging it. Here it is refused for the length of one gesture.
8639
+ */
8640
+ const preventSelectStart = selectStartEvent => {
8641
+ selectStartEvent.preventDefault();
8642
+ };
8643
+ document.addEventListener("selectstart", preventSelectStart);
8644
+ // A press also puts an end to the selection the page was already holding,
8645
+ // the way the browser's own press does: refusing selectstart keeps a new
8646
+ // selection from being made, it says nothing about the one painted before
8647
+ // — which would otherwise sit there through a gesture that has nothing to
8648
+ // do with it.
8649
+ collapseSelection();
8650
+ dragGesture.addReleaseCallback(() => {
8651
+ document.removeEventListener("selectstart", preventSelectStart);
8652
+ });
8653
+ /*
8654
+ * Refusing every selection also refuses the one a press is entitled to
8655
+ * make: a double click selects the word under it, and that selection is
8656
+ * over before the pointer has gone anywhere. It is made here instead,
8657
+ * spelled out (see selectWordAtPoint) rather than left to a browser
8658
+ * heuristic that cannot tell a drag from a click.
8659
+ *
8660
+ * On the document and outliving the gesture, because the gesture is
8661
+ * already over when the second click completes: dblclick comes after
8662
+ * mouseup, the gesture ends at pointerup. It is dropped when it fires, and
8663
+ * otherwise when the next press installs its own — a listener waiting for
8664
+ * a double click that never comes costs nothing until then.
8665
+ */
8666
+ removePendingDoubleClickListener();
8667
+ const onDoubleClick = dblclickEvent => {
8668
+ removePendingDoubleClickListener = NOOP;
8669
+ // A drag that happened is a gesture, not a click: the second press of a
8670
+ // double click can be the one that drags, and what it drags must not end
8671
+ // up selected too.
8672
+ if (dragGesture.gestureInfo.started) {
8673
+ return;
8674
+ }
8675
+ // Text the page says is not selectable stays not selectable: a
8676
+ // programmatic selection goes through `user-select: none` in every
8677
+ // engine — it is a rule about what the USER may start, and the browser
8678
+ // does not read it back when asked directly. Read here so that doing the
8679
+ // browser's work does not also undo what the page asked of it.
8680
+ if (!isSelectable(dblclickEvent.target)) {
8681
+ return;
8682
+ }
8683
+ selectWordAtPoint(dblclickEvent.clientX, dblclickEvent.clientY);
8684
+ };
8685
+ document.addEventListener("dblclick", onDoubleClick, {
8686
+ once: true
8687
+ });
8688
+ removePendingDoubleClickListener = () => {
8689
+ removePendingDoubleClickListener = NOOP;
8690
+ document.removeEventListener("dblclick", onDoubleClick);
8691
+ };
8626
8692
  const cleanup = initializer({
8627
8693
  onMove: dragViaPointer,
8628
8694
  onRelease: releaseViaPointer,
@@ -8815,6 +8881,57 @@ const definePropertyAsReadOnly = (object, propertyName) => {
8815
8881
  value: object[propertyName]
8816
8882
  });
8817
8883
  };
8884
+ const NOOP = () => {};
8885
+ let removePendingDoubleClickListener = NOOP;
8886
+
8887
+ // What a double click selects when the browser is allowed to do it itself: the
8888
+ // word around the caret the click lands on.
8889
+ const selectWordAtPoint = (x, y) => {
8890
+ const caretRange = createCaretRange(x, y);
8891
+ if (!caretRange) {
8892
+ return;
8893
+ }
8894
+ const selection = window.getSelection();
8895
+ selection.removeAllRanges();
8896
+ selection.addRange(caretRange);
8897
+ // "word" is not a position a Range can be built from — it is a movement the
8898
+ // selection knows how to make, and the caret walking to both of its edges is
8899
+ // what draws the word.
8900
+ if (selection.modify) {
8901
+ selection.modify("move", "backward", "word");
8902
+ selection.modify("extend", "forward", "word");
8903
+ }
8904
+ };
8905
+ const createCaretRange = (x, y) => {
8906
+ if (document.caretPositionFromPoint) {
8907
+ const caretPosition = document.caretPositionFromPoint(x, y);
8908
+ if (!caretPosition) {
8909
+ return null;
8910
+ }
8911
+ const range = document.createRange();
8912
+ range.setStart(caretPosition.offsetNode, caretPosition.offset);
8913
+ range.collapse(true);
8914
+ return range;
8915
+ }
8916
+ if (document.caretRangeFromPoint) {
8917
+ return document.caretRangeFromPoint(x, y);
8918
+ }
8919
+ return null;
8920
+ };
8921
+ const isSelectable = element => {
8922
+ if (!element || element.nodeType !== 1) {
8923
+ return true;
8924
+ }
8925
+ const computedStyle = window.getComputedStyle(element);
8926
+ const userSelect = computedStyle.userSelect || computedStyle.webkitUserSelect;
8927
+ return userSelect !== "none";
8928
+ };
8929
+ const collapseSelection = () => {
8930
+ const selection = window.getSelection();
8931
+ if (selection && !selection.isCollapsed) {
8932
+ selection.removeAllRanges();
8933
+ }
8934
+ };
8818
8935
 
8819
8936
  installImportMetaCssBuild(import.meta);/**
8820
8937
  * When a press becomes a drag.
@@ -8976,11 +9093,47 @@ const dragAfterLongPress = (grabEvent, dragGestureInitializer, {
8976
9093
  onPressCancel,
8977
9094
  onPress
8978
9095
  }) => {
9096
+ /*
9097
+ * WHETHER a touchmove can be refused at all is decided when the touch BEGINS,
9098
+ * from the non-passive listeners present at that moment — and on this path the
9099
+ * gesture, which is what refuses it (see preventTouchScroll in
9100
+ * drag_gesture.js), is only born once the wait is over. By then the browser has
9101
+ * long made up its mind: every touchmove it hands over is already
9102
+ * `cancelable: false`, the refusal does nothing, and the page scrolls away with
9103
+ * the object still under the finger — until the touch is taken for a scroll and
9104
+ * the pointer stream is cancelled, which is the drag dying mid-gesture.
9105
+ *
9106
+ * So the listener the decision looks for is put down with the FINGER, before
9107
+ * anyone knows whether this press is a drag. It refuses nothing — a press that
9108
+ * is still only a press must leave the scroll alone, which is exactly what the
9109
+ * wait is there to tell apart. Its only job is to be there, so that the
9110
+ * refusal made later is one the browser still listens to.
9111
+ */
9112
+ const keepTouchRefusable = () => {
9113
+ // Being registered IS the whole of it — see above.
9114
+ };
9115
+ const grabTarget = grabEvent.target;
9116
+ window.addEventListener("touchmove", keepTouchRefusable, {
9117
+ passive: false,
9118
+ capture: true
9119
+ });
9120
+ grabTarget.addEventListener("touchmove", keepTouchRefusable, {
9121
+ passive: false
9122
+ });
9123
+ const stopKeepingTouchRefusable = () => {
9124
+ window.removeEventListener("touchmove", keepTouchRefusable, {
9125
+ capture: true
9126
+ });
9127
+ grabTarget.removeEventListener("touchmove", keepTouchRefusable);
9128
+ };
8979
9129
  waitForPressHeld(grabEvent, {
8980
9130
  delay: longPressDelay,
8981
9131
  slop: longPressSlop,
8982
9132
  onPressStart,
8983
- onPressCancel,
9133
+ onPressCancel: pointerEvent => {
9134
+ stopKeepingTouchRefusable();
9135
+ onPressCancel?.(pointerEvent);
9136
+ },
8984
9137
  onPressHeld: (pressEvent, {
8985
9138
  endPress
8986
9139
  }) => {
@@ -8990,10 +9143,14 @@ const dragAfterLongPress = (grabEvent, dragGestureInitializer, {
8990
9143
  // for every way a drag can begin.
8991
9144
  const dragGesture = startDragGesture(dragGestureInitializer);
8992
9145
  if (!dragGesture) {
9146
+ stopKeepingTouchRefusable();
8993
9147
  endPress();
8994
9148
  return;
8995
9149
  }
8996
- dragGesture.addReleaseCallback(endPress);
9150
+ dragGesture.addReleaseCallback(() => {
9151
+ stopKeepingTouchRefusable();
9152
+ endPress();
9153
+ });
8997
9154
  }
8998
9155
  });
8999
9156
  };
@@ -10921,33 +11078,6 @@ const findTableCellCol = (cellElement) => {
10921
11078
  return correspondingCol;
10922
11079
  };
10923
11080
 
10924
- // Temporarily attach to the element so inherited CSS vars resolve correctly,
10925
- // then snapshot all drop-hint custom properties onto the scroll container
10926
- // so they survive once the element moves to the scroll container.
10927
- const moveCSSVars = (vars, fromEl, toEl) => {
10928
- const fromComputedStyle = getComputedStyle(fromEl);
10929
- const savedVars = {};
10930
- for (const varName of vars) {
10931
- const value = fromComputedStyle.getPropertyValue(varName).trim();
10932
- if (value) {
10933
- savedVars[varName] = toEl.style.getPropertyValue(varName);
10934
- toEl.style.setProperty(varName, value);
10935
- }
10936
- }
10937
-
10938
- return () => {
10939
- for (const varName of vars) {
10940
- if (varName in savedVars) {
10941
- if (savedVars[varName]) {
10942
- toEl.style.setProperty(varName, savedVars[varName]);
10943
- } else {
10944
- toEl.style.removeProperty(varName);
10945
- }
10946
- }
10947
- }
10948
- };
10949
- };
10950
-
10951
11081
  const applyStickyFrontiersToAutoScrollArea = (
10952
11082
  autoScrollArea,
10953
11083
  { direction, scrollContainer, dragName },
@@ -11081,6 +11211,12 @@ installImportMetaCssBuild(import.meta);/**
11081
11211
  * stable row of items to look between.
11082
11212
  * - **toss**: it is gotten rid of. The same copy, for the opposite reason: the
11083
11213
  * original stays until the answer says it is really gone.
11214
+ * - **land**: it comes down ON something. Also a copy, and the closest to
11215
+ * `reorder` — the difference is what a target IS: a row of a list is a place
11216
+ * BETWEEN two others, whereas a square of a board is a place of its own, which
11217
+ * may already be taken. So nothing is inserted and nothing is a no-op: the
11218
+ * answer is "this one came down on that one", and what that means (take the
11219
+ * place, swap the two, refuse) is the caller's.
11084
11220
  *
11085
11221
  * The caller lists which outcomes ITS element can answer, and only the machinery
11086
11222
  * those need runs: no copy for a move, no drop hint for something that can only be
@@ -11102,13 +11238,14 @@ const TOSS_DURATION_MS = 320;
11102
11238
  // Far enough to be off any screen, in the direction the hand was going.
11103
11239
  const TOSS_DISTANCE = 900;
11104
11240
  const css$1 = /* css */`
11105
- /* IN THE PAGE, NOT IN THE LIST: the hint lands on the edge of a row, which
11106
- for the last one is the very bottom of the scroll area — drawn inside it,
11107
- the line would push the scrollable area a few pixels further and make a
11241
+ /* IT COSTS THE LIST NOTHING: the hint lands on the edge of a row, which for
11242
+ the last one is the very bottom of the scroll area — a line taking up room
11243
+ there would push the scrollable area a few pixels further and make a
11108
11244
  scrollbar appear (or hide the hint under it) exactly when one is trying to
11109
- drop at the end. Placed in the body and positioned in viewport
11110
- coordinates, it can sit anywhere, overhang the list, and cost nothing to
11111
- the layout. Fixed, like the clone it accompanies. */
11245
+ drop at the end. Being fixed is what avoids it: a fixed box has the
11246
+ viewport as containing block, so it is left out of the scrollable overflow
11247
+ of every ancestor and can overhang the list freely. Same for the clone it
11248
+ accompanies. */
11112
11249
  .navi_drop_hint {
11113
11250
  /* A popover, so it lands in the top layer: no z-index to bid against the
11114
11251
  page, and nothing it can be hidden behind. Shown BEFORE the clone, which
@@ -11150,8 +11287,8 @@ const css$1 = /* css */`
11150
11287
  /* A chevron at each end, pointing in: the line alone is easy to lose against
11151
11288
  a list of borders and separators, two arrows read as "here" at a glance
11152
11289
  (same idea as the table's column drop preview). They overhang the line,
11153
- which costs nothing now that the hint is out of the scrollable area — and
11154
- the more they stick out, the easier they are to spot. */
11290
+ which costs nothing to a box left out of the scrollable area — and the more
11291
+ they stick out, the easier they are to spot. */
11155
11292
  .navi_drop_hint_cap {
11156
11293
  position: absolute;
11157
11294
  top: 50%;
@@ -11172,14 +11309,45 @@ const css$1 = /* css */`
11172
11309
  rotate: 90deg;
11173
11310
  }
11174
11311
 
11312
+ /* WHERE IT LANDS, when landing is ON a thing rather than between two: the
11313
+ place itself is lit up, because there is no gap to draw a line in. Fixed
11314
+ and in the top layer for the same reasons as the line above. */
11315
+ .navi_drop_surface {
11316
+ position: fixed;
11317
+ inset: auto;
11318
+ top: var(--drop-target-top);
11319
+ left: var(--drop-target-left);
11320
+ display: none;
11321
+ box-sizing: border-box;
11322
+ width: var(--drop-target-width);
11323
+ height: var(--drop-target-height);
11324
+ margin: 0;
11325
+ padding: 0;
11326
+ color: inherit;
11327
+ background: var(--drop-surface-background-color, rgba(68, 118, 255, 0.16));
11328
+ border: var(--drop-surface-border-width, 2px) solid
11329
+ var(--drop-surface-border-color, #4476ff);
11330
+ border-radius: var(--drop-surface-border-radius, 6px);
11331
+ pointer-events: none;
11332
+ overflow: visible;
11333
+ }
11334
+ .navi_drop_surface[data-drop-over]:popover-open {
11335
+ display: block;
11336
+ }
11337
+
11175
11338
  /* WHO CAN START A DRAG, said in the cursor.
11176
- A handle drags on the spot, so it shows the hand. A source only drags once
11177
- the intent shows (a few pixels of travel, or a long press) a plain click
11178
- stays a click but the text inside it cannot be selected (the gesture takes
11179
- the pointer), so an I-beam over it would promise something that does not
11180
- happen: it reads as a plain surface instead. An opted-out area keeps both
11181
- its cursor and its selection, and never starts a drag (see the check in
11182
- startDragTo).
11339
+ A handle exists only to drag, so it shows the hand. A source does not, and
11340
+ the gesture must not claim its cursor: it drags only once the intent shows
11341
+ (a few pixels of travel, or a long press), a plain click on it stays a
11342
+ click, and it is usually something else FIRST a link, a card one opens.
11343
+ The cursor says what the element is, and a hand insisting on the one thing
11344
+ it can also be would talk over that. So it is left alone — default, and not
11345
+ an I-beam, because dragging across the text does not select it (the gesture
11346
+ takes the pointer; see the selectstart refused in drag_gesture.js) — and
11347
+ whoever puts the drag there asks for the hand when a grab really is the
11348
+ first thing the element offers.
11349
+ An opted-out area keeps both its cursor and its selection, and never starts
11350
+ a drag (see the check in startDragTo).
11183
11351
  Controls inside a source keep their own cursor: cursor is inherited, and
11184
11352
  anything setting its own (a button's pointer) wins on itself.
11185
11353
  Only the resting cursor is set here: what it becomes once a drag is under
@@ -11190,11 +11358,9 @@ const css$1 = /* css */`
11190
11358
  }
11191
11359
  [data-drag-source] {
11192
11360
  cursor: default;
11193
- user-select: none;
11194
11361
  }
11195
11362
  [data-drag-ignore] {
11196
11363
  cursor: auto;
11197
- user-select: auto;
11198
11364
  }
11199
11365
 
11200
11366
  [navi-drag-clone-source] {
@@ -11218,13 +11384,10 @@ const css$1 = /* css */`
11218
11384
  color: inherit;
11219
11385
  background: transparent;
11220
11386
  border: none;
11221
- /* A var, and read from the dragged element (see dragCSSVars): what being
11222
- carried LOOKS like belongs to whoever owns the thing a row lifted off a
11223
- list wants this shadow, a sheet of paper leaving a board wants none, and its
11224
- shade is a theme's business either way. */
11225
- box-shadow: var(--drag-clone-shadow, 0 12px 28px rgba(0, 0, 0, 0.22));
11387
+ /* Carries the chain down to the copy, for an item whose own radius is an
11388
+ "inherit" from the list around it. */
11389
+ border-radius: inherit;
11226
11390
  opacity: 0.95;
11227
- transition: box-shadow 0.15s ease;
11228
11391
  pointer-events: none;
11229
11392
  /* Nothing in a copy being carried by a pointer is text to select: the
11230
11393
  selection belongs to the original, which is still in the page. This is the
@@ -11247,17 +11410,23 @@ const css$1 = /* css */`
11247
11410
  }
11248
11411
 
11249
11412
  [navi-drag-clone] {
11413
+ /* Cast by the copy itself rather than by the box around it, so it takes the
11414
+ shape of the thing — a rounded row throws a rounded shadow. Its value is a
11415
+ var read on the copy, which IS the dragged element: what being carried
11416
+ looks like belongs to whoever owns the thing — a row lifted off a list
11417
+ wants this shadow, a sheet of paper leaving a board wants none, and its
11418
+ shade is a theme's business either way. */
11419
+ box-shadow: var(--drag-clone-shadow, 0 12px 28px rgba(0, 0, 0, 0.22));
11250
11420
  transform: scale(var(--drag-clone-scale, 1.03));
11251
11421
  transform-origin: var(--drag-origin);
11252
- transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
11422
+ transition:
11423
+ transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1),
11424
+ box-shadow 0.15s ease;
11253
11425
  }
11254
11426
 
11255
11427
  @starting-style {
11256
- [navi-drag-clone-wrapper] {
11257
- box-shadow: none;
11258
- }
11259
-
11260
11428
  [navi-drag-clone] {
11429
+ box-shadow: none;
11261
11430
  transform: scale(1);
11262
11431
  }
11263
11432
  }
@@ -11265,7 +11434,6 @@ const css$1 = /* css */`
11265
11434
  // At module scope, not inside startDragTo: the cursor rules above say who
11266
11435
  // can start a drag, and they have to be true BEFORE anyone drags anything.
11267
11436
  import.meta.css = [css$1, "@jsenv/dom/src/interaction/drag/drag_to.js"];
11268
- const dragCSSVars = ["--drop-hint-size", "--drop-hint-background-color", "--drop-hint-border-radius", "--drop-hint-margin-x", "--drop-hint-margin-y", "--drop-hint-arrow-size", "--drag-clone-scale", "--drag-clone-shadow"];
11269
11437
 
11270
11438
  /**
11271
11439
  * Starts a drag-to-reorder interaction on a list item.
@@ -11276,9 +11444,9 @@ const dragCSSVars = ["--drop-hint-size", "--drop-hint-background-color", "--drop
11276
11444
  * reorders anything by accident.
11277
11445
  * - Clones the grabbed element and moves the clone while the original stays hidden in place
11278
11446
  * (keeps the layout intact so other items don't shift during the drag).
11279
- * - CSS vars (`--drop-hint-size`, `--drop-hint-background-color`, etc.) are read from the
11280
- * dragged element and moved to `document.documentElement` for the duration of the drag so
11281
- * the drop-hint and clone both in `document.body` can inherit them.
11447
+ * - The clone and the drop-hint live in the dragged element's own parent, so the CSS vars
11448
+ * that dress them (`--drag-clone-shadow`, `--drop-hint-size`, …) reach them by plain
11449
+ * inheritance, and so do the rules the list writes for its items.
11282
11450
  * - Shows a drop-hint line indicating where the item will land.
11283
11451
  * - Drop-target detection is intersection-based: the clone's bounding rect is compared
11284
11452
  * against every item that matches `itemSelector` in the scroll container.
@@ -11715,10 +11883,11 @@ const createDragToMoveGestureController = ({
11715
11883
  * Starts a drag, for one or more of the outcomes listed.
11716
11884
  *
11717
11885
  * @param {PointerEvent} event The `pointerdown` that may become a drag.
11718
- * @param {("move"|"reorder"|"toss")[]} effects
11719
- * What letting go of this element can mean. `reorder` and `toss` carry a copy;
11720
- * `move` carries the element itself. Asking for `move` and `reorder` together is
11721
- * asking one release to mean two things.
11886
+ * @param {("move"|"reorder"|"toss"|"land")[]} effects
11887
+ * What letting go of this element can mean. `reorder`, `toss` and `land` carry a
11888
+ * copy; `move` carries the element itself. Asking for `move` and `reorder`
11889
+ * together is asking one release to mean two things, and so is asking for
11890
+ * `reorder` and `land`.
11722
11891
  * @param {object} [options]
11723
11892
  * @param {Element} [options.draggedElement=event.currentTarget]
11724
11893
  * @param {(detail: {gestureInfo: object, x: number, y: number}) => Promise|void} [options.onMove]
@@ -11734,6 +11903,12 @@ const createDragToMoveGestureController = ({
11734
11903
  * It was thrown away. The copy leaves the screen while this runs and comes back
11735
11904
  * if the promise rejects, because the thing still exists and the screen has to
11736
11905
  * say so.
11906
+ * @param {function} [options.onLand]
11907
+ * `onLand(fromId, toId, syncCloneWithDropTarget)` — it came down on `toId`, which
11908
+ * is an element and never null: nothing under the copy is a cancelled release.
11909
+ * The copy is held until what comes back settles, exactly like `onReorder`.
11910
+ * `syncCloneWithDropTarget` takes an element when the place is not the shape of
11911
+ * what stands on it: the copy then takes THAT box instead of the target's.
11737
11912
  * @param {number} [options.tossDistance=110] How far a throw goes, in px.
11738
11913
  * @param {number} [options.tossSpeed=0.45] And how fast, in px/ms. BOTH are asked
11739
11914
  * for: one without the other is moving the thing while hesitating, and nothing is
@@ -11768,11 +11943,13 @@ const startDragTo = (event, effects, {
11768
11943
  }
11769
11944
  const canReorder = effects.includes("reorder");
11770
11945
  const canToss = effects.includes("toss");
11771
- if (canReorder || canToss) {
11946
+ const canLand = effects.includes("land");
11947
+ if (canReorder || canToss || canLand) {
11772
11948
  return startDragToCarryCopy(event, {
11773
11949
  draggedElement,
11774
11950
  canReorder,
11775
11951
  canToss,
11952
+ canLand,
11776
11953
  ...options
11777
11954
  });
11778
11955
  }
@@ -11857,6 +12034,7 @@ const resolveDropMeaning = ({
11857
12034
  hasDropTarget,
11858
12035
  canReorder,
11859
12036
  canToss,
12037
+ canLand,
11860
12038
  tossDistance = TOSS_DISTANCE_TO_COMMIT,
11861
12039
  tossSpeed = TOSS_SPEED_TO_COMMIT
11862
12040
  }) => {
@@ -11870,8 +12048,13 @@ const resolveDropMeaning = ({
11870
12048
  return "toss";
11871
12049
  }
11872
12050
  }
11873
- if (canReorder && hasDropTarget) {
11874
- return "reorder";
12051
+ if (hasDropTarget) {
12052
+ if (canLand) {
12053
+ return "land";
12054
+ }
12055
+ if (canReorder) {
12056
+ return "reorder";
12057
+ }
11875
12058
  }
11876
12059
  return "cancel";
11877
12060
  };
@@ -11880,13 +12063,15 @@ const resolveDropMeaning = ({
11880
12063
  * A COPY of the element is carried, and the original keeps its place in the
11881
12064
  * layout — which is what makes a reorder possible at all: nothing else moves
11882
12065
  * while the hand looks for a place, so there is a stable row of items to look
11883
- * between. A throw uses the same copy for the opposite reason: the original stays
11884
- * until the answer says it is really gone.
12066
+ * between. A landing on a place of a board is the same, and a throw uses that copy
12067
+ * for the opposite reason: the original stays until the answer says it is really
12068
+ * gone.
11885
12069
  */
11886
12070
  const startDragToCarryCopy = (event, {
11887
12071
  draggedElement,
11888
12072
  canReorder,
11889
12073
  canToss,
12074
+ canLand,
11890
12075
  // Something that can be thrown away has to be able to LEAVE. The default of
11891
12076
  // the layer below keeps what is dragged inside its scroll area, which is right
11892
12077
  // for a reorder (a row belongs to its list) and makes a throw impossible — the
@@ -11901,10 +12086,16 @@ const startDragToCarryCopy = (event, {
11901
12086
  itemSelector,
11902
12087
  getItemId,
11903
12088
  onReorder,
12089
+ onLand,
11904
12090
  onToss,
11905
12091
  tossDistance,
11906
12092
  tossSpeed,
11907
- direction = {
12093
+ // A list runs one way and reordering walks it; a board has places all around,
12094
+ // so something landing on one of them goes wherever the hand takes it.
12095
+ direction = canLand ? {
12096
+ x: true,
12097
+ y: true
12098
+ } : {
11908
12099
  x: false,
11909
12100
  y: true
11910
12101
  },
@@ -11930,9 +12121,6 @@ const startDragToCarryCopy = (event, {
11930
12121
  return dragAfterIntent(event, () => {
11931
12122
  const cloneWrapper = createDragClone(draggedElement, event);
11932
12123
  draggedElement.setAttribute("navi-drag-clone-source", "");
11933
- // Move drag related CSS vars from the element to the document
11934
- // so they're accessible to .navi_drop_hint and the clone (which are both in document.body)
11935
- const restoreCSSVars = moveCSSVars(dragCSSVars, draggedElement, document.documentElement);
11936
12124
  const gestureController = createDragToMoveGestureController({
11937
12125
  direction,
11938
12126
  releasePositionEffect: "manual",
@@ -11948,10 +12136,14 @@ const startDragToCarryCopy = (event, {
11948
12136
  dragGesture.gestureInfo.elementImpacted = cloneWrapper;
11949
12137
 
11950
12138
  // No place to land, no hint: an element that can only be thrown away has
11951
- // nowhere to be put.
11952
- const dropHintEl = canReorder ? createDropHint() : null;
12139
+ // nowhere to be put. What the hint LOOKS like follows what a place is here —
12140
+ // a line in the gap between two items, or the place itself lit up.
12141
+ const dropHintEl = canLand ? createDropSurface() : canReorder ? createDropHint() : null;
11953
12142
  if (dropHintEl) {
11954
- document.body.appendChild(dropHintEl);
12143
+ // In the container it draws into, which is where its own vars are set: the
12144
+ // shape of a drop hint is a property of the list or board it belongs to,
12145
+ // and reading it from there is inheritance rather than a hand-off.
12146
+ draggedElement.parentElement.appendChild(dropHintEl);
11955
12147
  }
11956
12148
  // The hint first, the clone second: that order is what stacks them in the
11957
12149
  // top layer.
@@ -11967,10 +12159,12 @@ const startDragToCarryCopy = (event, {
11967
12159
  return;
11968
12160
  }
11969
12161
  dropHintEl.removeAttribute("data-drop-edge");
12162
+ dropHintEl.removeAttribute("data-drop-over");
11970
12163
  dropHintEl.style.removeProperty("--drop-target-top");
11971
12164
  dropHintEl.style.removeProperty("--drop-target-bottom");
11972
12165
  dropHintEl.style.removeProperty("--drop-target-left");
11973
12166
  dropHintEl.style.removeProperty("--drop-target-width");
12167
+ dropHintEl.style.removeProperty("--drop-target-height");
11974
12168
  };
11975
12169
  const clearDropHint = () => {
11976
12170
  currentBeforeElement = undefined;
@@ -11990,13 +12184,32 @@ const startDragToCarryCopy = (event, {
11990
12184
  }
11991
12185
  }
11992
12186
  const dropTargetInfo = getDropTargetInfo(gestureInfo, items, {
11993
- fallbackToEdge: true
12187
+ // The edges of a LIST: above the first row means the top of it, below
12188
+ // the last one means the end of it. A board has no such reading — away
12189
+ // from every place is away from every place.
12190
+ fallbackToEdge: !canLand
11994
12191
  });
11995
12192
  gestureInfo.dropTargetInfo = dropTargetInfo || null;
11996
12193
  if (!dropTargetInfo) {
11997
12194
  clearDropHint();
11998
12195
  return;
11999
12196
  }
12197
+ if (canLand) {
12198
+ // The whole element is the target, so which of its edges the copy came
12199
+ // in by says nothing: there is no gap to be on one side of.
12200
+ const dropElement = dropTargetInfo.element;
12201
+ if (dropElement === currentReleaseElement) {
12202
+ return;
12203
+ }
12204
+ currentReleaseElement = dropElement;
12205
+ const dropRect = dropElement.getBoundingClientRect();
12206
+ dropHintEl.setAttribute("data-drop-over", "");
12207
+ dropHintEl.style.setProperty("--drop-target-top", `${dropRect.top}px`);
12208
+ dropHintEl.style.setProperty("--drop-target-left", `${dropRect.left}px`);
12209
+ dropHintEl.style.setProperty("--drop-target-width", `${dropRect.width}px`);
12210
+ dropHintEl.style.setProperty("--drop-target-height", `${dropRect.height}px`);
12211
+ return;
12212
+ }
12000
12213
  // Convert {element, edge} to a beforeElement using the items array
12001
12214
  // (not nextElementSibling, which breaks if non-item elements exist between items).
12002
12215
  // edge "start" → insert before the hovered element
@@ -12037,21 +12250,44 @@ const startDragToCarryCopy = (event, {
12037
12250
  dragGesture.addReleaseCallback(async gestureInfo => {
12038
12251
  clearDropHintDOM();
12039
12252
  dropHintEl?.remove();
12040
- restoreCSSVars();
12041
12253
 
12042
12254
  // What THIS release means, from what the element said it can answer. A
12043
12255
  // throw is asked about first: it is the more insistent of the two, and a
12044
12256
  // hand that sent the thing across the screen has not asked for it to swap
12045
12257
  // places with whatever it happened to fly over.
12046
- const hasDropTarget = currentBeforeElement !== undefined;
12258
+ const hasDropTarget = canLand ? currentReleaseElement !== undefined : currentBeforeElement !== undefined;
12047
12259
  const dropMeans = resolveDropMeaning({
12048
12260
  gestureInfo,
12049
12261
  hasDropTarget,
12050
12262
  canReorder,
12051
12263
  canToss,
12264
+ canLand,
12052
12265
  tossDistance,
12053
12266
  tossSpeed
12054
12267
  });
12268
+
12269
+ // The copy stops where the hand left it, and the answer is given a way to
12270
+ // take it the rest of the way — synchronously, inside a view transition, so
12271
+ // it is captured where it lands rather than where it was let go of.
12272
+ const landCopyOn = async (targetElement, answer) => {
12273
+ const clone = cloneWrapper.firstElementChild;
12274
+ // Bake the current visual position (transform included) into the CSS vars
12275
+ // so the copy stays where the user released it when the transform goes.
12276
+ setCloneViewportRect(cloneWrapper, cloneWrapper);
12277
+ gestureInfo.cancelPosition();
12278
+ // Where the copy comes down is not always the thing it came down ON: a
12279
+ // place of a board can be larger than what stands on it, and the copy
12280
+ // has to keep its own size and land where the item will be. Said with
12281
+ // an element, because the caller has one — the piece already standing
12282
+ // there, the empty slot waiting.
12283
+ const syncCloneWithDropTarget = (landingElement = targetElement) => {
12284
+ setCloneViewportRect(cloneWrapper, landingElement);
12285
+ // Removing this attr drops the CSS scale, so the browser captures the
12286
+ // copy at scale 1 as the "new" state.
12287
+ clone.removeAttribute("navi-drag-clone");
12288
+ };
12289
+ await answer(syncCloneWithDropTarget);
12290
+ };
12055
12291
  if (dropMeans === "toss") {
12056
12292
  // Bake the position the hand left it at, so the flight starts from
12057
12293
  // there rather than from where the clone was declared.
@@ -12064,25 +12300,10 @@ const startDragToCarryCopy = (event, {
12064
12300
  // place.
12065
12301
  await settleCloneBack(cloneWrapper, draggedElement);
12066
12302
  }
12067
- } else if (dropMeans === "reorder" && hasDropTarget) {
12068
- const clone = cloneWrapper.firstElementChild;
12069
- // Bake the current visual position (transform included) into the CSS vars
12070
- // so the clone stays where the user released it when we clear the transform.
12071
- setCloneViewportRect(cloneWrapper, cloneWrapper);
12072
- gestureInfo.cancelPosition();
12073
- const fromId = getItemId(draggedElement);
12074
- const toId = currentBeforeElement ? getItemId(currentBeforeElement) : null;
12075
- // provide onReorder a way to synchronously move the clone to the drop target
12076
- // (meant to be used inside a startViewTransition callback)
12077
- const syncCloneWithDropTarget = () => {
12078
- // Snap the CSS-var position to the drop target rect so the browser
12079
- // captures the "new" state at the landing position.
12080
- setCloneViewportRect(cloneWrapper, currentReleaseElement);
12081
- // Removing this attr drops the CSS scale(1.15), so the browser
12082
- // captures the clone at scale 1 as the "new" state.
12083
- clone.removeAttribute("navi-drag-clone");
12084
- };
12085
- await onReorder(fromId, toId, syncCloneWithDropTarget);
12303
+ } else if (dropMeans === "land") {
12304
+ await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onLand(getItemId(draggedElement), getItemId(currentReleaseElement), syncCloneWithDropTarget));
12305
+ } else if (dropMeans === "reorder") {
12306
+ await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onReorder(getItemId(draggedElement), currentBeforeElement ? getItemId(currentBeforeElement) : null, syncCloneWithDropTarget));
12086
12307
  }
12087
12308
  draggedElement.removeAttribute("navi-drag-clone-source");
12088
12309
  cloneWrapper.remove();
@@ -12153,6 +12374,14 @@ const createDropHint = () => {
12153
12374
  div.innerHTML = dropHintTemplate.trim();
12154
12375
  return div.firstElementChild;
12155
12376
  };
12377
+ const createDropSurface = () => {
12378
+ const div = document.createElement("div");
12379
+ div.className = "navi_drop_surface";
12380
+ // Manual, like the copy it accompanies: it is opened and closed with the drag
12381
+ // and must survive an Escape or a click elsewhere.
12382
+ div.setAttribute("popover", "manual");
12383
+ return div;
12384
+ };
12156
12385
 
12157
12386
  /**
12158
12387
  * The copy leaves the screen the way it was thrown, and the caller says what that
@@ -12211,13 +12440,6 @@ const createDragClone = (element, pointerEvent) => {
12211
12440
  // scale(1.15) expands from where the user clicked, not the element center.
12212
12441
  // These offsets are element-relative so viewport coords are correct here.
12213
12442
  wrapper.style.setProperty("--drag-origin", `${pointerEvent.clientX - rect.left}px ${pointerEvent.clientY - rect.top}px`);
12214
- // The clone is appended to document.body, so it loses inherited styles
12215
- // from the original parent. Copy the computed inherited properties that
12216
- // are most likely to affect visual appearance.
12217
- const computedStyle = getComputedStyle(element.parentElement);
12218
- for (const property of INHERITED_PROPERTIES_TO_COPY_SET) {
12219
- wrapper.style.setProperty(property, computedStyle.getPropertyValue(property));
12220
- }
12221
12443
  const elementClone = element.cloneNode(true);
12222
12444
  // A deep copy copies the ids too, and two elements answering to one id is a
12223
12445
  // document that lies: getElementById picks whichever comes first, an anchor
@@ -12235,12 +12457,15 @@ const createDragClone = (element, pointerEvent) => {
12235
12457
  elementClone.setAttribute("data-grabbed", "");
12236
12458
  elementClone.style.viewTransitionName = "navi-drag-clone";
12237
12459
  wrapper.appendChild(elementClone);
12238
- document.body.appendChild(wrapper);
12460
+ // Beside the thing it copies, so it stands where that thing stands: every
12461
+ // inherited value and every custom property the original reads, the copy reads
12462
+ // too, and a rule written for an item in this list finds the copy as well. The
12463
+ // top layer is what lets it stay there — a popover is painted above the page
12464
+ // whatever its depth in the tree, and being fixed keeps it out of the
12465
+ // scrollable overflow of the list it sits in.
12466
+ element.parentElement.appendChild(wrapper);
12239
12467
  return wrapper;
12240
12468
  };
12241
- const INHERITED_PROPERTIES_TO_COPY_SET = new Set(["color", "font-family", "font-size", "font-weight", "font-style", "line-height", "letter-spacing",
12242
- // in case the item has border-radius: inherit. The clone can inherit too
12243
- "border-radius"]);
12244
12469
 
12245
12470
  const startDragToResizeGesture = (
12246
12471
  pointerdownEvent,
@@ -12392,15 +12617,6 @@ import.meta.css = [/* css */`
12392
12617
  [data-drag-travel*="y"] * {
12393
12618
  overscroll-behavior-y: contain !important;
12394
12619
  }
12395
- :root[${WALKING_ATTRIBUTE}] {
12396
- /* A drag over text selects it on the way, and the blue trail says the
12397
- gesture was understood as something else. Not from the press: a press on
12398
- text IS how one selects it, and only a press that has become a travel has
12399
- said it was about something else — which is also why this cannot be the
12400
- whole answer, and why the selection made meanwhile is dropped by hand
12401
- (see dropSelection). */
12402
- user-select: none;
12403
- }
12404
12620
  `, "@jsenv/dom/src/interaction/drag/drag_to_travel.js"];
12405
12621
 
12406
12622
  // How far a pointer goes before it is a travel rather than a click: below this
@@ -12476,29 +12692,6 @@ const axesLeftBy = (axes, fromElement, stopElement, attribute) => {
12476
12692
  return left;
12477
12693
  };
12478
12694
 
12479
- /**
12480
- * What the browser painted blue while it was still allowed to think this press
12481
- * was about text.
12482
- *
12483
- * A mouse dragged across a page selects what it crosses, and it starts doing so
12484
- * from the first pixel — while this is still spending ten of them deciding
12485
- * whether the press is a travel at all. By the time it is one, a trail is
12486
- * already there. `user-select: none` (see the CSS) stops it GROWING, it does not
12487
- * take back what was made, and a selection already under way goes on being
12488
- * extended by some browsers whatever the property says.
12489
- *
12490
- * So it is dropped, and dropped again as it comes back. The cause is outside —
12491
- * one gesture, two things answering it, and the browser answers first — and
12492
- * cannot be removed from here; what can be removed is its trace, on every frame
12493
- * of a travel that is walking.
12494
- */
12495
- const dropSelection = () => {
12496
- const selection = window.getSelection();
12497
- if (selection && !selection.isCollapsed) {
12498
- selection.removeAllRanges();
12499
- }
12500
- };
12501
-
12502
12695
  /**
12503
12696
  * A scroller between the pointer and the box it is in, with room left the way
12504
12697
  * the gesture goes: it gets the gesture, and nothing travels — dragging a row
@@ -12817,9 +13010,6 @@ const startDragToTravel = (pointerDownEvent, {
12817
13010
  };
12818
13011
  document.documentElement.setAttribute(WALKING_ATTRIBUTE, axis);
12819
13012
  }
12820
- // Whatever the press was taken for until now, it was taken for something
12821
- // else (see dropSelection).
12822
- dropSelection();
12823
13013
  const {
12824
13014
  axis
12825
13015
  } = travel;
@@ -12881,10 +13071,6 @@ const startDragToTravel = (pointerDownEvent, {
12881
13071
  return;
12882
13072
  }
12883
13073
  finish();
12884
- // Last chance: the pointer moves once more as it goes up, and by then the
12885
- // attribute above is off — so a trail made on that last move would be the
12886
- // one that stays (see dropSelection).
12887
- dropSelection();
12888
13074
  const {
12889
13075
  axis,
12890
13076
  size,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.10",
3
+ "version": "0.17.12",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {