@jsenv/dom 0.17.17 → 0.17.19

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 +182 -42
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -8244,7 +8244,12 @@ const createDragGestureController = (options = {}) => {
8244
8244
  // metadata about interaction sources
8245
8245
  grabEvent: event,
8246
8246
  dragEvent: null,
8247
- releaseEvent: null
8247
+ releaseEvent: null,
8248
+ // The gesture ended without the hand ever letting go: the browser took
8249
+ // the touch to scroll with, another gesture took the pointer. Whoever
8250
+ // reads it must not commit anything — a release that was not asked for
8251
+ // says nothing about where things belong.
8252
+ cancelled: false
8248
8253
  };
8249
8254
  definePropertyAsReadOnly(gestureInfo, "name");
8250
8255
  definePropertyAsReadOnly(gestureInfo, "direction");
@@ -8556,9 +8561,13 @@ const createDragGestureController = (options = {}) => {
8556
8561
  };
8557
8562
  const release = ({
8558
8563
  event = new CustomEvent("programmatic"),
8564
+ cancelled = event.type === "pointercancel",
8559
8565
  releaseX = gestureInfo.dragX,
8560
8566
  releaseY = gestureInfo.dragY
8561
8567
  } = {}) => {
8568
+ // Written before the last drag is reported, so the callbacks reading the
8569
+ // end of the gesture all see the same one.
8570
+ gestureInfo.cancelled = cancelled;
8562
8571
  drag(releaseX, releaseY, {
8563
8572
  event,
8564
8573
  isRelease: true
@@ -8613,10 +8622,13 @@ const createDragGestureController = (options = {}) => {
8613
8622
  event: dragEvent
8614
8623
  });
8615
8624
  };
8616
- const releaseViaPointer = mouseupEvent => {
8617
- const [mouseReleaseX, mouseReleaseY] = mouseEventCoords(mouseupEvent);
8625
+ const releaseViaPointer = (pointerEvent, {
8626
+ cancelled
8627
+ } = {}) => {
8628
+ const [mouseReleaseX, mouseReleaseY] = mouseEventCoords(pointerEvent);
8618
8629
  dragGesture.release({
8619
- event: mouseupEvent,
8630
+ event: pointerEvent,
8631
+ cancelled,
8620
8632
  releaseX: mouseReleaseX,
8621
8633
  releaseY: mouseReleaseY
8622
8634
  });
@@ -8651,48 +8663,70 @@ const createDragGestureController = (options = {}) => {
8651
8663
  document.removeEventListener("selectstart", preventSelectStart);
8652
8664
  });
8653
8665
  /*
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
8666
+ * Refusing every selection also refuses the ones a press is entitled to
8667
+ * make: a double click selects the word under it, a triple click the
8668
+ * paragraph around it, and both are over before the pointer has gone
8669
+ * anywhere. They are made here instead, spelled out (see
8670
+ * selectWordAtPoint, selectParagraphAtPoint) rather than left to a browser
8658
8671
  * heuristic that cannot tell a drag from a click.
8659
8672
  *
8673
+ * Read from `click` rather than `dblclick`, because a triple click has no
8674
+ * event of its own: what tells the clicks apart is `detail`, the count the
8675
+ * browser keeps of how many presses landed in the same place in a row — 2
8676
+ * for a word, 3 and beyond for a paragraph (a fourth click keeps the
8677
+ * paragraph, the way the browser does).
8678
+ *
8660
8679
  * 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.
8680
+ * already over when the second click completes: click comes after mouseup,
8681
+ * the gesture ends at pointerup. Kept installed after it fires, since the
8682
+ * click that selects a word is also the one the next click turns into a
8683
+ * paragraph; it goes when the next press installs its own — a listener
8684
+ * waiting for a click that never comes costs nothing until then.
8665
8685
  */
8666
- removePendingDoubleClickListener();
8667
- const onDoubleClick = dblclickEvent => {
8668
- removePendingDoubleClickListener = NOOP;
8686
+ removePendingMultiClickListener();
8687
+ const onClick = clickEvent => {
8688
+ const clickCount = clickEvent.detail;
8689
+ if (clickCount < 2) {
8690
+ return;
8691
+ }
8669
8692
  // A drag that happened is a gesture, not a click: the second press of a
8670
8693
  // double click can be the one that drags, and what it drags must not end
8671
8694
  // up selected too.
8672
8695
  if (dragGesture.gestureInfo.started) {
8673
8696
  return;
8674
8697
  }
8698
+ // Only the clicks that continue THIS press: the listener outlives the
8699
+ // gesture and the page keeps being clicked elsewhere, where the browser
8700
+ // is doing its own selecting — a second opinion there would only fight
8701
+ // it.
8702
+ const clickTarget = clickEvent.target;
8703
+ if (clickTarget !== grabEvent.target && !clickTarget.contains(grabEvent.target)) {
8704
+ return;
8705
+ }
8675
8706
  // Text the page says is not selectable stays not selectable: a
8676
8707
  // programmatic selection goes through `user-select: none` in every
8677
8708
  // engine — it is a rule about what the USER may start, and the browser
8678
8709
  // does not read it back when asked directly. Read here so that doing the
8679
8710
  // browser's work does not also undo what the page asked of it.
8680
- if (!isSelectable(dblclickEvent.target)) {
8711
+ if (!isSelectable(clickEvent.target)) {
8712
+ return;
8713
+ }
8714
+ if (clickCount === 2) {
8715
+ selectWordAtPoint(clickEvent.clientX, clickEvent.clientY);
8681
8716
  return;
8682
8717
  }
8683
- selectWordAtPoint(dblclickEvent.clientX, dblclickEvent.clientY);
8718
+ selectParagraphAtPoint(clickEvent.clientX, clickEvent.clientY);
8684
8719
  };
8685
- document.addEventListener("dblclick", onDoubleClick, {
8686
- once: true
8687
- });
8688
- removePendingDoubleClickListener = () => {
8689
- removePendingDoubleClickListener = NOOP;
8690
- document.removeEventListener("dblclick", onDoubleClick);
8720
+ document.addEventListener("click", onClick);
8721
+ removePendingMultiClickListener = () => {
8722
+ removePendingMultiClickListener = NOOP;
8723
+ document.removeEventListener("click", onClick);
8691
8724
  };
8692
8725
  const cleanup = initializer({
8693
8726
  onMove: dragViaPointer,
8694
8727
  onRelease: releaseViaPointer,
8695
- gestureInfo: dragGesture.gestureInfo
8728
+ gestureInfo: dragGesture.gestureInfo,
8729
+ dragGesture
8696
8730
  });
8697
8731
  dragGesture.addReleaseCallback(() => {
8698
8732
  cleanup();
@@ -8704,7 +8738,8 @@ const createDragGestureController = (options = {}) => {
8704
8738
  return initDragByPointer(grabEvent, options, ({
8705
8739
  onMove,
8706
8740
  onRelease,
8707
- gestureInfo
8741
+ gestureInfo,
8742
+ dragGesture
8708
8743
  }) => {
8709
8744
  // Captured on something that will still be there at the end of the
8710
8745
  // gesture: the browser releases the capture when its element leaves the
@@ -8712,7 +8747,21 @@ const createDragGestureController = (options = {}) => {
8712
8747
  // finger would lose the pointer at its first move. Callers whose target
8713
8748
  // is stable have nothing to say and keep it.
8714
8749
  const target = options?.pointerCaptureElement || grabEvent.target;
8715
- target.setPointerCapture(grabEvent.pointerId);
8750
+ // The capture is ONE per pointer for the whole document: taking it is
8751
+ // taking it away from whoever had it, who is then told the exact same
8752
+ // thing it is told when its own gesture ends. So it is taken when this
8753
+ // gesture is established and not a moment earlier — for most callers
8754
+ // that is the grab itself (the intent was settled before, by a handle
8755
+ // or a long press), and for a caller that is still deciding what the
8756
+ // press means, it is whenever it says so (see capturePointer).
8757
+ let captured = false;
8758
+ dragGesture.capturePointer = () => {
8759
+ captured = true;
8760
+ target.setPointerCapture(grabEvent.pointerId);
8761
+ };
8762
+ if (!options?.pointerCaptureDeferred) {
8763
+ dragGesture.capturePointer();
8764
+ }
8716
8765
  /*
8717
8766
  * A touchmove left alone is the browser deciding the touch belongs to
8718
8767
  * it: it takes it to scroll with, and a touch it has taken is a pointer
@@ -8746,7 +8795,7 @@ const createDragGestureController = (options = {}) => {
8746
8795
  grabTarget.addEventListener("touchmove", preventTouchScroll, {
8747
8796
  passive: false
8748
8797
  });
8749
- // Only OUR capture ending means this gesture is over:
8798
+ // Only OUR capture ending is this gesture's business:
8750
8799
  // lostpointercapture bubbles, so a descendant giving up its own capture
8751
8800
  // walks straight into this listener. That is not a rare shape — it is
8752
8801
  // exactly what happens when a gesture hands over to another one (a
@@ -8754,16 +8803,37 @@ const createDragGestureController = (options = {}) => {
8754
8803
  // the pressed element, while the real one is being held on a container
8755
8804
  // above it), and taken as our own it kills the new gesture one
8756
8805
  // millisecond after it started.
8806
+ //
8807
+ // And when it IS ours, it is a loss, never an end: the ends a gesture
8808
+ // has are the pointer going up and the pointer being cancelled, both
8809
+ // listened for below. A capture that goes while the pointer is still
8810
+ // down was taken — by another gesture, or by the element it was held
8811
+ // on leaving the document — and what was being carried must go back
8812
+ // rather than land wherever the hand happened to be.
8757
8813
  const onCaptureLost = pointerEvent => {
8758
- if (pointerEvent.target !== target) {
8814
+ if (!captured || pointerEvent.target !== target) {
8759
8815
  return;
8760
8816
  }
8761
- onRelease(pointerEvent);
8817
+ onRelease(pointerEvent, {
8818
+ cancelled: true
8819
+ });
8762
8820
  };
8763
8821
  target.addEventListener("lostpointercapture", onCaptureLost);
8764
8822
  target.addEventListener("pointercancel", onRelease);
8765
- target.addEventListener("pointermove", onMove);
8766
8823
  target.addEventListener("pointerup", onRelease);
8824
+ // Read from the window rather than from the element while the pointer
8825
+ // is not this gesture's: without a capture a move is delivered to
8826
+ // whatever is under the pointer, and a hand that has left the element
8827
+ // is exactly the hand this gesture is trying to make sense of. The
8828
+ // capture phase reaches the window before anything else, captured or
8829
+ // not, so there is one place to read whichever way the gesture ends up.
8830
+ const onPointerMove = pointerEvent => {
8831
+ if (pointerEvent.pointerId !== grabEvent.pointerId) {
8832
+ return;
8833
+ }
8834
+ onMove(pointerEvent);
8835
+ };
8836
+ window.addEventListener("pointermove", onPointerMove, true);
8767
8837
  // The end of the pointer is also listened for on the window, because
8768
8838
  // the end is the one event a gesture cannot afford to miss and the
8769
8839
  // element it is captured on is not always on its way: a pointer can
@@ -8793,15 +8863,17 @@ const createDragGestureController = (options = {}) => {
8793
8863
  grabTarget.removeEventListener("touchmove", preventTouchScroll);
8794
8864
  target.removeEventListener("lostpointercapture", onCaptureLost);
8795
8865
  target.removeEventListener("pointercancel", onRelease);
8796
- target.removeEventListener("pointermove", onMove);
8866
+ window.removeEventListener("pointermove", onPointerMove, true);
8797
8867
  target.removeEventListener("pointerup", onRelease);
8798
8868
  window.removeEventListener("pointerup", onPointerEnd, true);
8799
8869
  window.removeEventListener("pointercancel", onPointerEnd, true);
8800
- // Asked for only while there is something to give back: a pointer
8801
- // that is up no longer exists, the browser has already dropped the
8870
+ // Only what this gesture took, and only while there is something to
8871
+ // give back: a capture on that element may be someone else's (two
8872
+ // gestures reading the same press hold the same node), and a pointer
8873
+ // that is up no longer exists — the browser has already dropped the
8802
8874
  // capture with it, and asking again throws ("No active pointer with
8803
- // the given id is found") on the most ordinary release there is.
8804
- if (target.hasPointerCapture(grabEvent.pointerId)) {
8875
+ // the given id is found") on the most ordinary release there is.
8876
+ if (captured && target.hasPointerCapture(grabEvent.pointerId)) {
8805
8877
  target.releasePointerCapture(grabEvent.pointerId);
8806
8878
  }
8807
8879
  };
@@ -8882,7 +8954,7 @@ const definePropertyAsReadOnly = (object, propertyName) => {
8882
8954
  });
8883
8955
  };
8884
8956
  const NOOP = () => {};
8885
- let removePendingDoubleClickListener = NOOP;
8957
+ let removePendingMultiClickListener = NOOP;
8886
8958
 
8887
8959
  // What a double click selects when the browser is allowed to do it itself: the
8888
8960
  // word around the caret the click lands on.
@@ -8902,6 +8974,46 @@ const selectWordAtPoint = (x, y) => {
8902
8974
  selection.modify("extend", "forward", "word");
8903
8975
  }
8904
8976
  };
8977
+
8978
+ // What a triple click selects when the browser is allowed to do it itself: the
8979
+ // paragraph around the caret the click lands on — in the DOM, the contents of
8980
+ // the closest block the caret sits in.
8981
+ //
8982
+ // Drawn from the box tree rather than from `selection.modify("extend",
8983
+ // "forward", "paragraph")`: "paragraph" is a granularity Gecko never
8984
+ // implemented (it stops at word and line), so the modify route would select a
8985
+ // line in Firefox and a paragraph in Chrome.
8986
+ const selectParagraphAtPoint = (x, y) => {
8987
+ const caretRange = createCaretRange(x, y);
8988
+ if (!caretRange) {
8989
+ return;
8990
+ }
8991
+ const block = getClosestBlock(caretRange.startContainer);
8992
+ if (!block) {
8993
+ return;
8994
+ }
8995
+ const selection = window.getSelection();
8996
+ selection.removeAllRanges();
8997
+ const paragraphRange = document.createRange();
8998
+ paragraphRange.selectNodeContents(block);
8999
+ selection.addRange(paragraphRange);
9000
+ };
9001
+ const getClosestBlock = node => {
9002
+ let element = node.nodeType === 1 ? node : node.parentElement;
9003
+ while (element) {
9004
+ const {
9005
+ display
9006
+ } = window.getComputedStyle(element);
9007
+ // Everything that is not laid out as a line inside another line: the first
9008
+ // ancestor that breaks the flow is the one whose text reads as a paragraph
9009
+ // of its own.
9010
+ if (!display.startsWith("inline") && display !== "contents") {
9011
+ return element;
9012
+ }
9013
+ element = element.parentElement;
9014
+ }
9015
+ return null;
9016
+ };
8905
9017
  const createCaretRange = (x, y) => {
8906
9018
  if (document.caretPositionFromPoint) {
8907
9019
  const caretPosition = document.caretPositionFromPoint(x, y);
@@ -9162,7 +9274,13 @@ const dragAfterDistance = (grabEvent, dragGestureInitializer, threshold) => {
9162
9274
  }
9163
9275
  });
9164
9276
  const significantDragGesture = significantDragGestureController.grabViaPointer(grabEvent, {
9165
- element: grabEvent.target
9277
+ element: grabEvent.target,
9278
+ // This one owns nothing: it measures a distance to find out whether there
9279
+ // is a gesture at all, and it is over the moment there is. Taking the
9280
+ // pointer for that would take it from whoever is already holding this same
9281
+ // element for a gesture of their own — and giving it back at the threshold
9282
+ // would tell them theirs is over.
9283
+ pointerCaptureDeferred: true
9166
9284
  });
9167
9285
  };
9168
9286
  const dragAfterLongPress = (grabEvent, dragGestureInitializer, {
@@ -12091,6 +12209,12 @@ const resolveDropMeaning = ({
12091
12209
  tossDistance = TOSS_DISTANCE_TO_COMMIT,
12092
12210
  tossSpeed = TOSS_SPEED_TO_COMMIT
12093
12211
  }) => {
12212
+ if (gestureInfo.cancelled) {
12213
+ // Nobody let go of anything: the gesture was taken away mid-air (the
12214
+ // pointer cancelled, another gesture taking it). Where the thing happened
12215
+ // to be at that moment is not a place it was put.
12216
+ return "cancel";
12217
+ }
12094
12218
  if (canToss) {
12095
12219
  const {
12096
12220
  xDelta,
@@ -12884,8 +13008,10 @@ const DRAG_RESISTANCE = 0.3;
12884
13008
 
12885
13009
  // What a drag must not start on: something that reads the pointer itself. A
12886
13010
  // button or a link is not in the list — dragging from one travels, and the
12887
- // click it would have made is swallowed on the way out.
12888
- const DRAG_EXCLUDED_SELECTOR = ["input", "textarea", "select", '[contenteditable=""]', '[contenteditable="true"]', "[data-no-drag-travel]"].join(",");
13011
+ // click it would have made is swallowed on the way out. A drag source is: it
13012
+ // answers the same press, and a travel starting there takes the pointer capture
13013
+ // away from a gesture already carrying something.
13014
+ const DRAG_EXCLUDED_SELECTOR = ["input", "textarea", "select", '[contenteditable=""]', '[contenteditable="true"]', "[data-drag-source]", "[data-drag-handle]", "[data-no-drag-travel]"].join(",");
12889
13015
 
12890
13016
  // Which axes a box travels on, one attribute per gesture, said in the DOM by
12891
13017
  // whoever owns the box: it is what a box ABOVE another reads to know the
@@ -13278,6 +13404,9 @@ const startDragToTravel = (pointerDownEvent, {
13278
13404
  pulled: started.slack || 0
13279
13405
  };
13280
13406
  document.documentElement.setAttribute(WALKING_ATTRIBUTE, axis);
13407
+ // The travel exists: from here the pointer is this box's, and it is
13408
+ // followed wherever it goes.
13409
+ dragGesture.capturePointer();
13281
13410
  }
13282
13411
  const {
13283
13412
  axis
@@ -13349,9 +13478,12 @@ const startDragToTravel = (pointerDownEvent, {
13349
13478
  const towardsSomething = pulled > 0 ? travel.travelBack : travel.travelOn;
13350
13479
  const velocity = axis === "x" ? gestureInfo.velocityX : gestureInfo.velocityY;
13351
13480
  // A gesture taken away rather than let go of (the browser scrolling
13352
- // something else, a call coming in) said nothing: things go back.
13481
+ // something else, a call coming in, another gesture taking the pointer)
13482
+ // said nothing: things go back.
13353
13483
  const releaseEvent = gestureInfo.releaseEvent || gestureInfo.dragEvent;
13354
- const cancelled = releaseEvent?.type === "pointercancel";
13484
+ const {
13485
+ cancelled
13486
+ } = gestureInfo;
13355
13487
  onEnd({
13356
13488
  axis,
13357
13489
  pulled,
@@ -13382,7 +13514,15 @@ const startDragToTravel = (pointerDownEvent, {
13382
13514
  // gesture may take that away (a page that travels navigates, and the
13383
13515
  // router unmounts the page being left), and a capture whose element
13384
13516
  // leaves the document is a capture the browser drops.
13385
- pointerCaptureElement: element
13517
+ pointerCaptureElement: element,
13518
+ // A travel is established in two steps, and the pointer is only owned
13519
+ // after the second: the distance below says the press is not a click, and
13520
+ // the first move says which axis it leans on — which this box may not
13521
+ // walk, or the caller may refuse. Taken at the first step, the capture
13522
+ // would be taken away from whoever else is reading the same press for
13523
+ // gestures that give themselves up one event later. It is claimed once
13524
+ // the travel exists, in onDrag below.
13525
+ pointerCaptureDeferred: true
13386
13526
  });
13387
13527
  return dragGesture;
13388
13528
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.17",
3
+ "version": "0.17.19",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {