@jsenv/dom 0.17.16 → 0.17.18

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 +105 -20
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -8651,43 +8651,64 @@ const createDragGestureController = (options = {}) => {
8651
8651
  document.removeEventListener("selectstart", preventSelectStart);
8652
8652
  });
8653
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
8654
+ * Refusing every selection also refuses the ones a press is entitled to
8655
+ * make: a double click selects the word under it, a triple click the
8656
+ * paragraph around it, and both are over before the pointer has gone
8657
+ * anywhere. They are made here instead, spelled out (see
8658
+ * selectWordAtPoint, selectParagraphAtPoint) rather than left to a browser
8658
8659
  * heuristic that cannot tell a drag from a click.
8659
8660
  *
8661
+ * Read from `click` rather than `dblclick`, because a triple click has no
8662
+ * event of its own: what tells the clicks apart is `detail`, the count the
8663
+ * browser keeps of how many presses landed in the same place in a row — 2
8664
+ * for a word, 3 and beyond for a paragraph (a fourth click keeps the
8665
+ * paragraph, the way the browser does).
8666
+ *
8660
8667
  * 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.
8668
+ * already over when the second click completes: click comes after mouseup,
8669
+ * the gesture ends at pointerup. Kept installed after it fires, since the
8670
+ * click that selects a word is also the one the next click turns into a
8671
+ * paragraph; it goes when the next press installs its own — a listener
8672
+ * waiting for a click that never comes costs nothing until then.
8665
8673
  */
8666
- removePendingDoubleClickListener();
8667
- const onDoubleClick = dblclickEvent => {
8668
- removePendingDoubleClickListener = NOOP;
8674
+ removePendingMultiClickListener();
8675
+ const onClick = clickEvent => {
8676
+ const clickCount = clickEvent.detail;
8677
+ if (clickCount < 2) {
8678
+ return;
8679
+ }
8669
8680
  // A drag that happened is a gesture, not a click: the second press of a
8670
8681
  // double click can be the one that drags, and what it drags must not end
8671
8682
  // up selected too.
8672
8683
  if (dragGesture.gestureInfo.started) {
8673
8684
  return;
8674
8685
  }
8686
+ // Only the clicks that continue THIS press: the listener outlives the
8687
+ // gesture and the page keeps being clicked elsewhere, where the browser
8688
+ // is doing its own selecting — a second opinion there would only fight
8689
+ // it.
8690
+ const clickTarget = clickEvent.target;
8691
+ if (clickTarget !== grabEvent.target && !clickTarget.contains(grabEvent.target)) {
8692
+ return;
8693
+ }
8675
8694
  // Text the page says is not selectable stays not selectable: a
8676
8695
  // programmatic selection goes through `user-select: none` in every
8677
8696
  // engine — it is a rule about what the USER may start, and the browser
8678
8697
  // does not read it back when asked directly. Read here so that doing the
8679
8698
  // browser's work does not also undo what the page asked of it.
8680
- if (!isSelectable(dblclickEvent.target)) {
8699
+ if (!isSelectable(clickEvent.target)) {
8700
+ return;
8701
+ }
8702
+ if (clickCount === 2) {
8703
+ selectWordAtPoint(clickEvent.clientX, clickEvent.clientY);
8681
8704
  return;
8682
8705
  }
8683
- selectWordAtPoint(dblclickEvent.clientX, dblclickEvent.clientY);
8706
+ selectParagraphAtPoint(clickEvent.clientX, clickEvent.clientY);
8684
8707
  };
8685
- document.addEventListener("dblclick", onDoubleClick, {
8686
- once: true
8687
- });
8688
- removePendingDoubleClickListener = () => {
8689
- removePendingDoubleClickListener = NOOP;
8690
- document.removeEventListener("dblclick", onDoubleClick);
8708
+ document.addEventListener("click", onClick);
8709
+ removePendingMultiClickListener = () => {
8710
+ removePendingMultiClickListener = NOOP;
8711
+ document.removeEventListener("click", onClick);
8691
8712
  };
8692
8713
  const cleanup = initializer({
8693
8714
  onMove: dragViaPointer,
@@ -8882,7 +8903,7 @@ const definePropertyAsReadOnly = (object, propertyName) => {
8882
8903
  });
8883
8904
  };
8884
8905
  const NOOP = () => {};
8885
- let removePendingDoubleClickListener = NOOP;
8906
+ let removePendingMultiClickListener = NOOP;
8886
8907
 
8887
8908
  // What a double click selects when the browser is allowed to do it itself: the
8888
8909
  // word around the caret the click lands on.
@@ -8902,6 +8923,46 @@ const selectWordAtPoint = (x, y) => {
8902
8923
  selection.modify("extend", "forward", "word");
8903
8924
  }
8904
8925
  };
8926
+
8927
+ // What a triple click selects when the browser is allowed to do it itself: the
8928
+ // paragraph around the caret the click lands on — in the DOM, the contents of
8929
+ // the closest block the caret sits in.
8930
+ //
8931
+ // Drawn from the box tree rather than from `selection.modify("extend",
8932
+ // "forward", "paragraph")`: "paragraph" is a granularity Gecko never
8933
+ // implemented (it stops at word and line), so the modify route would select a
8934
+ // line in Firefox and a paragraph in Chrome.
8935
+ const selectParagraphAtPoint = (x, y) => {
8936
+ const caretRange = createCaretRange(x, y);
8937
+ if (!caretRange) {
8938
+ return;
8939
+ }
8940
+ const block = getClosestBlock(caretRange.startContainer);
8941
+ if (!block) {
8942
+ return;
8943
+ }
8944
+ const selection = window.getSelection();
8945
+ selection.removeAllRanges();
8946
+ const paragraphRange = document.createRange();
8947
+ paragraphRange.selectNodeContents(block);
8948
+ selection.addRange(paragraphRange);
8949
+ };
8950
+ const getClosestBlock = node => {
8951
+ let element = node.nodeType === 1 ? node : node.parentElement;
8952
+ while (element) {
8953
+ const {
8954
+ display
8955
+ } = window.getComputedStyle(element);
8956
+ // Everything that is not laid out as a line inside another line: the first
8957
+ // ancestor that breaks the flow is the one whose text reads as a paragraph
8958
+ // of its own.
8959
+ if (!display.startsWith("inline") && display !== "contents") {
8960
+ return element;
8961
+ }
8962
+ element = element.parentElement;
8963
+ }
8964
+ return null;
8965
+ };
8905
8966
  const createCaretRange = (x, y) => {
8906
8967
  if (document.caretPositionFromPoint) {
8907
8968
  const caretPosition = document.caretPositionFromPoint(x, y);
@@ -12894,6 +12955,16 @@ const DRAG_EXCLUDED_SELECTOR = ["input", "textarea", "select", '[contenteditable
12894
12955
  const DRAG_AXES_ATTRIBUTE = "data-travel-by-drag";
12895
12956
  const WHEEL_AXES_ATTRIBUTE = "data-travel-by-wheel";
12896
12957
 
12958
+ // A surface the browser paints in the top layer: it is still a DOM descendant
12959
+ // of whatever it was written in, and it is nowhere near it on screen — it
12960
+ // covers everything. So a gesture that happened on it is not the gesture of any
12961
+ // box it merely sits on top of, and every walk up the tree from the pointer
12962
+ // ends here: the boxes above are behind, and behind is not under the finger.
12963
+ const TOP_LAYER_SELECTOR = [":popover-open", "dialog:modal", ":fullscreen"].join(",");
12964
+ const isTopLayer = element => {
12965
+ return element.matches(TOP_LAYER_SELECTOR);
12966
+ };
12967
+
12897
12968
  /**
12898
12969
  * What is left for this box of the axes it travels, once the boxes it CONTAINS
12899
12970
  * have taken theirs: a row of slides inside a page that walks between pages, a
@@ -12908,6 +12979,10 @@ const WHEEL_AXES_ATTRIBUTE = "data-travel-by-wheel";
12908
12979
  * the browser for the pointer LAST, which is the outermost box — the wrong one,
12909
12980
  * and past that point the inner one stops being told anything. So the box that
12910
12981
  * does not own the gesture must never ask for it.
12982
+ *
12983
+ * A box lifted into the top layer on the way up takes everything: a popover or a
12984
+ * modal dialog is written inside a slide and painted over the whole screen, so
12985
+ * the slides are nowhere near the finger and none of the axes are left.
12911
12986
  */
12912
12987
  const axesLeftBy = (axes, fromElement, stopElement, attribute) => {
12913
12988
  if (!stopElement.contains(fromElement)) {
@@ -12919,6 +12994,11 @@ const axesLeftBy = (axes, fromElement, stopElement, attribute) => {
12919
12994
  let left = axes;
12920
12995
  let element = fromElement;
12921
12996
  while (element && element !== stopElement && element.nodeType === 1) {
12997
+ if (isTopLayer(element)) {
12998
+ // The gesture happened on a surface painted over this box, not in it:
12999
+ // there is nothing left of it here, whatever axes are still unclaimed.
13000
+ return "";
13001
+ }
12922
13002
  const taken = element.getAttribute(attribute);
12923
13003
  if (taken) {
12924
13004
  let rest = "";
@@ -12946,6 +13026,11 @@ const axesLeftBy = (axes, fromElement, stopElement, attribute) => {
12946
13026
  const scrollRoomTowards = (fromElement, stopElement, axis, sign) => {
12947
13027
  let element = fromElement;
12948
13028
  while (element && element !== stopElement && element.nodeType === 1) {
13029
+ if (isTopLayer(element)) {
13030
+ // A scroller above a top-layer surface is painted behind it: whatever
13031
+ // room it has left is not room the finger is asking for.
13032
+ return false;
13033
+ }
12949
13034
  const size = axis === "x" ? element.clientWidth : element.clientHeight;
12950
13035
  const scrollSize = axis === "x" ? element.scrollWidth : element.scrollHeight;
12951
13036
  if (scrollSize > size + 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.16",
3
+ "version": "0.17.18",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {