@jsenv/navi 0.29.29 → 0.29.30
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.
- package/dist/jsenv_navi.js +795 -31
- package/dist/jsenv_navi.js.map +21 -11
- package/dist/jsenv_navi_side_effects.js +0 -1
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +5 -1
- package/docs/control_group.md +95 -0
- package/docs/interactions.md +31 -1
- package/docs/z_index.md +40 -16
- package/package.json +5 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* using @jsenv/navi as intended.
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, coarsePointerSignal } from "./jsenv_navi_side_effects.js";
|
|
6
|
-
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel,
|
|
6
|
+
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
7
7
|
export { contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
8
8
|
import { signal, computed, effect, batch, useSignal } from "@preact/signals";
|
|
9
9
|
import { createContext, isValidElement, h, Fragment, render, toChildArray, options, cloneElement } from "preact";
|
|
@@ -11,8 +11,66 @@ import { useContext, useLayoutEffect, useCallback, useRef, useState, useEffect,
|
|
|
11
11
|
import { jsx, jsxs, Fragment as Fragment$1 } from "preact/jsx-runtime";
|
|
12
12
|
import { createValidity, parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, durationToISOString } from "@jsenv/validity";
|
|
13
13
|
export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
|
|
14
|
-
import { createPortal, Suspense, forwardRef } from "preact/compat";
|
|
15
14
|
import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
|
|
15
|
+
import { createPortal, Suspense, forwardRef } from "preact/compat";
|
|
16
|
+
|
|
17
|
+
installImportMetaCssBuild(import.meta);/**
|
|
18
|
+
* Every z-index navi can be seen competing on, in one place, ordered.
|
|
19
|
+
*
|
|
20
|
+
* The point is the overview: reading this file must be enough to know what
|
|
21
|
+
* paints over what, and adding a value here forces the question "against
|
|
22
|
+
* what?" to be answered before a number is written. These are not meant as a
|
|
23
|
+
* theming surface — override one only to get out of a conflict with an app
|
|
24
|
+
* that cannot move.
|
|
25
|
+
*
|
|
26
|
+
* What belongs here: anything whose stacking is decided against an element
|
|
27
|
+
* from ANOTHER component — a bar, a popup, a control raising itself above the
|
|
28
|
+
* neighbour it overlaps. A z-index that only orders a component's own parts
|
|
29
|
+
* (a shadow under its own content, a checkbox mark over its own box) stays a
|
|
30
|
+
* literal next to the rule that needs it: it says "above my sibling", nothing
|
|
31
|
+
* more, and routing it through a token would only dilute the overview.
|
|
32
|
+
*
|
|
33
|
+
* The bands are decades apart so a band can grow without reaching the next
|
|
34
|
+
* one, and so a value read in a devtools panel says which band it came from.
|
|
35
|
+
*
|
|
36
|
+
* Read docs/z_index.md first — DOM order and `isolation: isolate` come before
|
|
37
|
+
* any of these, and a number is the last resort, not the first tool.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const css$10 = /* css */`
|
|
41
|
+
@layer navi {
|
|
42
|
+
:root {
|
|
43
|
+
/* A control that overlaps its neighbours (the members of a Group share
|
|
44
|
+
one border along each seam) and has to paint in front of them while
|
|
45
|
+
the user is on it: its border color change, then its focus ring, which
|
|
46
|
+
a neighbour painted later would otherwise slice in half. Focus above
|
|
47
|
+
hover, since a focused member can sit next to a hovered one. */
|
|
48
|
+
--navi-z-index-control-hovered: 1;
|
|
49
|
+
--navi-z-index-control-focused: 2;
|
|
50
|
+
|
|
51
|
+
/* Kept stuck while something scrolls under it: a list header, the head
|
|
52
|
+
and foot of a side panel, a table's sticky cells. Above raised
|
|
53
|
+
controls — a control scrolling past must go under the header that
|
|
54
|
+
pins the column it belongs to, never over it. */
|
|
55
|
+
--navi-z-index-sticky: 10;
|
|
56
|
+
|
|
57
|
+
/* Pinned to the viewport, over the whole page: FixedBar. A decade of its
|
|
58
|
+
own and well above the sticky band, so that no sticky cell and no
|
|
59
|
+
hovered control can ever be seen crossing it. */
|
|
60
|
+
--navi-z-index-bar: 100;
|
|
61
|
+
|
|
62
|
+
/* Popups: Dialog/Popover with layer="local", their backdrop, and the
|
|
63
|
+
validation callouts. Above everything the page can produce, which is
|
|
64
|
+
the whole point of the gap — a popup never has to guess. Each opened
|
|
65
|
+
popup adds its stack order on top, so the last one opened wins.
|
|
66
|
+
Dialog/Popover with layer="top" use the browser top layer instead and
|
|
67
|
+
appear in no scale at all. */
|
|
68
|
+
--navi-z-index-popup: 1000;
|
|
69
|
+
--navi-z-index-callout: var(--navi-z-index-popup);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
import.meta.css = [css$10, "@jsenv/navi/src/navi_z_indexes.js"];
|
|
16
74
|
|
|
17
75
|
const addIntoArray = (array, ...valuesToAdd) => {
|
|
18
76
|
if (valuesToAdd.length === 1) {
|
|
@@ -7583,7 +7641,7 @@ const css$_ = /* css */`
|
|
|
7583
7641
|
--callout-background-color: white;
|
|
7584
7642
|
--callout-icon-color: black;
|
|
7585
7643
|
--callout-padding: 8px;
|
|
7586
|
-
--callout-z-index:
|
|
7644
|
+
--callout-z-index: var(--navi-z-index-callout);
|
|
7587
7645
|
}
|
|
7588
7646
|
}
|
|
7589
7647
|
|
|
@@ -11697,6 +11755,647 @@ const readNumberFromDom = (element, attribute, defaultValue) => {
|
|
|
11697
11755
|
return value;
|
|
11698
11756
|
};
|
|
11699
11757
|
|
|
11758
|
+
/**
|
|
11759
|
+
* The interactions the browser already has an event for.
|
|
11760
|
+
*
|
|
11761
|
+
* Nothing to detect: the name IS the event type, so this listens and says it
|
|
11762
|
+
* happened. Registered through the same door an application uses (see
|
|
11763
|
+
* interaction_registry.js) — navi has no private one.
|
|
11764
|
+
*
|
|
11765
|
+
* `contextmenu` is the only one that takes something away: the browser's own menu
|
|
11766
|
+
* would cover the answer to the request it is.
|
|
11767
|
+
*/
|
|
11768
|
+
|
|
11769
|
+
|
|
11770
|
+
const NATIVE_TYPE_SET = new Set([
|
|
11771
|
+
"mousedown",
|
|
11772
|
+
"mouseup",
|
|
11773
|
+
"click",
|
|
11774
|
+
"dblclick",
|
|
11775
|
+
"contextmenu",
|
|
11776
|
+
]);
|
|
11777
|
+
|
|
11778
|
+
defineInteractionDetector({
|
|
11779
|
+
name: "native",
|
|
11780
|
+
claims: (type) => NATIVE_TYPE_SET.has(type),
|
|
11781
|
+
setup: (element, trigger, { types }) => {
|
|
11782
|
+
const listeners = types.map((type) => {
|
|
11783
|
+
const listener = (nativeEvent) => {
|
|
11784
|
+
if (type === "contextmenu") {
|
|
11785
|
+
nativeEvent.preventDefault();
|
|
11786
|
+
}
|
|
11787
|
+
trigger(type, nativeEvent);
|
|
11788
|
+
};
|
|
11789
|
+
element.addEventListener(type, listener);
|
|
11790
|
+
return [type, listener];
|
|
11791
|
+
});
|
|
11792
|
+
return () => {
|
|
11793
|
+
for (const [type, listener] of listeners) {
|
|
11794
|
+
element.removeEventListener(type, listener);
|
|
11795
|
+
}
|
|
11796
|
+
};
|
|
11797
|
+
},
|
|
11798
|
+
});
|
|
11799
|
+
|
|
11800
|
+
installImportMetaCssBuild(import.meta);/**
|
|
11801
|
+
* What a press can turn out to be: a swipe, or a hold.
|
|
11802
|
+
*
|
|
11803
|
+
* One detector for both, because they dispute the SAME press and something has to
|
|
11804
|
+
* arbitrate them in one place — read apart, a hold that drifts three pixels both
|
|
11805
|
+
* opens the menu and starts putting the row away. A finger lands; it leaves
|
|
11806
|
+
* sideways (a swipe), it stays still (a hold), it lifts at once (a click, which is
|
|
11807
|
+
* not this detector's business).
|
|
11808
|
+
*
|
|
11809
|
+
* Naming the interactions in `interactions` rather than letting one callback read
|
|
11810
|
+
* the pointer is what lets this know, BEFORE the first pixel, which of them the
|
|
11811
|
+
* element takes. Two things depend on knowing that early:
|
|
11812
|
+
*
|
|
11813
|
+
* - **The arbitration between nested boxes.** A row swiped sideways lives inside a
|
|
11814
|
+
* container that travels sideways too, and the innermost box must take the axis
|
|
11815
|
+
* it walks — read from the DOM at the press, and only from there (see axesLeftBy
|
|
11816
|
+
* in @jsenv/dom). The axis comes from the name (`swipe_left` and `swipe_right`
|
|
11817
|
+
* say `x`), so the attribute is written at render time. Written during the
|
|
11818
|
+
* gesture it would arrive too late: a browser decides what a touch may do when
|
|
11819
|
+
* the touch BEGINS. Same for `touch-action` and iOS's callout below, which is
|
|
11820
|
+
* why they are a stylesheet and not a line of JS in the pointerdown.
|
|
11821
|
+
*
|
|
11822
|
+
* - **The click a gesture leaves behind.** A hold ends with a `pointerup`, so the
|
|
11823
|
+
* browser follows it with a `click` on whatever the finger was over — a link,
|
|
11824
|
+
* and the page navigates as well as opening the menu.
|
|
11825
|
+
*
|
|
11826
|
+
* A swipe makes the element follow the finger — there is nothing to decide about
|
|
11827
|
+
* that — and says where it is up to, for the caller to draw the rest with:
|
|
11828
|
+
*
|
|
11829
|
+
* - `--swipe-pulled`: how far it has come, signed, in px.
|
|
11830
|
+
* - `--swipe-progress`: the same as a fraction of the element, signed. Inherited,
|
|
11831
|
+
* so anything inside the element can read it.
|
|
11832
|
+
* - `[data-swiping="left|right|up|down"]`: which way, while a finger holds it.
|
|
11833
|
+
* - `[data-swipe-past-threshold]`: letting go now would go through with it.
|
|
11834
|
+
*
|
|
11835
|
+
* WHAT is revealed behind is the caller's: navi does not know what putting a row
|
|
11836
|
+
* away looks like. A trail is usually a child of the swiped element sized off
|
|
11837
|
+
* `--swipe-pulled`, which is what makes those values reachable from CSS at all — a
|
|
11838
|
+
* sibling could not read them.
|
|
11839
|
+
*
|
|
11840
|
+
* A swipe is its own gesture, not the drag-to-travel of a slide container: it
|
|
11841
|
+
* borrows the same reader (`startDragToTravel`, whose axis lock, resistance, flick
|
|
11842
|
+
* and click-swallowing are exactly what a swipe needs) with its own settings, and
|
|
11843
|
+
* it is those settings that differ — a third of a row rather than a third of a
|
|
11844
|
+
* screen, tuned per element with `data-swipe-threshold`.
|
|
11845
|
+
*
|
|
11846
|
+
* A hold does NOT take the context menu with it. Declaring one says what a held
|
|
11847
|
+
* FINGER does — a finger held down being the system's own context-menu gesture,
|
|
11848
|
+
* which is why that one is refused while the wait runs (see waitForPressHeld). A
|
|
11849
|
+
* right click is not that press: it comes from the other button and it is the
|
|
11850
|
+
* user asking for the browser's menu, so it keeps opening it. An element that
|
|
11851
|
+
* wants the right click to do what the hold does says so, with `contextmenu`
|
|
11852
|
+
* beside it.
|
|
11853
|
+
*
|
|
11854
|
+
* A hold CAN open a popup while the finger is still down — a menu appearing under
|
|
11855
|
+
* a waiting finger, which is the native gesture. navi's Popover is `popover="manual"`
|
|
11856
|
+
* and owns its own dismissal, so the `pointerup` that ends the press is not read as
|
|
11857
|
+
* an interaction outside it (the browser's light dismiss, which would close it on
|
|
11858
|
+
* that very event, only applies to `popover="auto"`).
|
|
11859
|
+
*/
|
|
11860
|
+
|
|
11861
|
+
// The axis each swipe names, which is the whole reason they are named rather than
|
|
11862
|
+
// counted: an element that takes a horizontal swipe has to say so in the DOM
|
|
11863
|
+
// before it is touched.
|
|
11864
|
+
const AXIS_BY_SWIPE_TYPE = {
|
|
11865
|
+
swipe_left: "x",
|
|
11866
|
+
swipe_right: "x",
|
|
11867
|
+
swipe_up: "y",
|
|
11868
|
+
swipe_down: "y"
|
|
11869
|
+
};
|
|
11870
|
+
// Which way the content goes. A drag towards positive x moves the row right, which
|
|
11871
|
+
// is a swipe right — and, for the travel underneath, the direction that brings in
|
|
11872
|
+
// what comes BEFORE (`travelBack`).
|
|
11873
|
+
const SWIPE_TYPE_BY_AXIS = {
|
|
11874
|
+
x: {
|
|
11875
|
+
positive: "swipe_right",
|
|
11876
|
+
negative: "swipe_left"
|
|
11877
|
+
},
|
|
11878
|
+
y: {
|
|
11879
|
+
positive: "swipe_down",
|
|
11880
|
+
negative: "swipe_up"
|
|
11881
|
+
}
|
|
11882
|
+
};
|
|
11883
|
+
|
|
11884
|
+
// How much of the element has to be pulled for letting go to go through with it. A
|
|
11885
|
+
// FRACTION and never a distance: the same gesture must mean the same thing on a
|
|
11886
|
+
// phone and on a wide screen. Speed answers on its own on top of this — a brief
|
|
11887
|
+
// flick counts whatever the distance covered.
|
|
11888
|
+
const SWIPE_THRESHOLD_DEFAULT = 0.33;
|
|
11889
|
+
// Under the system context-menu delay, so the press is answered before the menu it
|
|
11890
|
+
// would otherwise open.
|
|
11891
|
+
const LONGPRESS_DELAY_DEFAULT = 450;
|
|
11892
|
+
// Past this the finger is going somewhere: it swipes, it does not hold.
|
|
11893
|
+
const LONGPRESS_SLOP_DEFAULT = 8;
|
|
11894
|
+
// How long the element takes to reach where the gesture leaves it, or to come
|
|
11895
|
+
// back. Written into the CSS below from here: the state is cleaned up when the
|
|
11896
|
+
// movement is over, so a duration living only in the stylesheet would be a timing
|
|
11897
|
+
// JS has to know and cannot read reliably.
|
|
11898
|
+
const SETTLE_DURATION_MS = 200;
|
|
11899
|
+
|
|
11900
|
+
// Read off the element or off any ancestor carrying it, so a whole list is tuned
|
|
11901
|
+
// in one place and a stylesheet can read the same value.
|
|
11902
|
+
const SWIPE_THRESHOLD_ATTRIBUTE = "data-swipe-threshold";
|
|
11903
|
+
const LONGPRESS_DELAY_ATTRIBUTE = "data-longpress-delay";
|
|
11904
|
+
const LONGPRESS_SLOP_ATTRIBUTE = "data-longpress-slop";
|
|
11905
|
+
|
|
11906
|
+
// Which axes this element takes a swipe on, and that it takes a hold: said in the
|
|
11907
|
+
// DOM at render time, for the CSS below and for the boxes above to read.
|
|
11908
|
+
const SWIPE_AXES_ATTRIBUTE = "data-swipe";
|
|
11909
|
+
const LONGPRESS_ATTRIBUTE = "data-longpress";
|
|
11910
|
+
import.meta.css = [/* css */`
|
|
11911
|
+
/* Declared, so the browser sees a NUMBER it can interpolate and calculate with:
|
|
11912
|
+
what a swipe reveals behind the element is drawn from this, and an undeclared
|
|
11913
|
+
custom property only ever jumps from one value to the next. Inherited, so
|
|
11914
|
+
anything inside the element can read it. */
|
|
11915
|
+
@property --swipe-progress {
|
|
11916
|
+
syntax: "<number>";
|
|
11917
|
+
inherits: true;
|
|
11918
|
+
initial-value: 0;
|
|
11919
|
+
}
|
|
11920
|
+
@property --swipe-pulled {
|
|
11921
|
+
syntax: "<length>";
|
|
11922
|
+
inherits: false;
|
|
11923
|
+
initial-value: 0px;
|
|
11924
|
+
}
|
|
11925
|
+
|
|
11926
|
+
/* What a touch may do on an element that takes a swipe: the axis the swipe walks
|
|
11927
|
+
is taken, the other is left to the page — so a row is swiped sideways and the
|
|
11928
|
+
list still scrolls under the same finger. */
|
|
11929
|
+
[${SWIPE_AXES_ATTRIBUTE}="x"] {
|
|
11930
|
+
touch-action: pan-y;
|
|
11931
|
+
}
|
|
11932
|
+
[${SWIPE_AXES_ATTRIBUTE}="y"] {
|
|
11933
|
+
touch-action: pan-x;
|
|
11934
|
+
}
|
|
11935
|
+
[${SWIPE_AXES_ATTRIBUTE}="xy"] {
|
|
11936
|
+
touch-action: none;
|
|
11937
|
+
}
|
|
11938
|
+
|
|
11939
|
+
/* iOS shows its callout (Copy / Look Up) and selects the word under the finger on
|
|
11940
|
+
a press held still, and does not always route that through an event that can be
|
|
11941
|
+
refused. Same reason as the drag sources in @jsenv/dom: it has to be true
|
|
11942
|
+
before the finger lands. */
|
|
11943
|
+
[${LONGPRESS_ATTRIBUTE}] {
|
|
11944
|
+
-webkit-touch-callout: none;
|
|
11945
|
+
}
|
|
11946
|
+
|
|
11947
|
+
/* The element follows the finger. The translate property rather than a transform,
|
|
11948
|
+
so whatever transform the element (or its theme) already has is left alone. */
|
|
11949
|
+
[data-swiping="left"],
|
|
11950
|
+
[data-swiping="right"] {
|
|
11951
|
+
translate: var(--swipe-pulled) 0;
|
|
11952
|
+
}
|
|
11953
|
+
[data-swiping="up"],
|
|
11954
|
+
[data-swiping="down"] {
|
|
11955
|
+
translate: 0 var(--swipe-pulled);
|
|
11956
|
+
}
|
|
11957
|
+
/* No transition while the finger holds it — the element is where the hand put
|
|
11958
|
+
it — and one when the hand lets go. */
|
|
11959
|
+
[data-swipe-settling] {
|
|
11960
|
+
transition: translate ${SETTLE_DURATION_MS}ms ease-out;
|
|
11961
|
+
}
|
|
11962
|
+
`, "@jsenv/navi/src/control/interaction/interaction_press.js"];
|
|
11963
|
+
defineInteractionDetector({
|
|
11964
|
+
name: "press",
|
|
11965
|
+
claims: type => type in AXIS_BY_SWIPE_TYPE || type === "longpress",
|
|
11966
|
+
setup: (element, trigger, {
|
|
11967
|
+
types,
|
|
11968
|
+
readConfig
|
|
11969
|
+
}) => {
|
|
11970
|
+
let axes = "";
|
|
11971
|
+
for (const type of types) {
|
|
11972
|
+
const axis = AXIS_BY_SWIPE_TYPE[type];
|
|
11973
|
+
if (axis && !axes.includes(axis)) {
|
|
11974
|
+
axes += axis;
|
|
11975
|
+
}
|
|
11976
|
+
}
|
|
11977
|
+
const hasLongPress = types.includes("longpress");
|
|
11978
|
+
const undo = [];
|
|
11979
|
+
const mark = (attribute, value) => {
|
|
11980
|
+
element.setAttribute(attribute, value);
|
|
11981
|
+
undo.push(() => {
|
|
11982
|
+
element.removeAttribute(attribute);
|
|
11983
|
+
});
|
|
11984
|
+
};
|
|
11985
|
+
if (axes) {
|
|
11986
|
+
mark(SWIPE_AXES_ATTRIBUTE, axes);
|
|
11987
|
+
// Read by the boxes ABOVE this one: an axis swiped here is not theirs to
|
|
11988
|
+
// travel (see axesLeftBy).
|
|
11989
|
+
mark("data-travel-by-drag", axes);
|
|
11990
|
+
// …and nothing inside hands its leftover scroll to the page while the
|
|
11991
|
+
// element is being pulled.
|
|
11992
|
+
mark("data-drag-travel", axes);
|
|
11993
|
+
|
|
11994
|
+
// A link and an image are draggable without anyone asking, and a native
|
|
11995
|
+
// drag IS press-and-move: the browser claims the gesture, takes the pointer
|
|
11996
|
+
// events with it and paints a ghost of the row the hand is trying to swipe.
|
|
11997
|
+
// Two things are needed, and neither covers the other:
|
|
11998
|
+
// - `draggable` refuses it on the element itself, before it starts;
|
|
11999
|
+
// - a swipe is usually a row with a link or a thumbnail INSIDE it, and
|
|
12000
|
+
// those are draggable in their own right. `dragstart` bubbles, so
|
|
12001
|
+
// refusing it here refuses theirs too — and it is the only refusal every
|
|
12002
|
+
// browser honours (`-webkit-user-drag` is one engine's).
|
|
12003
|
+
// The cost is stated rather than worked around: an element that takes a
|
|
12004
|
+
// swipe cannot also be dragged out of the page, because there is one
|
|
12005
|
+
// gesture and it cannot mean both.
|
|
12006
|
+
mark("draggable", "false");
|
|
12007
|
+
const onDragStart = dragStartEvent => {
|
|
12008
|
+
dragStartEvent.preventDefault();
|
|
12009
|
+
};
|
|
12010
|
+
element.addEventListener("dragstart", onDragStart);
|
|
12011
|
+
undo.push(() => {
|
|
12012
|
+
element.removeEventListener("dragstart", onDragStart);
|
|
12013
|
+
});
|
|
12014
|
+
}
|
|
12015
|
+
if (hasLongPress) {
|
|
12016
|
+
mark(LONGPRESS_ATTRIBUTE, "");
|
|
12017
|
+
}
|
|
12018
|
+
const onPointerDown = pointerDownEvent => {
|
|
12019
|
+
if (pointerDownEvent.button !== 0) {
|
|
12020
|
+
return;
|
|
12021
|
+
}
|
|
12022
|
+
let swipe = null;
|
|
12023
|
+
let press = null;
|
|
12024
|
+
if (axes) {
|
|
12025
|
+
swipe = startSwipe(pointerDownEvent, {
|
|
12026
|
+
element,
|
|
12027
|
+
axes,
|
|
12028
|
+
types,
|
|
12029
|
+
trigger,
|
|
12030
|
+
threshold: readConfig(SWIPE_THRESHOLD_ATTRIBUTE, SWIPE_THRESHOLD_DEFAULT),
|
|
12031
|
+
// The axis is the definitive word on what this press is: a finger that
|
|
12032
|
+
// has picked one is swiping, not holding. The slop below cancels most
|
|
12033
|
+
// holds before this (it is the smaller distance), but a press that
|
|
12034
|
+
// resolves the axis without drifting — a mouse, a flick — has to be
|
|
12035
|
+
// taken from the wait too.
|
|
12036
|
+
onSwipeStart: () => {
|
|
12037
|
+
press?.cancel();
|
|
12038
|
+
press = null;
|
|
12039
|
+
}
|
|
12040
|
+
});
|
|
12041
|
+
}
|
|
12042
|
+
if (hasLongPress) {
|
|
12043
|
+
press = waitForPressHeld(pointerDownEvent, {
|
|
12044
|
+
delay: readConfig(LONGPRESS_DELAY_ATTRIBUTE, LONGPRESS_DELAY_DEFAULT),
|
|
12045
|
+
slop: readConfig(LONGPRESS_SLOP_ATTRIBUTE, LONGPRESS_SLOP_DEFAULT),
|
|
12046
|
+
onPressHeld: (pressEvent, {
|
|
12047
|
+
endPress
|
|
12048
|
+
}) => {
|
|
12049
|
+
// The hold won the arbitration: the swipe never got the distance it
|
|
12050
|
+
// needed, and must not get it from whatever the finger does next.
|
|
12051
|
+
swipe?.stop();
|
|
12052
|
+
swipe = null;
|
|
12053
|
+
const clickSuppressionIsOver = suppressClickAfterGesture();
|
|
12054
|
+
const onPointerEnd = () => {
|
|
12055
|
+
window.removeEventListener("pointerup", onPointerEnd, true);
|
|
12056
|
+
window.removeEventListener("pointercancel", onPointerEnd, true);
|
|
12057
|
+
endPress();
|
|
12058
|
+
clickSuppressionIsOver();
|
|
12059
|
+
};
|
|
12060
|
+
window.addEventListener("pointerup", onPointerEnd, true);
|
|
12061
|
+
window.addEventListener("pointercancel", onPointerEnd, true);
|
|
12062
|
+
trigger("longpress", pressEvent, {
|
|
12063
|
+
pointerType: pressEvent.pointerType
|
|
12064
|
+
});
|
|
12065
|
+
}
|
|
12066
|
+
});
|
|
12067
|
+
}
|
|
12068
|
+
};
|
|
12069
|
+
element.addEventListener("pointerdown", onPointerDown);
|
|
12070
|
+
undo.push(() => {
|
|
12071
|
+
element.removeEventListener("pointerdown", onPointerDown);
|
|
12072
|
+
});
|
|
12073
|
+
return () => {
|
|
12074
|
+
for (const undoOne of undo) {
|
|
12075
|
+
undoOne();
|
|
12076
|
+
}
|
|
12077
|
+
};
|
|
12078
|
+
}
|
|
12079
|
+
});
|
|
12080
|
+
const startSwipe = (pointerDownEvent, {
|
|
12081
|
+
element,
|
|
12082
|
+
axes,
|
|
12083
|
+
types,
|
|
12084
|
+
trigger,
|
|
12085
|
+
threshold,
|
|
12086
|
+
onSwipeStart
|
|
12087
|
+
}) => {
|
|
12088
|
+
let settleTimeout = null;
|
|
12089
|
+
const paint = ({
|
|
12090
|
+
pulled,
|
|
12091
|
+
progress,
|
|
12092
|
+
type
|
|
12093
|
+
}) => {
|
|
12094
|
+
element.style.setProperty("--swipe-pulled", `${pulled}px`);
|
|
12095
|
+
element.style.setProperty("--swipe-progress", progress);
|
|
12096
|
+
element.setAttribute("data-swiping", type.slice("swipe_".length));
|
|
12097
|
+
element.toggleAttribute("data-swipe-past-threshold", Math.abs(progress) >= threshold);
|
|
12098
|
+
};
|
|
12099
|
+
const forget = () => {
|
|
12100
|
+
clearTimeout(settleTimeout);
|
|
12101
|
+
element.removeAttribute("data-swiping");
|
|
12102
|
+
element.removeAttribute("data-swipe-past-threshold");
|
|
12103
|
+
element.removeAttribute("data-swipe-settling");
|
|
12104
|
+
element.style.removeProperty("--swipe-pulled");
|
|
12105
|
+
element.style.removeProperty("--swipe-progress");
|
|
12106
|
+
};
|
|
12107
|
+
// Where the gesture leaves the element, and later back to rest. Cleaned up on a
|
|
12108
|
+
// timeout rather than on transitionend: a swipe let go of at the very edge has
|
|
12109
|
+
// nothing left to move (the pull is clamped to the element's size), and a
|
|
12110
|
+
// movement that does not happen reports no end.
|
|
12111
|
+
const settleTo = ({
|
|
12112
|
+
pulled,
|
|
12113
|
+
progress,
|
|
12114
|
+
type
|
|
12115
|
+
}, {
|
|
12116
|
+
thenForget = true
|
|
12117
|
+
} = {}) => {
|
|
12118
|
+
clearTimeout(settleTimeout);
|
|
12119
|
+
element.setAttribute("data-swipe-settling", "");
|
|
12120
|
+
paint({
|
|
12121
|
+
pulled,
|
|
12122
|
+
progress,
|
|
12123
|
+
type
|
|
12124
|
+
});
|
|
12125
|
+
if (thenForget) {
|
|
12126
|
+
settleTimeout = setTimeout(forget, SETTLE_DURATION_MS);
|
|
12127
|
+
}
|
|
12128
|
+
};
|
|
12129
|
+
return startDragToTravel(pointerDownEvent, {
|
|
12130
|
+
element,
|
|
12131
|
+
axes,
|
|
12132
|
+
commitRatio: threshold,
|
|
12133
|
+
onStart: ({
|
|
12134
|
+
axis
|
|
12135
|
+
}) => {
|
|
12136
|
+
const {
|
|
12137
|
+
positive,
|
|
12138
|
+
negative
|
|
12139
|
+
} = SWIPE_TYPE_BY_AXIS[axis];
|
|
12140
|
+
const hasPositive = types.includes(positive);
|
|
12141
|
+
const hasNegative = types.includes(negative);
|
|
12142
|
+
if (!hasPositive && !hasNegative) {
|
|
12143
|
+
return false;
|
|
12144
|
+
}
|
|
12145
|
+
const {
|
|
12146
|
+
width,
|
|
12147
|
+
height
|
|
12148
|
+
} = element.getBoundingClientRect();
|
|
12149
|
+
const size = axis === "x" ? width : height;
|
|
12150
|
+
if (!size) {
|
|
12151
|
+
return false;
|
|
12152
|
+
}
|
|
12153
|
+
onSwipeStart();
|
|
12154
|
+
clearTimeout(settleTimeout);
|
|
12155
|
+
element.removeAttribute("data-swipe-settling");
|
|
12156
|
+
// A side nothing is declared for is not refused, it resists: the gesture is
|
|
12157
|
+
// answered (something moves) while saying there is nothing that way.
|
|
12158
|
+
return {
|
|
12159
|
+
size,
|
|
12160
|
+
travelBack: hasPositive,
|
|
12161
|
+
travelOn: hasNegative
|
|
12162
|
+
};
|
|
12163
|
+
},
|
|
12164
|
+
onPull: ({
|
|
12165
|
+
axis,
|
|
12166
|
+
pulled,
|
|
12167
|
+
progress
|
|
12168
|
+
}) => {
|
|
12169
|
+
paint({
|
|
12170
|
+
pulled,
|
|
12171
|
+
progress,
|
|
12172
|
+
type: swipeTypeOf(axis, pulled)
|
|
12173
|
+
});
|
|
12174
|
+
},
|
|
12175
|
+
onEnd: ({
|
|
12176
|
+
axis,
|
|
12177
|
+
pulled,
|
|
12178
|
+
size,
|
|
12179
|
+
sign,
|
|
12180
|
+
travels,
|
|
12181
|
+
event
|
|
12182
|
+
}) => {
|
|
12183
|
+
const type = swipeTypeOf(axis, pulled);
|
|
12184
|
+
const restingPlace = {
|
|
12185
|
+
pulled: 0,
|
|
12186
|
+
progress: 0,
|
|
12187
|
+
type
|
|
12188
|
+
};
|
|
12189
|
+
if (!travels) {
|
|
12190
|
+
settleTo(restingPlace);
|
|
12191
|
+
return;
|
|
12192
|
+
}
|
|
12193
|
+
// Out, and out is where it stays for as long as the answer takes: a row that
|
|
12194
|
+
// comes back and leaves again says the gesture was not understood.
|
|
12195
|
+
settleTo({
|
|
12196
|
+
pulled: sign * size,
|
|
12197
|
+
progress: sign,
|
|
12198
|
+
type
|
|
12199
|
+
}, {
|
|
12200
|
+
thenForget: false
|
|
12201
|
+
});
|
|
12202
|
+
const pending = trigger(type, event, {
|
|
12203
|
+
axis,
|
|
12204
|
+
sign,
|
|
12205
|
+
pulled,
|
|
12206
|
+
size,
|
|
12207
|
+
progress: pulled / size
|
|
12208
|
+
});
|
|
12209
|
+
if (!pending) {
|
|
12210
|
+
settleTo(restingPlace);
|
|
12211
|
+
return;
|
|
12212
|
+
}
|
|
12213
|
+
// However it ended, the element comes back: a failure leaves the row in
|
|
12214
|
+
// place and it can be tried again, and a success is the caller's to answer
|
|
12215
|
+
// (a list that redemands its rows, a row that leaves). Nothing here makes it
|
|
12216
|
+
// disappear — navi does not know what "put away" means.
|
|
12217
|
+
const comeBack = () => {
|
|
12218
|
+
settleTo(restingPlace);
|
|
12219
|
+
};
|
|
12220
|
+
pending.then(comeBack, comeBack);
|
|
12221
|
+
},
|
|
12222
|
+
// Nothing to put back: onGiveUp only ever comes from a press that never became
|
|
12223
|
+
// a swipe, so nothing was painted by it.
|
|
12224
|
+
onGiveUp: () => {}
|
|
12225
|
+
});
|
|
12226
|
+
};
|
|
12227
|
+
const swipeTypeOf = (axis, pulled) => {
|
|
12228
|
+
const {
|
|
12229
|
+
positive,
|
|
12230
|
+
negative
|
|
12231
|
+
} = SWIPE_TYPE_BY_AXIS[axis];
|
|
12232
|
+
return pulled > 0 ? positive : negative;
|
|
12233
|
+
};
|
|
12234
|
+
|
|
12235
|
+
/**
|
|
12236
|
+
* `move`, `reorder`, `toss` — one grab, and what letting go of it means.
|
|
12237
|
+
*
|
|
12238
|
+
* All three are the same gesture: the element is picked up and carried. What
|
|
12239
|
+
* differs is the answer at the release, so one detector reads them all — it is one
|
|
12240
|
+
* press, and something has to arbitrate it.
|
|
12241
|
+
*
|
|
12242
|
+
* interactions={{ reorder: moveBefore, toss: remove }}
|
|
12243
|
+
* interactions={{ move: remember }}
|
|
12244
|
+
*
|
|
12245
|
+
* `reorder` and `toss` combine: a task dragged onto another changes places, the
|
|
12246
|
+
* same task thrown far and fast is gotten rid of. `move` does not combine with
|
|
12247
|
+
* `reorder` — an element either goes where it is put or takes a place in a list,
|
|
12248
|
+
* and the two answers cannot both be true of one release.
|
|
12249
|
+
*
|
|
12250
|
+
* `move` carries the element ITSELF and leaves it where it was put; the other two
|
|
12251
|
+
* carry a copy and put the original back. That is the same difference said in
|
|
12252
|
+
* layout terms: something moved has a new place of its own, something reordered
|
|
12253
|
+
* had its place taken by the list.
|
|
12254
|
+
*
|
|
12255
|
+
* Nothing of the gesture is decided here. `startDragTo` owns all of it — the
|
|
12256
|
+
* copy carried above the page while the original keeps its place in the layout, the
|
|
12257
|
+
* drop hint, the drop targets found by intersection, the no-op drops filtered out,
|
|
12258
|
+
* the flight of a thrown copy and its return when the answer refuses. This says
|
|
12259
|
+
* which elements are the items, how they are named, and what a given release means.
|
|
12260
|
+
*
|
|
12261
|
+
* WHICH ELEMENTS. Every element declaring `reorder` marks itself, so the set of
|
|
12262
|
+
* items IS the set of elements that declared it — no selector to pass, nothing to
|
|
12263
|
+
* keep in sync with the markup, and an item that must not move simply does not
|
|
12264
|
+
* declare it. An element that only declares `toss` marks nothing: it is not a place
|
|
12265
|
+
* anything lands.
|
|
12266
|
+
*
|
|
12267
|
+
* HOW THEY ARE NAMED: by `id`. A reorder is answered in terms of what moved and
|
|
12268
|
+
* what it landed before, and a DOM index cannot say it — a list draws fewer rows
|
|
12269
|
+
* than it has, and a search reorders them.
|
|
12270
|
+
*
|
|
12271
|
+
* WHAT COMES BACK, in the interaction's own detail:
|
|
12272
|
+
*
|
|
12273
|
+
* interactions={{
|
|
12274
|
+
* reorder: (event) => {
|
|
12275
|
+
* const { fromId, toId, syncCloneWithDropTarget } = event.detail;
|
|
12276
|
+
* return document.startViewTransition(() => {
|
|
12277
|
+
* syncCloneWithDropTarget();
|
|
12278
|
+
* setOrder(moveBefore(order, fromId, toId));
|
|
12279
|
+
* }).finished;
|
|
12280
|
+
* },
|
|
12281
|
+
* toss: (event) => remove(event.detail.id),
|
|
12282
|
+
* }}
|
|
12283
|
+
*
|
|
12284
|
+
* `toId` is null for a drop at the end. `syncCloneWithDropTarget` must be called
|
|
12285
|
+
* synchronously inside the transition callback, next to the state change, so the
|
|
12286
|
+
* copy is captured where it lands rather than where it was let go of.
|
|
12287
|
+
*
|
|
12288
|
+
* And the promise matters in both cases: the gesture holds its copy until the
|
|
12289
|
+
* answer settles. Returning the transition is what makes a landing continuous; a
|
|
12290
|
+
* `toss` that rejects brings the copy back, because the thing still exists and the
|
|
12291
|
+
* screen has to say so.
|
|
12292
|
+
*
|
|
12293
|
+
* Starting a document transition is the application's call and not navi's: a
|
|
12294
|
+
* `view-transition-name` must be unique per document, so only the application can
|
|
12295
|
+
* name what moves.
|
|
12296
|
+
*
|
|
12297
|
+
* What the copy LOOKS like is the application's too. A copy of a transparent
|
|
12298
|
+
* element is invisible — a row usually gets its background from the list around it,
|
|
12299
|
+
* which the copy has left — so it is dressed through the attributes the gesture
|
|
12300
|
+
* writes: `navi-drag-clone` on the copy, `navi-drag-clone-source` on the original.
|
|
12301
|
+
*/
|
|
12302
|
+
|
|
12303
|
+
|
|
12304
|
+
const MOVE = "move";
|
|
12305
|
+
const REORDER = "reorder";
|
|
12306
|
+
const TOSS = "toss";
|
|
12307
|
+
|
|
12308
|
+
// What makes an element a place something can land, written by the detector itself.
|
|
12309
|
+
const REORDERABLE_ATTRIBUTE = "data-reorderable";
|
|
12310
|
+
// Which axes the drag walks: "x", "y" or "xy". Its default is not the same for
|
|
12311
|
+
// every outcome — a list runs one way, and something being put somewhere goes
|
|
12312
|
+
// wherever it is put.
|
|
12313
|
+
const AXIS_ATTRIBUTE = "data-drag-axis";
|
|
12314
|
+
const DELAY_ATTRIBUTE = "data-drag-delay";
|
|
12315
|
+
const SLOP_ATTRIBUTE = "data-drag-slop";
|
|
12316
|
+
const THRESHOLD_ATTRIBUTE = "data-drag-threshold";
|
|
12317
|
+
const TOSS_DISTANCE_ATTRIBUTE = "data-toss-distance";
|
|
12318
|
+
const TOSS_SPEED_ATTRIBUTE = "data-toss-speed";
|
|
12319
|
+
|
|
12320
|
+
defineInteractionDetector({
|
|
12321
|
+
name: "drag",
|
|
12322
|
+
claims: (type) => type === MOVE || type === REORDER || type === TOSS,
|
|
12323
|
+
setup: (element, trigger, { types, readConfig }) => {
|
|
12324
|
+
const canMove = types.includes(MOVE);
|
|
12325
|
+
const canReorder = types.includes(REORDER);
|
|
12326
|
+
const canToss = types.includes(TOSS);
|
|
12327
|
+
// Read at setup rather than at the press: a container can say it, and it is
|
|
12328
|
+
// what the gesture is about rather than something it discovers.
|
|
12329
|
+
const axisHolder = element.closest(`[${AXIS_ATTRIBUTE}]`);
|
|
12330
|
+
const axes =
|
|
12331
|
+
axisHolder?.getAttribute(AXIS_ATTRIBUTE) ||
|
|
12332
|
+
// A list runs one way, and reordering walks it. Anything else goes wherever
|
|
12333
|
+
// the hand takes it: a thing put somewhere has two axes to be put along, and
|
|
12334
|
+
// a throw goes where it was thrown.
|
|
12335
|
+
(canReorder && !canToss ? "y" : "xy");
|
|
12336
|
+
|
|
12337
|
+
if (canReorder) {
|
|
12338
|
+
element.setAttribute(REORDERABLE_ATTRIBUTE, "");
|
|
12339
|
+
}
|
|
12340
|
+
// What @jsenv/dom puts on a drag source: no iOS callout, and the touch left to
|
|
12341
|
+
// the scroll until the press becomes a grab. Its value is the axis the
|
|
12342
|
+
// SURROUNDINGS scroll on, which for a list is the axis the list runs on.
|
|
12343
|
+
element.setAttribute("data-drag-source", axes === "x" ? "x" : "");
|
|
12344
|
+
|
|
12345
|
+
const onPointerDown = (pointerDownEvent) => {
|
|
12346
|
+
// What this element says a release can mean. The gesture then runs only what
|
|
12347
|
+
// those need — no copy for a move, no drop hint for something that can only
|
|
12348
|
+
// be thrown away.
|
|
12349
|
+
startDragTo(pointerDownEvent, types, {
|
|
12350
|
+
draggedElement: element,
|
|
12351
|
+
// Nothing to land on when nothing reorders.
|
|
12352
|
+
itemSelector: canReorder ? `[${REORDERABLE_ATTRIBUTE}]` : undefined,
|
|
12353
|
+
getItemId: (itemElement) => itemElement.id,
|
|
12354
|
+
direction: { x: axes.includes("x"), y: axes.includes("y") },
|
|
12355
|
+
// Where it may go, said in the DOM. A thing that is put somewhere stays
|
|
12356
|
+
// inside what one can SEE of its container ("scrollport", not "scroll":
|
|
12357
|
+
// the scrollable area can be far larger than the box, and "inside the box"
|
|
12358
|
+
// is what a hand expects). `data-drag-free` lifts that. Left alone for a
|
|
12359
|
+
// throw, which frees the area on its own — it has to be able to leave.
|
|
12360
|
+
areaConstraint: element.closest(`[data-drag-free]`)
|
|
12361
|
+
? "none"
|
|
12362
|
+
: canMove
|
|
12363
|
+
? "scrollport"
|
|
12364
|
+
: undefined,
|
|
12365
|
+
threshold: readConfig(THRESHOLD_ATTRIBUTE, undefined),
|
|
12366
|
+
longPressDelay: readConfig(DELAY_ATTRIBUTE, undefined),
|
|
12367
|
+
longPressSlop: readConfig(SLOP_ATTRIBUTE, undefined),
|
|
12368
|
+
tossDistance: readConfig(TOSS_DISTANCE_ATTRIBUTE, undefined),
|
|
12369
|
+
tossSpeed: readConfig(TOSS_SPEED_ATTRIBUTE, undefined),
|
|
12370
|
+
// Handed straight back in all three cases: what `trigger` returns is a
|
|
12371
|
+
// promise while the answer is still going, which is what the gesture waits
|
|
12372
|
+
// on before it lets go of what it carries.
|
|
12373
|
+
onReorder: (fromId, toId, syncCloneWithDropTarget) =>
|
|
12374
|
+
trigger(REORDER, pointerDownEvent, {
|
|
12375
|
+
fromId,
|
|
12376
|
+
toId,
|
|
12377
|
+
syncCloneWithDropTarget,
|
|
12378
|
+
}),
|
|
12379
|
+
onToss: ({ gestureInfo }) =>
|
|
12380
|
+
trigger(TOSS, pointerDownEvent, {
|
|
12381
|
+
id: element.id,
|
|
12382
|
+
velocity: gestureInfo.velocity,
|
|
12383
|
+
x: gestureInfo.layout.xDelta,
|
|
12384
|
+
y: gestureInfo.layout.yDelta,
|
|
12385
|
+
}),
|
|
12386
|
+
onMove: ({ x, y }) => trigger(MOVE, pointerDownEvent, { x, y }),
|
|
12387
|
+
});
|
|
12388
|
+
};
|
|
12389
|
+
element.addEventListener("pointerdown", onPointerDown);
|
|
12390
|
+
|
|
12391
|
+
return () => {
|
|
12392
|
+
element.removeAttribute(REORDERABLE_ATTRIBUTE);
|
|
12393
|
+
element.removeAttribute("data-drag-source");
|
|
12394
|
+
element.removeEventListener("pointerdown", onPointerDown);
|
|
12395
|
+
};
|
|
12396
|
+
},
|
|
12397
|
+
});
|
|
12398
|
+
|
|
11700
12399
|
const isSignal = (value) => {
|
|
11701
12400
|
return getSignalType(value) !== null;
|
|
11702
12401
|
};
|
|
@@ -15399,6 +16098,43 @@ const isSameKey = (browserEventKey, key) => {
|
|
|
15399
16098
|
return false;
|
|
15400
16099
|
};
|
|
15401
16100
|
|
|
16101
|
+
/**
|
|
16102
|
+
* `"keyboard:ctrl+backspace"` — an interaction asked for with keys.
|
|
16103
|
+
*
|
|
16104
|
+
* The name carries the shortcut, so a control says what a shortcut DOES in the
|
|
16105
|
+
* same place it says what a swipe does, rather than in a separate shortcut list
|
|
16106
|
+
* whose entries then have to find their way back to the action.
|
|
16107
|
+
*
|
|
16108
|
+
* The shortcut itself is not parsed here: `createOnKeyDownForShortcuts` already
|
|
16109
|
+
* knows what "ctrl+backspace" means, which keys a control may take without
|
|
16110
|
+
* swallowing what the browser owed them, and which it may not.
|
|
16111
|
+
*
|
|
16112
|
+
* The interaction is dispatched under its full name, colon included, so
|
|
16113
|
+
* `findEvent(event, "keyboard:ctrl+backspace")` gets back to it — the shortcut
|
|
16114
|
+
* that asked is what the action wants to know, not that a key went down.
|
|
16115
|
+
*/
|
|
16116
|
+
|
|
16117
|
+
|
|
16118
|
+
const KEYBOARD_PREFIX = "keyboard:";
|
|
16119
|
+
|
|
16120
|
+
defineInteractionDetector({
|
|
16121
|
+
name: "keyboard",
|
|
16122
|
+
claims: (type) => type.startsWith(KEYBOARD_PREFIX),
|
|
16123
|
+
setup: (element, trigger, { types }) => {
|
|
16124
|
+
const shortcuts = {};
|
|
16125
|
+
for (const type of types) {
|
|
16126
|
+
shortcuts[type.slice(KEYBOARD_PREFIX.length)] = (keyboardEvent) => {
|
|
16127
|
+
trigger(type, keyboardEvent);
|
|
16128
|
+
};
|
|
16129
|
+
}
|
|
16130
|
+
const onKeyDown = createOnKeyDownForShortcuts(shortcuts);
|
|
16131
|
+
element.addEventListener("keydown", onKeyDown);
|
|
16132
|
+
return () => {
|
|
16133
|
+
element.removeEventListener("keydown", onKeyDown);
|
|
16134
|
+
};
|
|
16135
|
+
},
|
|
16136
|
+
});
|
|
16137
|
+
|
|
15402
16138
|
/**
|
|
15403
16139
|
* Merges a component's base className with className received from props.
|
|
15404
16140
|
*
|
|
@@ -27033,7 +27769,7 @@ const css$V = /* css */`
|
|
|
27033
27769
|
position: absolute;
|
|
27034
27770
|
inset: unset;
|
|
27035
27771
|
/* Custom renderer only — see openLocalDialogCount above */
|
|
27036
|
-
z-index: calc(var(--navi-
|
|
27772
|
+
z-index: calc(var(--navi-z-index-popup) + var(--dialog-stack-order, 0));
|
|
27037
27773
|
min-width: min(
|
|
27038
27774
|
max(var(--anchor-width, 0px), var(--dialog-min-width, 0px)),
|
|
27039
27775
|
var(--x-dialog-max-width)
|
|
@@ -28302,7 +29038,7 @@ const css$U = /* css */`
|
|
|
28302
29038
|
most-recently-opened local popover always outranks an earlier one,
|
|
28303
29039
|
regardless of DOM position — a plain positioned div gets no free
|
|
28304
29040
|
"last shown wins" the way the top-layer renderer does. */
|
|
28305
|
-
z-index: calc(var(--navi-
|
|
29041
|
+
z-index: calc(var(--navi-z-index-popup) + var(--popover-stack-order, 0));
|
|
28306
29042
|
min-width: min(var(--popover-min-width, 0px), var(--x-popover-max-width));
|
|
28307
29043
|
max-width: var(--x-popover-max-width);
|
|
28308
29044
|
min-height: min(
|
|
@@ -28435,7 +29171,7 @@ const css$U = /* css */`
|
|
|
28435
29171
|
instance still resolve via DOM order (content rendered after its own
|
|
28436
29172
|
backdrop, see this file's top comment), so this only needs to beat
|
|
28437
29173
|
*other* popovers' own backdrop/content, not its own. */
|
|
28438
|
-
z-index: calc(var(--navi-
|
|
29174
|
+
z-index: calc(var(--navi-z-index-popup) + var(--popover-stack-order, 0));
|
|
28439
29175
|
margin: 0;
|
|
28440
29176
|
padding: 0;
|
|
28441
29177
|
background: transparent;
|
|
@@ -41486,7 +42222,7 @@ const css$L = /* css */`
|
|
|
41486
42222
|
|
|
41487
42223
|
.navi_fixed_bar {
|
|
41488
42224
|
position: fixed;
|
|
41489
|
-
z-index:
|
|
42225
|
+
z-index: var(--navi-z-index-bar);
|
|
41490
42226
|
display: flex;
|
|
41491
42227
|
box-sizing: border-box;
|
|
41492
42228
|
margin: auto;
|
|
@@ -46354,32 +47090,53 @@ HTMLFormElement.prototype.requestSubmit = function (submitter) {
|
|
|
46354
47090
|
// form.dispatchEvent(customEvent);
|
|
46355
47091
|
// };
|
|
46356
47092
|
|
|
46357
|
-
installImportMetaCssBuild(import.meta)
|
|
47093
|
+
installImportMetaCssBuild(import.meta);/**
|
|
47094
|
+
* A row or column of controls sharing one frame: the borders where two of them
|
|
47095
|
+
* meet are drawn once instead of twice, and only the outer corners stay
|
|
47096
|
+
* rounded. See docs/control_group.md.
|
|
47097
|
+
*/
|
|
47098
|
+
const css$B = /* css */`
|
|
46358
47099
|
.navi_group {
|
|
46359
|
-
--group-border-width:
|
|
46360
|
-
|
|
47100
|
+
--group-border-width: var(--navi-control-border-width);
|
|
47101
|
+
|
|
47102
|
+
/* Squaring the joined corners is said on the direct child alone: a navi
|
|
47103
|
+
control declares the radius of its frame on its own root, and whatever
|
|
47104
|
+
inner element actually draws the frame inherits it. .navi_button_content
|
|
47105
|
+
is the exception — a button's frame sits on it and a button can arrive
|
|
47106
|
+
wrapped (a tooltip, a link), so it is named on its own. */
|
|
47107
|
+
|
|
47108
|
+
/* Members overlap by the width of one border, so along each seam one of the
|
|
47109
|
+
two borders covers the other. Whichever member the user is on has to be
|
|
47110
|
+
the one on top: it is the one whose border changes color, and the one
|
|
47111
|
+
whose focus ring goes all the way around — half a ring, cut by the
|
|
47112
|
+
neighbour painted after it, is what this avoids. z-index needs a
|
|
47113
|
+
positioned element to mean anything, hence position: relative.
|
|
47114
|
+
Deliberately not paired with isolation: isolate — a stacking context
|
|
47115
|
+
here would also trap the popup of a picker held in the group, which
|
|
47116
|
+
counts on its own band reaching the whole page. What keeps these two
|
|
47117
|
+
values from escaping is instead that everything they could reach is a
|
|
47118
|
+
band above them (see navi_z_indexes.js). */
|
|
46361
47119
|
> *:hover,
|
|
46362
47120
|
> *[data-hover] {
|
|
46363
47121
|
position: relative;
|
|
46364
|
-
z-index:
|
|
47122
|
+
z-index: var(--navi-z-index-control-hovered);
|
|
46365
47123
|
}
|
|
46366
47124
|
> *:focus-visible,
|
|
46367
47125
|
> *[data-focus-visible] {
|
|
46368
47126
|
position: relative;
|
|
46369
|
-
z-index:
|
|
47127
|
+
z-index: var(--navi-z-index-control-focused);
|
|
46370
47128
|
}
|
|
46371
47129
|
|
|
46372
47130
|
/* Horizontal (default): Cumulative margin for border overlap */
|
|
46373
47131
|
&:not([data-vertical]) {
|
|
46374
47132
|
> *:not(:first-child) {
|
|
46375
|
-
margin-left: calc(var(--border-width) * -1);
|
|
47133
|
+
margin-left: calc(var(--border-width, var(--group-border-width)) * -1);
|
|
46376
47134
|
}
|
|
46377
47135
|
> *:first-child:not(:only-child) {
|
|
46378
47136
|
border-top-right-radius: 0 !important;
|
|
46379
47137
|
border-bottom-right-radius: 0 !important;
|
|
46380
47138
|
|
|
46381
|
-
.navi_button_content
|
|
46382
|
-
.navi_native_input {
|
|
47139
|
+
.navi_button_content {
|
|
46383
47140
|
border-top-right-radius: 0 !important;
|
|
46384
47141
|
border-bottom-right-radius: 0 !important;
|
|
46385
47142
|
}
|
|
@@ -46389,8 +47146,7 @@ installImportMetaCssBuild(import.meta);const css$B = /* css */`
|
|
|
46389
47146
|
border-top-left-radius: 0 !important;
|
|
46390
47147
|
border-bottom-left-radius: 0 !important;
|
|
46391
47148
|
|
|
46392
|
-
.navi_button_content
|
|
46393
|
-
.navi_native_input {
|
|
47149
|
+
.navi_button_content {
|
|
46394
47150
|
border-top-left-radius: 0 !important;
|
|
46395
47151
|
border-bottom-left-radius: 0 !important;
|
|
46396
47152
|
}
|
|
@@ -46399,8 +47155,7 @@ installImportMetaCssBuild(import.meta);const css$B = /* css */`
|
|
|
46399
47155
|
> *:not(:first-child):not(:last-child) {
|
|
46400
47156
|
border-radius: 0 !important;
|
|
46401
47157
|
|
|
46402
|
-
.navi_button_content
|
|
46403
|
-
.navi_native_input {
|
|
47158
|
+
.navi_button_content {
|
|
46404
47159
|
border-radius: 0 !important;
|
|
46405
47160
|
}
|
|
46406
47161
|
}
|
|
@@ -46409,14 +47164,13 @@ installImportMetaCssBuild(import.meta);const css$B = /* css */`
|
|
|
46409
47164
|
/* Vertical: Cumulative margin for border overlap */
|
|
46410
47165
|
&[data-vertical] {
|
|
46411
47166
|
> *:not(:first-child) {
|
|
46412
|
-
margin-top: calc(var(--group-border-width) * -1);
|
|
47167
|
+
margin-top: calc(var(--border-width, var(--group-border-width)) * -1);
|
|
46413
47168
|
}
|
|
46414
47169
|
> *:first-child:not(:only-child) {
|
|
46415
47170
|
border-bottom-right-radius: 0 !important;
|
|
46416
47171
|
border-bottom-left-radius: 0 !important;
|
|
46417
47172
|
|
|
46418
|
-
.navi_button_content
|
|
46419
|
-
.navi_native_input {
|
|
47173
|
+
.navi_button_content {
|
|
46420
47174
|
border-bottom-right-radius: 0 !important;
|
|
46421
47175
|
border-bottom-left-radius: 0 !important;
|
|
46422
47176
|
}
|
|
@@ -46426,8 +47180,7 @@ installImportMetaCssBuild(import.meta);const css$B = /* css */`
|
|
|
46426
47180
|
border-top-left-radius: 0 !important;
|
|
46427
47181
|
border-top-right-radius: 0 !important;
|
|
46428
47182
|
|
|
46429
|
-
.navi_button_content
|
|
46430
|
-
.navi_native_input {
|
|
47183
|
+
.navi_button_content {
|
|
46431
47184
|
border-top-left-radius: 0 !important;
|
|
46432
47185
|
border-top-right-radius: 0 !important;
|
|
46433
47186
|
}
|
|
@@ -46436,8 +47189,7 @@ installImportMetaCssBuild(import.meta);const css$B = /* css */`
|
|
|
46436
47189
|
> *:not(:first-child):not(:last-child) {
|
|
46437
47190
|
border-radius: 0 !important;
|
|
46438
47191
|
|
|
46439
|
-
.navi_button_content
|
|
46440
|
-
.navi_native_input {
|
|
47192
|
+
.navi_button_content {
|
|
46441
47193
|
border-radius: 0 !important;
|
|
46442
47194
|
}
|
|
46443
47195
|
}
|
|
@@ -52387,7 +53139,7 @@ const css$v = /* css */`
|
|
|
52387
53139
|
position: sticky;
|
|
52388
53140
|
top: 0;
|
|
52389
53141
|
left: 0;
|
|
52390
|
-
z-index:
|
|
53142
|
+
z-index: var(--navi-z-index-sticky);
|
|
52391
53143
|
order: -2;
|
|
52392
53144
|
}
|
|
52393
53145
|
.navi_list_fallback,
|
|
@@ -52494,7 +53246,7 @@ const css$v = /* css */`
|
|
|
52494
53246
|
position: sticky;
|
|
52495
53247
|
right: 0;
|
|
52496
53248
|
bottom: 0;
|
|
52497
|
-
z-index:
|
|
53249
|
+
z-index: var(--navi-z-index-sticky);
|
|
52498
53250
|
order: 2;
|
|
52499
53251
|
}
|
|
52500
53252
|
|
|
@@ -52510,7 +53262,7 @@ const css$v = /* css */`
|
|
|
52510
53262
|
.navi_list_item_group_label {
|
|
52511
53263
|
position: sticky;
|
|
52512
53264
|
top: var(--list-group-label-top, var(--x-list-group-label-top, 0px));
|
|
52513
|
-
z-index:
|
|
53265
|
+
z-index: var(--navi-z-index-sticky);
|
|
52514
53266
|
display: block;
|
|
52515
53267
|
background-color: var(--list-group-label-background-color);
|
|
52516
53268
|
user-select: none;
|
|
@@ -56622,6 +57374,11 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
|
|
|
56622
57374
|
font-size: var(--picker-font-size);
|
|
56623
57375
|
font-family: var(--picker-font-family);
|
|
56624
57376
|
text-align: inherit;
|
|
57377
|
+
/* The frame is drawn by the box, but its radius is declared here, on the
|
|
57378
|
+
control root, like every other navi control does — so anything styling
|
|
57379
|
+
the picker from the outside (a Group squaring the corners it joins) has
|
|
57380
|
+
one element to talk to, and the box follows. */
|
|
57381
|
+
border-radius: var(--picker-border-radius);
|
|
56625
57382
|
|
|
56626
57383
|
.navi_picker_box {
|
|
56627
57384
|
position: relative;
|
|
@@ -56643,7 +57400,7 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
|
|
|
56643
57400
|
border-width: var(--picker-border-width);
|
|
56644
57401
|
border-style: solid;
|
|
56645
57402
|
border-color: var(--x-picker-border-color);
|
|
56646
|
-
border-radius:
|
|
57403
|
+
border-radius: inherit;
|
|
56647
57404
|
outline-width: var(--picker-outline-width);
|
|
56648
57405
|
outline-style: none;
|
|
56649
57406
|
outline-color: var(--picker-outline-color);
|
|
@@ -62536,6 +63293,13 @@ const createIsolatedItemTracker = () => {
|
|
|
62536
63293
|
return [useIsolatedItemTrackerProvider, useTrackIsolatedItem, useTrackedIsolatedItem, useTrackedIsolatedItems];
|
|
62537
63294
|
};
|
|
62538
63295
|
|
|
63296
|
+
/**
|
|
63297
|
+
* The table's own scale: these order the table's parts among themselves and are
|
|
63298
|
+
* derived from each other, never written as literals twice. They stay under the
|
|
63299
|
+
* bands of src/navi_z_indexes.js, which is where anything competing with
|
|
63300
|
+
* another component belongs.
|
|
63301
|
+
*/
|
|
63302
|
+
|
|
62539
63303
|
const Z_INDEX_EDITING = 1; /* To go above neighbours, but should not be too big to stay under the sticky cells */
|
|
62540
63304
|
|
|
62541
63305
|
/* needed because cell uses position:relative, sticky must win even if before in DOM order */
|
|
@@ -67954,7 +68718,7 @@ const css = /* css */`
|
|
|
67954
68718
|
.navi_side_panel_head,
|
|
67955
68719
|
.navi_side_panel_foot {
|
|
67956
68720
|
position: sticky;
|
|
67957
|
-
z-index:
|
|
68721
|
+
z-index: var(--navi-z-index-sticky);
|
|
67958
68722
|
background-color: var(--navi-popup-background-color);
|
|
67959
68723
|
}
|
|
67960
68724
|
/* The separator token, not the popup border: these lines split two
|