@jsenv/dom 0.17.10 → 0.17.11

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 +293 -147
  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.
@@ -10921,33 +11038,6 @@ const findTableCellCol = (cellElement) => {
10921
11038
  return correspondingCol;
10922
11039
  };
10923
11040
 
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
11041
  const applyStickyFrontiersToAutoScrollArea = (
10952
11042
  autoScrollArea,
10953
11043
  { direction, scrollContainer, dragName },
@@ -11081,6 +11171,12 @@ installImportMetaCssBuild(import.meta);/**
11081
11171
  * stable row of items to look between.
11082
11172
  * - **toss**: it is gotten rid of. The same copy, for the opposite reason: the
11083
11173
  * original stays until the answer says it is really gone.
11174
+ * - **land**: it comes down ON something. Also a copy, and the closest to
11175
+ * `reorder` — the difference is what a target IS: a row of a list is a place
11176
+ * BETWEEN two others, whereas a square of a board is a place of its own, which
11177
+ * may already be taken. So nothing is inserted and nothing is a no-op: the
11178
+ * answer is "this one came down on that one", and what that means (take the
11179
+ * place, swap the two, refuse) is the caller's.
11084
11180
  *
11085
11181
  * The caller lists which outcomes ITS element can answer, and only the machinery
11086
11182
  * those need runs: no copy for a move, no drop hint for something that can only be
@@ -11102,13 +11198,14 @@ const TOSS_DURATION_MS = 320;
11102
11198
  // Far enough to be off any screen, in the direction the hand was going.
11103
11199
  const TOSS_DISTANCE = 900;
11104
11200
  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
11201
+ /* IT COSTS THE LIST NOTHING: the hint lands on the edge of a row, which for
11202
+ the last one is the very bottom of the scroll area — a line taking up room
11203
+ there would push the scrollable area a few pixels further and make a
11108
11204
  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. */
11205
+ drop at the end. Being fixed is what avoids it: a fixed box has the
11206
+ viewport as containing block, so it is left out of the scrollable overflow
11207
+ of every ancestor and can overhang the list freely. Same for the clone it
11208
+ accompanies. */
11112
11209
  .navi_drop_hint {
11113
11210
  /* A popover, so it lands in the top layer: no z-index to bid against the
11114
11211
  page, and nothing it can be hidden behind. Shown BEFORE the clone, which
@@ -11150,8 +11247,8 @@ const css$1 = /* css */`
11150
11247
  /* A chevron at each end, pointing in: the line alone is easy to lose against
11151
11248
  a list of borders and separators, two arrows read as "here" at a glance
11152
11249
  (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. */
11250
+ which costs nothing to a box left out of the scrollable area — and the more
11251
+ they stick out, the easier they are to spot. */
11155
11252
  .navi_drop_hint_cap {
11156
11253
  position: absolute;
11157
11254
  top: 50%;
@@ -11172,14 +11269,45 @@ const css$1 = /* css */`
11172
11269
  rotate: 90deg;
11173
11270
  }
11174
11271
 
11272
+ /* WHERE IT LANDS, when landing is ON a thing rather than between two: the
11273
+ place itself is lit up, because there is no gap to draw a line in. Fixed
11274
+ and in the top layer for the same reasons as the line above. */
11275
+ .navi_drop_surface {
11276
+ position: fixed;
11277
+ inset: auto;
11278
+ top: var(--drop-target-top);
11279
+ left: var(--drop-target-left);
11280
+ display: none;
11281
+ box-sizing: border-box;
11282
+ width: var(--drop-target-width);
11283
+ height: var(--drop-target-height);
11284
+ margin: 0;
11285
+ padding: 0;
11286
+ color: inherit;
11287
+ background: var(--drop-surface-background-color, rgba(68, 118, 255, 0.16));
11288
+ border: var(--drop-surface-border-width, 2px) solid
11289
+ var(--drop-surface-border-color, #4476ff);
11290
+ border-radius: var(--drop-surface-border-radius, 6px);
11291
+ pointer-events: none;
11292
+ overflow: visible;
11293
+ }
11294
+ .navi_drop_surface[data-drop-over]:popover-open {
11295
+ display: block;
11296
+ }
11297
+
11175
11298
  /* 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).
11299
+ A handle exists only to drag, so it shows the hand. A source does not, and
11300
+ the gesture must not claim its cursor: it drags only once the intent shows
11301
+ (a few pixels of travel, or a long press), a plain click on it stays a
11302
+ click, and it is usually something else FIRST a link, a card one opens.
11303
+ The cursor says what the element is, and a hand insisting on the one thing
11304
+ it can also be would talk over that. So it is left alone — default, and not
11305
+ an I-beam, because dragging across the text does not select it (the gesture
11306
+ takes the pointer; see the selectstart refused in drag_gesture.js) — and
11307
+ whoever puts the drag there asks for the hand when a grab really is the
11308
+ first thing the element offers.
11309
+ An opted-out area keeps both its cursor and its selection, and never starts
11310
+ a drag (see the check in startDragTo).
11183
11311
  Controls inside a source keep their own cursor: cursor is inherited, and
11184
11312
  anything setting its own (a button's pointer) wins on itself.
11185
11313
  Only the resting cursor is set here: what it becomes once a drag is under
@@ -11190,11 +11318,9 @@ const css$1 = /* css */`
11190
11318
  }
11191
11319
  [data-drag-source] {
11192
11320
  cursor: default;
11193
- user-select: none;
11194
11321
  }
11195
11322
  [data-drag-ignore] {
11196
11323
  cursor: auto;
11197
- user-select: auto;
11198
11324
  }
11199
11325
 
11200
11326
  [navi-drag-clone-source] {
@@ -11218,13 +11344,10 @@ const css$1 = /* css */`
11218
11344
  color: inherit;
11219
11345
  background: transparent;
11220
11346
  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));
11347
+ /* Carries the chain down to the copy, for an item whose own radius is an
11348
+ "inherit" from the list around it. */
11349
+ border-radius: inherit;
11226
11350
  opacity: 0.95;
11227
- transition: box-shadow 0.15s ease;
11228
11351
  pointer-events: none;
11229
11352
  /* Nothing in a copy being carried by a pointer is text to select: the
11230
11353
  selection belongs to the original, which is still in the page. This is the
@@ -11247,17 +11370,23 @@ const css$1 = /* css */`
11247
11370
  }
11248
11371
 
11249
11372
  [navi-drag-clone] {
11373
+ /* Cast by the copy itself rather than by the box around it, so it takes the
11374
+ shape of the thing — a rounded row throws a rounded shadow. Its value is a
11375
+ var read on the copy, which IS the dragged element: what being carried
11376
+ looks like belongs to whoever owns the thing — a row lifted off a list
11377
+ wants this shadow, a sheet of paper leaving a board wants none, and its
11378
+ shade is a theme's business either way. */
11379
+ box-shadow: var(--drag-clone-shadow, 0 12px 28px rgba(0, 0, 0, 0.22));
11250
11380
  transform: scale(var(--drag-clone-scale, 1.03));
11251
11381
  transform-origin: var(--drag-origin);
11252
- transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
11382
+ transition:
11383
+ transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1),
11384
+ box-shadow 0.15s ease;
11253
11385
  }
11254
11386
 
11255
11387
  @starting-style {
11256
- [navi-drag-clone-wrapper] {
11257
- box-shadow: none;
11258
- }
11259
-
11260
11388
  [navi-drag-clone] {
11389
+ box-shadow: none;
11261
11390
  transform: scale(1);
11262
11391
  }
11263
11392
  }
@@ -11265,7 +11394,6 @@ const css$1 = /* css */`
11265
11394
  // At module scope, not inside startDragTo: the cursor rules above say who
11266
11395
  // can start a drag, and they have to be true BEFORE anyone drags anything.
11267
11396
  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
11397
 
11270
11398
  /**
11271
11399
  * Starts a drag-to-reorder interaction on a list item.
@@ -11276,9 +11404,9 @@ const dragCSSVars = ["--drop-hint-size", "--drop-hint-background-color", "--drop
11276
11404
  * reorders anything by accident.
11277
11405
  * - Clones the grabbed element and moves the clone while the original stays hidden in place
11278
11406
  * (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.
11407
+ * - The clone and the drop-hint live in the dragged element's own parent, so the CSS vars
11408
+ * that dress them (`--drag-clone-shadow`, `--drop-hint-size`, …) reach them by plain
11409
+ * inheritance, and so do the rules the list writes for its items.
11282
11410
  * - Shows a drop-hint line indicating where the item will land.
11283
11411
  * - Drop-target detection is intersection-based: the clone's bounding rect is compared
11284
11412
  * against every item that matches `itemSelector` in the scroll container.
@@ -11715,10 +11843,11 @@ const createDragToMoveGestureController = ({
11715
11843
  * Starts a drag, for one or more of the outcomes listed.
11716
11844
  *
11717
11845
  * @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.
11846
+ * @param {("move"|"reorder"|"toss"|"land")[]} effects
11847
+ * What letting go of this element can mean. `reorder`, `toss` and `land` carry a
11848
+ * copy; `move` carries the element itself. Asking for `move` and `reorder`
11849
+ * together is asking one release to mean two things, and so is asking for
11850
+ * `reorder` and `land`.
11722
11851
  * @param {object} [options]
11723
11852
  * @param {Element} [options.draggedElement=event.currentTarget]
11724
11853
  * @param {(detail: {gestureInfo: object, x: number, y: number}) => Promise|void} [options.onMove]
@@ -11734,6 +11863,12 @@ const createDragToMoveGestureController = ({
11734
11863
  * It was thrown away. The copy leaves the screen while this runs and comes back
11735
11864
  * if the promise rejects, because the thing still exists and the screen has to
11736
11865
  * say so.
11866
+ * @param {function} [options.onLand]
11867
+ * `onLand(fromId, toId, syncCloneWithDropTarget)` — it came down on `toId`, which
11868
+ * is an element and never null: nothing under the copy is a cancelled release.
11869
+ * The copy is held until what comes back settles, exactly like `onReorder`.
11870
+ * `syncCloneWithDropTarget` takes an element when the place is not the shape of
11871
+ * what stands on it: the copy then takes THAT box instead of the target's.
11737
11872
  * @param {number} [options.tossDistance=110] How far a throw goes, in px.
11738
11873
  * @param {number} [options.tossSpeed=0.45] And how fast, in px/ms. BOTH are asked
11739
11874
  * for: one without the other is moving the thing while hesitating, and nothing is
@@ -11768,11 +11903,13 @@ const startDragTo = (event, effects, {
11768
11903
  }
11769
11904
  const canReorder = effects.includes("reorder");
11770
11905
  const canToss = effects.includes("toss");
11771
- if (canReorder || canToss) {
11906
+ const canLand = effects.includes("land");
11907
+ if (canReorder || canToss || canLand) {
11772
11908
  return startDragToCarryCopy(event, {
11773
11909
  draggedElement,
11774
11910
  canReorder,
11775
11911
  canToss,
11912
+ canLand,
11776
11913
  ...options
11777
11914
  });
11778
11915
  }
@@ -11857,6 +11994,7 @@ const resolveDropMeaning = ({
11857
11994
  hasDropTarget,
11858
11995
  canReorder,
11859
11996
  canToss,
11997
+ canLand,
11860
11998
  tossDistance = TOSS_DISTANCE_TO_COMMIT,
11861
11999
  tossSpeed = TOSS_SPEED_TO_COMMIT
11862
12000
  }) => {
@@ -11870,8 +12008,13 @@ const resolveDropMeaning = ({
11870
12008
  return "toss";
11871
12009
  }
11872
12010
  }
11873
- if (canReorder && hasDropTarget) {
11874
- return "reorder";
12011
+ if (hasDropTarget) {
12012
+ if (canLand) {
12013
+ return "land";
12014
+ }
12015
+ if (canReorder) {
12016
+ return "reorder";
12017
+ }
11875
12018
  }
11876
12019
  return "cancel";
11877
12020
  };
@@ -11880,13 +12023,15 @@ const resolveDropMeaning = ({
11880
12023
  * A COPY of the element is carried, and the original keeps its place in the
11881
12024
  * layout — which is what makes a reorder possible at all: nothing else moves
11882
12025
  * 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.
12026
+ * between. A landing on a place of a board is the same, and a throw uses that copy
12027
+ * for the opposite reason: the original stays until the answer says it is really
12028
+ * gone.
11885
12029
  */
11886
12030
  const startDragToCarryCopy = (event, {
11887
12031
  draggedElement,
11888
12032
  canReorder,
11889
12033
  canToss,
12034
+ canLand,
11890
12035
  // Something that can be thrown away has to be able to LEAVE. The default of
11891
12036
  // the layer below keeps what is dragged inside its scroll area, which is right
11892
12037
  // for a reorder (a row belongs to its list) and makes a throw impossible — the
@@ -11901,10 +12046,16 @@ const startDragToCarryCopy = (event, {
11901
12046
  itemSelector,
11902
12047
  getItemId,
11903
12048
  onReorder,
12049
+ onLand,
11904
12050
  onToss,
11905
12051
  tossDistance,
11906
12052
  tossSpeed,
11907
- direction = {
12053
+ // A list runs one way and reordering walks it; a board has places all around,
12054
+ // so something landing on one of them goes wherever the hand takes it.
12055
+ direction = canLand ? {
12056
+ x: true,
12057
+ y: true
12058
+ } : {
11908
12059
  x: false,
11909
12060
  y: true
11910
12061
  },
@@ -11930,9 +12081,6 @@ const startDragToCarryCopy = (event, {
11930
12081
  return dragAfterIntent(event, () => {
11931
12082
  const cloneWrapper = createDragClone(draggedElement, event);
11932
12083
  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
12084
  const gestureController = createDragToMoveGestureController({
11937
12085
  direction,
11938
12086
  releasePositionEffect: "manual",
@@ -11948,10 +12096,14 @@ const startDragToCarryCopy = (event, {
11948
12096
  dragGesture.gestureInfo.elementImpacted = cloneWrapper;
11949
12097
 
11950
12098
  // 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;
12099
+ // nowhere to be put. What the hint LOOKS like follows what a place is here —
12100
+ // a line in the gap between two items, or the place itself lit up.
12101
+ const dropHintEl = canLand ? createDropSurface() : canReorder ? createDropHint() : null;
11953
12102
  if (dropHintEl) {
11954
- document.body.appendChild(dropHintEl);
12103
+ // In the container it draws into, which is where its own vars are set: the
12104
+ // shape of a drop hint is a property of the list or board it belongs to,
12105
+ // and reading it from there is inheritance rather than a hand-off.
12106
+ draggedElement.parentElement.appendChild(dropHintEl);
11955
12107
  }
11956
12108
  // The hint first, the clone second: that order is what stacks them in the
11957
12109
  // top layer.
@@ -11967,10 +12119,12 @@ const startDragToCarryCopy = (event, {
11967
12119
  return;
11968
12120
  }
11969
12121
  dropHintEl.removeAttribute("data-drop-edge");
12122
+ dropHintEl.removeAttribute("data-drop-over");
11970
12123
  dropHintEl.style.removeProperty("--drop-target-top");
11971
12124
  dropHintEl.style.removeProperty("--drop-target-bottom");
11972
12125
  dropHintEl.style.removeProperty("--drop-target-left");
11973
12126
  dropHintEl.style.removeProperty("--drop-target-width");
12127
+ dropHintEl.style.removeProperty("--drop-target-height");
11974
12128
  };
11975
12129
  const clearDropHint = () => {
11976
12130
  currentBeforeElement = undefined;
@@ -11990,13 +12144,32 @@ const startDragToCarryCopy = (event, {
11990
12144
  }
11991
12145
  }
11992
12146
  const dropTargetInfo = getDropTargetInfo(gestureInfo, items, {
11993
- fallbackToEdge: true
12147
+ // The edges of a LIST: above the first row means the top of it, below
12148
+ // the last one means the end of it. A board has no such reading — away
12149
+ // from every place is away from every place.
12150
+ fallbackToEdge: !canLand
11994
12151
  });
11995
12152
  gestureInfo.dropTargetInfo = dropTargetInfo || null;
11996
12153
  if (!dropTargetInfo) {
11997
12154
  clearDropHint();
11998
12155
  return;
11999
12156
  }
12157
+ if (canLand) {
12158
+ // The whole element is the target, so which of its edges the copy came
12159
+ // in by says nothing: there is no gap to be on one side of.
12160
+ const dropElement = dropTargetInfo.element;
12161
+ if (dropElement === currentReleaseElement) {
12162
+ return;
12163
+ }
12164
+ currentReleaseElement = dropElement;
12165
+ const dropRect = dropElement.getBoundingClientRect();
12166
+ dropHintEl.setAttribute("data-drop-over", "");
12167
+ dropHintEl.style.setProperty("--drop-target-top", `${dropRect.top}px`);
12168
+ dropHintEl.style.setProperty("--drop-target-left", `${dropRect.left}px`);
12169
+ dropHintEl.style.setProperty("--drop-target-width", `${dropRect.width}px`);
12170
+ dropHintEl.style.setProperty("--drop-target-height", `${dropRect.height}px`);
12171
+ return;
12172
+ }
12000
12173
  // Convert {element, edge} to a beforeElement using the items array
12001
12174
  // (not nextElementSibling, which breaks if non-item elements exist between items).
12002
12175
  // edge "start" → insert before the hovered element
@@ -12037,21 +12210,44 @@ const startDragToCarryCopy = (event, {
12037
12210
  dragGesture.addReleaseCallback(async gestureInfo => {
12038
12211
  clearDropHintDOM();
12039
12212
  dropHintEl?.remove();
12040
- restoreCSSVars();
12041
12213
 
12042
12214
  // What THIS release means, from what the element said it can answer. A
12043
12215
  // throw is asked about first: it is the more insistent of the two, and a
12044
12216
  // hand that sent the thing across the screen has not asked for it to swap
12045
12217
  // places with whatever it happened to fly over.
12046
- const hasDropTarget = currentBeforeElement !== undefined;
12218
+ const hasDropTarget = canLand ? currentReleaseElement !== undefined : currentBeforeElement !== undefined;
12047
12219
  const dropMeans = resolveDropMeaning({
12048
12220
  gestureInfo,
12049
12221
  hasDropTarget,
12050
12222
  canReorder,
12051
12223
  canToss,
12224
+ canLand,
12052
12225
  tossDistance,
12053
12226
  tossSpeed
12054
12227
  });
12228
+
12229
+ // The copy stops where the hand left it, and the answer is given a way to
12230
+ // take it the rest of the way — synchronously, inside a view transition, so
12231
+ // it is captured where it lands rather than where it was let go of.
12232
+ const landCopyOn = async (targetElement, answer) => {
12233
+ const clone = cloneWrapper.firstElementChild;
12234
+ // Bake the current visual position (transform included) into the CSS vars
12235
+ // so the copy stays where the user released it when the transform goes.
12236
+ setCloneViewportRect(cloneWrapper, cloneWrapper);
12237
+ gestureInfo.cancelPosition();
12238
+ // Where the copy comes down is not always the thing it came down ON: a
12239
+ // place of a board can be larger than what stands on it, and the copy
12240
+ // has to keep its own size and land where the item will be. Said with
12241
+ // an element, because the caller has one — the piece already standing
12242
+ // there, the empty slot waiting.
12243
+ const syncCloneWithDropTarget = (landingElement = targetElement) => {
12244
+ setCloneViewportRect(cloneWrapper, landingElement);
12245
+ // Removing this attr drops the CSS scale, so the browser captures the
12246
+ // copy at scale 1 as the "new" state.
12247
+ clone.removeAttribute("navi-drag-clone");
12248
+ };
12249
+ await answer(syncCloneWithDropTarget);
12250
+ };
12055
12251
  if (dropMeans === "toss") {
12056
12252
  // Bake the position the hand left it at, so the flight starts from
12057
12253
  // there rather than from where the clone was declared.
@@ -12064,25 +12260,10 @@ const startDragToCarryCopy = (event, {
12064
12260
  // place.
12065
12261
  await settleCloneBack(cloneWrapper, draggedElement);
12066
12262
  }
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);
12263
+ } else if (dropMeans === "land") {
12264
+ await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onLand(getItemId(draggedElement), getItemId(currentReleaseElement), syncCloneWithDropTarget));
12265
+ } else if (dropMeans === "reorder") {
12266
+ await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onReorder(getItemId(draggedElement), currentBeforeElement ? getItemId(currentBeforeElement) : null, syncCloneWithDropTarget));
12086
12267
  }
12087
12268
  draggedElement.removeAttribute("navi-drag-clone-source");
12088
12269
  cloneWrapper.remove();
@@ -12153,6 +12334,14 @@ const createDropHint = () => {
12153
12334
  div.innerHTML = dropHintTemplate.trim();
12154
12335
  return div.firstElementChild;
12155
12336
  };
12337
+ const createDropSurface = () => {
12338
+ const div = document.createElement("div");
12339
+ div.className = "navi_drop_surface";
12340
+ // Manual, like the copy it accompanies: it is opened and closed with the drag
12341
+ // and must survive an Escape or a click elsewhere.
12342
+ div.setAttribute("popover", "manual");
12343
+ return div;
12344
+ };
12156
12345
 
12157
12346
  /**
12158
12347
  * The copy leaves the screen the way it was thrown, and the caller says what that
@@ -12211,13 +12400,6 @@ const createDragClone = (element, pointerEvent) => {
12211
12400
  // scale(1.15) expands from where the user clicked, not the element center.
12212
12401
  // These offsets are element-relative so viewport coords are correct here.
12213
12402
  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
12403
  const elementClone = element.cloneNode(true);
12222
12404
  // A deep copy copies the ids too, and two elements answering to one id is a
12223
12405
  // document that lies: getElementById picks whichever comes first, an anchor
@@ -12235,12 +12417,15 @@ const createDragClone = (element, pointerEvent) => {
12235
12417
  elementClone.setAttribute("data-grabbed", "");
12236
12418
  elementClone.style.viewTransitionName = "navi-drag-clone";
12237
12419
  wrapper.appendChild(elementClone);
12238
- document.body.appendChild(wrapper);
12420
+ // Beside the thing it copies, so it stands where that thing stands: every
12421
+ // inherited value and every custom property the original reads, the copy reads
12422
+ // too, and a rule written for an item in this list finds the copy as well. The
12423
+ // top layer is what lets it stay there — a popover is painted above the page
12424
+ // whatever its depth in the tree, and being fixed keeps it out of the
12425
+ // scrollable overflow of the list it sits in.
12426
+ element.parentElement.appendChild(wrapper);
12239
12427
  return wrapper;
12240
12428
  };
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
12429
 
12245
12430
  const startDragToResizeGesture = (
12246
12431
  pointerdownEvent,
@@ -12392,15 +12577,6 @@ import.meta.css = [/* css */`
12392
12577
  [data-drag-travel*="y"] * {
12393
12578
  overscroll-behavior-y: contain !important;
12394
12579
  }
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
12580
  `, "@jsenv/dom/src/interaction/drag/drag_to_travel.js"];
12405
12581
 
12406
12582
  // How far a pointer goes before it is a travel rather than a click: below this
@@ -12476,29 +12652,6 @@ const axesLeftBy = (axes, fromElement, stopElement, attribute) => {
12476
12652
  return left;
12477
12653
  };
12478
12654
 
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
12655
  /**
12503
12656
  * A scroller between the pointer and the box it is in, with room left the way
12504
12657
  * the gesture goes: it gets the gesture, and nothing travels — dragging a row
@@ -12817,9 +12970,6 @@ const startDragToTravel = (pointerDownEvent, {
12817
12970
  };
12818
12971
  document.documentElement.setAttribute(WALKING_ATTRIBUTE, axis);
12819
12972
  }
12820
- // Whatever the press was taken for until now, it was taken for something
12821
- // else (see dropSelection).
12822
- dropSelection();
12823
12973
  const {
12824
12974
  axis
12825
12975
  } = travel;
@@ -12881,10 +13031,6 @@ const startDragToTravel = (pointerDownEvent, {
12881
13031
  return;
12882
13032
  }
12883
13033
  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
13034
  const {
12889
13035
  axis,
12890
13036
  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.11",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {