@jsenv/dom 0.17.27 → 0.17.29

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 +103 -35
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -2702,6 +2702,12 @@ const normalizeStyle = (
2702
2702
 
2703
2703
  if (colorPropertySet.has(propertyName)) {
2704
2704
  if (typeof value === "string") {
2705
+ if (context === "css") {
2706
+ // A string is already what the DOM takes, and it takes more of them
2707
+ // (system colors, relative color syntax) than this parser knows;
2708
+ // parsing it to rgba and back would only cost.
2709
+ return value;
2710
+ }
2705
2711
  if (isCSSKeyword(value)) {
2706
2712
  return value;
2707
2713
  }
@@ -9478,7 +9484,9 @@ const css$4 = /* css */`
9478
9484
  touch-action: pinch-zoom;
9479
9485
  }
9480
9486
  [data-drag-ignore],
9481
- [data-own-target] {
9487
+ [data-own-target],
9488
+ [data-drag-source] [popover],
9489
+ [data-drag-source] dialog {
9482
9490
  -webkit-touch-callout: default;
9483
9491
  touch-action: auto;
9484
9492
  }
@@ -11920,7 +11928,8 @@ const css$1 = /* css */`
11920
11928
  whoever puts the drag there asks for the hand when a grab really is the
11921
11929
  first thing the element offers.
11922
11930
  An opted-out area keeps both its cursor and its selection, and never starts
11923
- a drag (see the check in startDragTo).
11931
+ a drag (see the check in startDragTo); a popover or a dialog anchored in a
11932
+ source is one without having to say so.
11924
11933
  Controls inside a source keep their own cursor: cursor is inherited, and
11925
11934
  anything setting its own (a button's pointer) wins on itself.
11926
11935
  Only the resting cursor is set here: what it becomes once a drag is under
@@ -11933,7 +11942,9 @@ const css$1 = /* css */`
11933
11942
  cursor: default;
11934
11943
  }
11935
11944
  [data-drag-ignore],
11936
- [data-own-target] {
11945
+ [data-own-target],
11946
+ [data-drag-source] [popover],
11947
+ [data-drag-source] dialog {
11937
11948
  cursor: auto;
11938
11949
  }
11939
11950
 
@@ -12033,8 +12044,12 @@ import.meta.css = [css$1, "@jsenv/dom/src/interaction/drag/drag_to.js"];
12033
12044
  // control that reads the pointer itself. `data-own-target` is the same fact said
12034
12045
  // once for every gesture there is: an element declaring that a press landing on
12035
12046
  // it is aimed AT it, whatever it happens to sit inside (see also
12036
- // DRAG_EXCLUDED_SELECTOR in drag_to_travel.js).
12037
- const DRAG_IGNORED_SELECTOR = "[data-drag-ignore],[data-own-target]";
12047
+ // DRAG_EXCLUDED_SELECTOR in drag_to_travel.js). A popover or a dialog says it
12048
+ // without being asked: it is a layer OVER the surface, so a press in it is aimed
12049
+ // at the layer — yet it stays a descendant of whatever it is anchored in (a
12050
+ // callout next to a button, in the card that carries both), and the press
12051
+ // bubbles through the surface as if it had landed on it.
12052
+ const DRAG_IGNORED_SELECTOR = "[data-drag-ignore],[data-own-target],[popover],dialog";
12038
12053
 
12039
12054
  /**
12040
12055
  * Starts a drag-to-reorder interaction on a list item.
@@ -12534,8 +12549,8 @@ const startDragTo = (event, effects, {
12534
12549
  ...options
12535
12550
  } = {}) => {
12536
12551
  // An area that opted out of dragging (a text one wants to select, a control that
12537
- // owns the gesture): the press there is none of our business.
12538
- if (event.target.closest && event.target.closest(DRAG_IGNORED_SELECTOR)) {
12552
+ // owns the gesture, a callout): the press there is none of our business.
12553
+ if (isPressIgnored(event.target, draggedElement)) {
12539
12554
  return undefined;
12540
12555
  }
12541
12556
  // A secondary button (right click and friends) is a context menu, not a grab.
@@ -12716,8 +12731,8 @@ const startDragToCarryCopy = (event, {
12716
12731
  ...options
12717
12732
  }) => {
12718
12733
  // An area that opted out of dragging (a text one wants to select, a control
12719
- // that owns the gesture): the press there is none of our business.
12720
- if (event.target.closest && event.target.closest(DRAG_IGNORED_SELECTOR)) {
12734
+ // that owns the gesture, a callout): the press there is none of our business.
12735
+ if (isPressIgnored(event.target, draggedElement)) {
12721
12736
  return undefined;
12722
12737
  }
12723
12738
  // A secondary button (right click and friends) is a context menu, not a grab.
@@ -13225,6 +13240,18 @@ const createDragClone = (element, pointerEvent) => {
13225
13240
  return wrapper;
13226
13241
  };
13227
13242
 
13243
+ // The nearest word about the press wins: an opted-out area INSIDE the dragged
13244
+ // element takes the press away from it, one AROUND it does not — a list
13245
+ // reordered inside a dialog, or a dialog carried by its own handle, is itself the
13246
+ // last thing said before the finger.
13247
+ const isPressIgnored = (target, draggedElement) => {
13248
+ if (!target.closest) {
13249
+ return false;
13250
+ }
13251
+ const ignored = target.closest(DRAG_IGNORED_SELECTOR);
13252
+ return Boolean(ignored) && !ignored.contains(draggedElement);
13253
+ };
13254
+
13228
13255
  const startDragToResizeGesture = (
13229
13256
  pointerdownEvent,
13230
13257
  { onDragStart, onDrag, onRelease, ...options },
@@ -13465,8 +13492,10 @@ const DRAG_RESISTANCE = 0.3;
13465
13492
  // place whose only purpose is to be taken hold of, from the first pixel. And so
13466
13493
  // is an OWN TARGET: an element saying a press landing on it is aimed at IT,
13467
13494
  // which is the same sentence said to every gesture at once rather than to this
13468
- // one (see DRAG_IGNORED_SELECTOR in drag_to.js).
13469
- const DRAG_EXCLUDED_SELECTOR = ["input", "textarea", "select", '[contenteditable=""]', '[contenteditable="true"]', "[data-drag-handle]", "[data-no-drag-travel]", "[data-own-target]"].join(",");
13495
+ // one (see DRAG_IGNORED_SELECTOR in drag_to.js). And so is a popover or a
13496
+ // dialog: a layer OVER the box, whose press only bubbles through the box because
13497
+ // the layer is anchored in it.
13498
+ const DRAG_EXCLUDED_SELECTOR = ["input", "textarea", "select", '[contenteditable=""]', '[contenteditable="true"]', "[data-drag-handle]", "[data-no-drag-travel]", "[data-own-target]", "[popover]", "dialog"].join(",");
13470
13499
 
13471
13500
  // Which axes a box travels on, one attribute per gesture, said in the DOM by
13472
13501
  // whoever owns the box: it is what a box ABOVE another reads to know the
@@ -13692,7 +13721,7 @@ const startDragToTravel = (pointerDownEvent, {
13692
13721
  onGiveUp = () => {}
13693
13722
  }) => {
13694
13723
  const target = pointerDownEvent.target;
13695
- if (!target.closest || target.closest(DRAG_EXCLUDED_SELECTOR)) {
13724
+ if (!target.closest || isPressExcluded(target, element)) {
13696
13725
  return null;
13697
13726
  }
13698
13727
  // A box between the finger and this one that travels the same way, and then
@@ -14153,7 +14182,7 @@ const watchWheelTravel = (element, {
14153
14182
  const {
14154
14183
  target
14155
14184
  } = wheelEvent;
14156
- if (target.closest && target.closest(DRAG_EXCLUDED_SELECTOR) || scrollRoomTowards(target, element, axis, sign) ||
14185
+ if (target.closest && isPressExcluded(target, element) || scrollRoomTowards(target, element, axis, sign) ||
14157
14186
  // …plus the third: a box below this one that travels on this axis. Its
14158
14187
  // watcher hears the same wheel event this one does — they all listen at
14159
14188
  // the document — so without this both step, and one push moves two
@@ -14244,6 +14273,14 @@ const watchWheelTravel = (element, {
14244
14273
  };
14245
14274
  };
14246
14275
 
14276
+ // The nearest word wins: what is excluded INSIDE the box takes the press away
14277
+ // from it, what is around the box does not — a docked dialog IS the box that
14278
+ // travels, and being a dialog is no reason for it to refuse its own press.
14279
+ const isPressExcluded = (target, element) => {
14280
+ const excluded = target.closest(DRAG_EXCLUDED_SELECTOR);
14281
+ return Boolean(excluded) && !excluded.contains(element);
14282
+ };
14283
+
14247
14284
  // Shared by navi's own use_displayed_layout_effect.js (rich "navi_displayed"
14248
14285
  // CustomEvent, open transitions only) and visible_rect.js (needs both
14249
14286
  // directions: hide when a container closes, recheck when it reopens) — the
@@ -19720,14 +19757,22 @@ const getMaxWidth = (
19720
19757
  * Summing their `width` values therefore over-counts the true line width.
19721
19758
  *
19722
19759
  * Instead we compute the bounding extent per line: track the minimum `left`
19723
- * and maximum `right` across all rects sharing the same rounded `top`, then
19724
- * use `right - left` as the line width. This is correct regardless of nesting
19760
+ * and maximum `right` across all rects of the same line, then use
19761
+ * `right - left` as the line width. This is correct regardless of nesting
19725
19762
  * depth and works well for regular inline text content.
19726
19763
  *
19727
- * Limitation: rects are grouped by `Math.round(r.top)`, so elements on the
19728
- * same visual line but with slightly different baselines (e.g. an icon taller
19729
- * than surrounding text) could be counted as separate lines. This is unlikely
19730
- * to matter in practice for normal text rendering.
19764
+ * ## Implementation note lines are found by vertical overlap
19765
+ *
19766
+ * Boxes sharing a line do not share a `top`: an inline icon sized to 1em and
19767
+ * sitting on the baseline, a superscript, an inline-block, all start a pixel
19768
+ * or a fraction of one away from the text beside them (and that fraction
19769
+ * moves with a sub-pixel scroll offset). Grouping rects by their rounded
19770
+ * `top` would count such a box as a line of its own, and the "longest line"
19771
+ * would come out as the text without it — short enough to make it wrap once
19772
+ * applied as a width. A rect therefore joins the line it overlaps vertically
19773
+ * by more than half the smaller of the two heights: boxes on one line share
19774
+ * most of their height, while the text rects of two consecutive lines overlap
19775
+ * by a sliver at most (a line-height below the font's content height).
19731
19776
  *
19732
19777
  * Limitation: `range.getClientRects()` returns rects for text nodes and inline
19733
19778
  * boxes as laid out in the flow, ignoring any `overflow: hidden` or `max-width`
@@ -19745,31 +19790,41 @@ const measureLongestVisualLineWidth = (el) => {
19745
19790
  const range = document.createRange();
19746
19791
  range.selectNodeContents(el);
19747
19792
 
19748
- const lineBoundsByTop = new Map();
19749
- for (const r of range.getClientRects()) {
19750
- if (r.width === 0) {
19793
+ const lines = [];
19794
+ for (const rect of range.getClientRects()) {
19795
+ if (rect.width === 0) {
19751
19796
  continue;
19752
19797
  }
19753
- const top = Math.round(r.top);
19754
- const existing = lineBoundsByTop.get(top);
19755
- if (existing === undefined) {
19756
- lineBoundsByTop.set(top, { left: r.left, right: r.right });
19757
- } else {
19758
- if (r.left < existing.left) {
19759
- existing.left = r.left;
19760
- }
19761
- if (r.right > existing.right) {
19762
- existing.right = r.right;
19763
- }
19798
+ const line = lines.find((candidate) => sharesLine(candidate, rect));
19799
+ if (line === undefined) {
19800
+ lines.push({
19801
+ top: rect.top,
19802
+ bottom: rect.bottom,
19803
+ left: rect.left,
19804
+ right: rect.right,
19805
+ });
19806
+ continue;
19807
+ }
19808
+ if (rect.top < line.top) {
19809
+ line.top = rect.top;
19810
+ }
19811
+ if (rect.bottom > line.bottom) {
19812
+ line.bottom = rect.bottom;
19813
+ }
19814
+ if (rect.left < line.left) {
19815
+ line.left = rect.left;
19816
+ }
19817
+ if (rect.right > line.right) {
19818
+ line.right = rect.right;
19764
19819
  }
19765
19820
  }
19766
19821
 
19767
- if (lineBoundsByTop.size <= 1) {
19822
+ if (lines.length <= 1) {
19768
19823
  return null;
19769
19824
  }
19770
19825
 
19771
19826
  let longestLineWidth = 0;
19772
- for (const { left, right } of lineBoundsByTop.values()) {
19827
+ for (const { left, right } of lines) {
19773
19828
  const w = right - left;
19774
19829
  if (w > longestLineWidth) {
19775
19830
  longestLineWidth = w;
@@ -19778,6 +19833,19 @@ const measureLongestVisualLineWidth = (el) => {
19778
19833
  return longestLineWidth;
19779
19834
  };
19780
19835
 
19836
+ const sharesLine = (line, rect) => {
19837
+ const overlapTop = rect.top > line.top ? rect.top : line.top;
19838
+ const overlapBottom = rect.bottom < line.bottom ? rect.bottom : line.bottom;
19839
+ const overlap = overlapBottom - overlapTop;
19840
+ if (overlap <= 0) {
19841
+ return false;
19842
+ }
19843
+ const rectHeight = rect.bottom - rect.top;
19844
+ const lineHeight = line.bottom - line.top;
19845
+ const smallerHeight = rectHeight < lineHeight ? rectHeight : lineHeight;
19846
+ return overlap > smallerHeight / 2;
19847
+ };
19848
+
19781
19849
  // Measures the width of the widest row of direct children.
19782
19850
  // Uses children's bounding rects (which respect overflow:hidden / max-width)
19783
19851
  // rather than Range.getClientRects() which sees through clipping boundaries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.27",
3
+ "version": "0.17.29",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {