@jsenv/navi 0.29.21 → 0.29.23
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/README.md +2 -0
- package/dist/jsenv_navi.js +2275 -454
- package/dist/jsenv_navi.js.map +255 -118
- package/docs/AI_INSTRUCTIONS.md +22 -0
- package/docs/drag_to_travel.md +371 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, installImportMetaCssBuild, coarsePointerSignal } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
import { createContext, isValidElement, h, Fragment, toChildArray, render, options, cloneElement } from "preact";
|
|
7
|
-
import { useContext, useLayoutEffect, useRef,
|
|
7
|
+
import { useContext, useLayoutEffect, useRef, useCallback, useState, useMemo, useId, useEffect, useErrorBoundary } from "preact/hooks";
|
|
8
8
|
import { jsx, jsxs, Fragment as Fragment$1 } from "preact/jsx-runtime";
|
|
9
9
|
import { computed, signal, effect, batch, useSignal } from "@preact/signals";
|
|
10
|
-
import { createPubSub, normalizeStyle, mergeOneStyle, getPositionedParent, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeTwoStyles, normalizeStyles, resolveCSSSize, measureLongestVisualLineWidth, hasCSSSizeUnit, resolveOklchLightness, contrastColor, createIterableWeakSet, getElementSignature, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, createEventGroupLogger, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, getKeyboardEventDefaultAction, chainEvent, activeElementSignal, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, findBefore, findAfter, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation,
|
|
10
|
+
import { createPubSub, normalizeStyle, mergeOneStyle, getPositionedParent, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeTwoStyles, normalizeStyles, resolveCSSSize, measureLongestVisualLineWidth, hasCSSSizeUnit, resolveOklchLightness, contrastColor, createIterableWeakSet, getElementSignature, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, createEventGroupLogger, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, getKeyboardEventDefaultAction, chainEvent, activeElementSignal, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, startDragToTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
11
11
|
export { contrastColor, startDragToReorder } from "@jsenv/dom";
|
|
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";
|
|
@@ -1109,6 +1109,10 @@ naviI18n.addAll({
|
|
|
1109
1109
|
fr: "Ce champ doit contenir au maximum [max] caractères (il contient actuellement [count] caractères).",
|
|
1110
1110
|
en: "This field must contain at most [max] characters (it currently contains [count] characters).",
|
|
1111
1111
|
},
|
|
1112
|
+
"constraint.max_length.selection": {
|
|
1113
|
+
fr: "Sélectionnez au maximum [max] choix ([count] actuellement).",
|
|
1114
|
+
en: "Select at most [max] choices ([count] currently).",
|
|
1115
|
+
},
|
|
1112
1116
|
"constraint.type.number.default": {
|
|
1113
1117
|
fr: "Ce champ doit être un nombre.",
|
|
1114
1118
|
en: "This field must be a number.",
|
|
@@ -1361,6 +1365,11 @@ naviI18n.addAll({
|
|
|
1361
1365
|
fr: "Ce champ ne peut pas contenir plus de [max] caractère[s], une partie a été tronquée.",
|
|
1362
1366
|
en: "This field cannot contain more than [max] character[s]; the value was truncated.",
|
|
1363
1367
|
},
|
|
1368
|
+
// maxLengthGuard on a multiple selection: one more item would exceed the limit
|
|
1369
|
+
"constraint.guard.max_length.selection": {
|
|
1370
|
+
fr: "[max] max.",
|
|
1371
|
+
en: "[max] max.",
|
|
1372
|
+
},
|
|
1364
1373
|
});
|
|
1365
1374
|
|
|
1366
1375
|
// Date/time placeholder tokens — shown when no value is selected
|
|
@@ -9169,7 +9178,7 @@ const useComposeElementRef = (syncElement, externalRef) => {
|
|
|
9169
9178
|
const refCallback = (el) => {
|
|
9170
9179
|
elRef.current = el;
|
|
9171
9180
|
// Keep .current in sync immediately so useEffect callbacks that read
|
|
9172
|
-
// ref.current
|
|
9181
|
+
// ref.current see the element, not null.
|
|
9173
9182
|
refCallback.current = el;
|
|
9174
9183
|
const currentExternalRef = externalRefRef.current;
|
|
9175
9184
|
if (currentExternalRef) {
|
|
@@ -9197,54 +9206,6 @@ const useComposeElementRef = (syncElement, externalRef) => {
|
|
|
9197
9206
|
return refCallback;
|
|
9198
9207
|
};
|
|
9199
9208
|
|
|
9200
|
-
/**
|
|
9201
|
-
* Tracks whether an element is fully visible in its scroll container and sets
|
|
9202
|
-
* the `navi-partially-hidden` attribute when any part of it is clipped.
|
|
9203
|
-
*
|
|
9204
|
-
* This is used to suppress `view-transition-name` on elements that are partially
|
|
9205
|
-
* outside the viewport or a scrollable container. Without this, a partially clipped
|
|
9206
|
-
* element would still participate in view transitions, producing ghost animations or
|
|
9207
|
-
* incorrect cross-fade effects.
|
|
9208
|
-
*
|
|
9209
|
-
* CSS usage:
|
|
9210
|
-
* ```css
|
|
9211
|
-
* [navi-partially-hidden] {
|
|
9212
|
-
* view-transition-name: none !important;
|
|
9213
|
-
* }
|
|
9214
|
-
* ```
|
|
9215
|
-
*
|
|
9216
|
-
* `Box` enables this hook automatically when a `viewTransitionName` prop is provided.
|
|
9217
|
-
*
|
|
9218
|
-
* @param {import("preact").RefObject} ref - Ref to the element to observe.
|
|
9219
|
-
* @param {boolean} enabled - Only observe when true (typically when view-transition-name is set).
|
|
9220
|
-
*/
|
|
9221
|
-
const usePartiallyHidden = (ref, enabled) => {
|
|
9222
|
-
useEffect(() => {
|
|
9223
|
-
const el = ref.current;
|
|
9224
|
-
if (!el || !enabled) {
|
|
9225
|
-
return undefined;
|
|
9226
|
-
}
|
|
9227
|
-
return setupPartiallyHidden(el);
|
|
9228
|
-
}, [enabled]);
|
|
9229
|
-
};
|
|
9230
|
-
|
|
9231
|
-
const setupPartiallyHidden = (el) => {
|
|
9232
|
-
const observer = new IntersectionObserver(
|
|
9233
|
-
([entry]) => {
|
|
9234
|
-
if (entry.intersectionRatio >= 0.99) {
|
|
9235
|
-
el.removeAttribute("navi-partially-hidden");
|
|
9236
|
-
} else {
|
|
9237
|
-
el.setAttribute("navi-partially-hidden", "");
|
|
9238
|
-
}
|
|
9239
|
-
},
|
|
9240
|
-
{ threshold: 0.99 },
|
|
9241
|
-
);
|
|
9242
|
-
observer.observe(el);
|
|
9243
|
-
return () => {
|
|
9244
|
-
observer.disconnect();
|
|
9245
|
-
};
|
|
9246
|
-
};
|
|
9247
|
-
|
|
9248
9209
|
installImportMetaCssBuild(import.meta);/**
|
|
9249
9210
|
* Box - A Swiss Army Knife for Layout
|
|
9250
9211
|
*
|
|
@@ -9477,12 +9438,6 @@ import.meta.css = [/* css */`
|
|
|
9477
9438
|
[hidden] {
|
|
9478
9439
|
display: none !important;
|
|
9479
9440
|
}
|
|
9480
|
-
|
|
9481
|
-
/* Partially hidden (or fully hidden) element should not participate in view transition no matter what */
|
|
9482
|
-
/* Otherwise they appear immedatly and fully visible from a fully/partially hidden state */
|
|
9483
|
-
[navi-partially-hidden] {
|
|
9484
|
-
view-transition-name: none !important;
|
|
9485
|
-
}
|
|
9486
9441
|
`, "@jsenv/navi/src/box/box.jsx"];
|
|
9487
9442
|
const PSEUDO_CLASSES_DEFAULT = [];
|
|
9488
9443
|
const PSEUDO_ELEMENTS_DEFAULT = [];
|
|
@@ -9984,7 +9939,6 @@ const Box = props => {
|
|
|
9984
9939
|
});
|
|
9985
9940
|
}, styleDeps);
|
|
9986
9941
|
finalRef = useComposeElementRef(syncBox, ref);
|
|
9987
|
-
usePartiallyHidden(finalRef, Boolean(rest.viewTransitionName));
|
|
9988
9942
|
}
|
|
9989
9943
|
let innerChildren = children;
|
|
9990
9944
|
if (separator) {
|
|
@@ -10166,7 +10120,7 @@ const setupNetworkMonitoring = () => {
|
|
|
10166
10120
|
};
|
|
10167
10121
|
setupNetworkMonitoring();
|
|
10168
10122
|
|
|
10169
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
10123
|
+
installImportMetaCssBuild(import.meta);const css$_ = /* css */`
|
|
10170
10124
|
.navi_loading_indicator_fluid_container {
|
|
10171
10125
|
position: relative;
|
|
10172
10126
|
display: flex;
|
|
@@ -10198,7 +10152,7 @@ const LoadingIndicatorFluid = ({
|
|
|
10198
10152
|
visuallyHidden,
|
|
10199
10153
|
...rest
|
|
10200
10154
|
}) => {
|
|
10201
|
-
import.meta.css = [css$
|
|
10155
|
+
import.meta.css = [css$_, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
|
|
10202
10156
|
const ref = useRef(null);
|
|
10203
10157
|
// The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
|
|
10204
10158
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
@@ -10403,7 +10357,7 @@ const LoadingRectangleSvg = ({
|
|
|
10403
10357
|
});
|
|
10404
10358
|
};
|
|
10405
10359
|
|
|
10406
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
10360
|
+
installImportMetaCssBuild(import.meta);const css$Z = /* css */`
|
|
10407
10361
|
.navi_loading_outline_wrapper {
|
|
10408
10362
|
position: absolute;
|
|
10409
10363
|
/* Controls place the outline slightly outside their box, right on top of
|
|
@@ -10440,7 +10394,7 @@ installImportMetaCssBuild(import.meta);const css$Y = /* css */`
|
|
|
10440
10394
|
}
|
|
10441
10395
|
`;
|
|
10442
10396
|
const LoadingOutline = props => {
|
|
10443
|
-
import.meta.css = [css$
|
|
10397
|
+
import.meta.css = [css$Z, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
|
|
10444
10398
|
if (props.containerRef) {
|
|
10445
10399
|
const container = props.containerRef.current;
|
|
10446
10400
|
if (!container) {
|
|
@@ -10774,7 +10728,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
|
|
|
10774
10728
|
};
|
|
10775
10729
|
|
|
10776
10730
|
installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
|
|
10777
|
-
const css$
|
|
10731
|
+
const css$Y = /* css */`
|
|
10778
10732
|
@layer navi {
|
|
10779
10733
|
.navi_text {
|
|
10780
10734
|
&[data-skeleton] {
|
|
@@ -11280,7 +11234,7 @@ const TextShrinkWrap = props => {
|
|
|
11280
11234
|
});
|
|
11281
11235
|
};
|
|
11282
11236
|
const TextUI = props => {
|
|
11283
|
-
import.meta.css = [css$
|
|
11237
|
+
import.meta.css = [css$Y, "@jsenv/navi/src/text/text.jsx"];
|
|
11284
11238
|
let {
|
|
11285
11239
|
ref,
|
|
11286
11240
|
spacing,
|
|
@@ -13707,7 +13661,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
13707
13661
|
* - Arrow automatically shows when pointing at a valid anchor element
|
|
13708
13662
|
* - Centers in viewport when no anchor element provided or anchor is too big
|
|
13709
13663
|
*/
|
|
13710
|
-
const css$
|
|
13664
|
+
const css$X = /* css */`
|
|
13711
13665
|
@layer navi {
|
|
13712
13666
|
.navi_callout {
|
|
13713
13667
|
/* A callout is parented to what it explains, so it inherits from it — and
|
|
@@ -13946,7 +13900,7 @@ const openCallout = (message, {
|
|
|
13946
13900
|
skipFocus = false,
|
|
13947
13901
|
debug = () => {}
|
|
13948
13902
|
} = {}) => {
|
|
13949
|
-
import.meta.css = [css$
|
|
13903
|
+
import.meta.css = [css$X, "@jsenv/navi/src/control/rules/callout/callout.js"];
|
|
13950
13904
|
if (debug === true) {
|
|
13951
13905
|
debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
|
|
13952
13906
|
}
|
|
@@ -16607,6 +16561,31 @@ const MAX_LENGTH_CONSTRAINT = {
|
|
|
16607
16561
|
messageAttribute: "data-max-length-message",
|
|
16608
16562
|
check: (field) => {
|
|
16609
16563
|
const type = field.controlHostProps.type ?? "text";
|
|
16564
|
+
// A multiple selection has a length the way a string has one — how many
|
|
16565
|
+
// items it holds — so it wears the same constraint under the same name. The
|
|
16566
|
+
// group answers for it: a checkbox on its own holds one value, not a count.
|
|
16567
|
+
if (field.controlType === "checkbox_group") {
|
|
16568
|
+
const maxLength =
|
|
16569
|
+
field.controlHostProps.maxLength ?? field.props?.maxLengthGuard;
|
|
16570
|
+
if (maxLength === undefined) {
|
|
16571
|
+
return null;
|
|
16572
|
+
}
|
|
16573
|
+
const uiState = field.uiState;
|
|
16574
|
+
if (!Array.isArray(uiState)) {
|
|
16575
|
+
return null;
|
|
16576
|
+
}
|
|
16577
|
+
const count = uiState.length;
|
|
16578
|
+
if (count <= maxLength) {
|
|
16579
|
+
return null;
|
|
16580
|
+
}
|
|
16581
|
+
return {
|
|
16582
|
+
message: naviI18n("constraint.max_length.selection", {
|
|
16583
|
+
max: String(maxLength),
|
|
16584
|
+
count: String(count),
|
|
16585
|
+
}),
|
|
16586
|
+
target: field.ref.current,
|
|
16587
|
+
};
|
|
16588
|
+
}
|
|
16610
16589
|
const isInput =
|
|
16611
16590
|
field.controlType === "input" || field.controlType === "picker";
|
|
16612
16591
|
const isTextarea =
|
|
@@ -18144,6 +18123,23 @@ const READONLY_CONSTRAINT = {
|
|
|
18144
18123
|
return null;
|
|
18145
18124
|
}
|
|
18146
18125
|
|
|
18126
|
+
// A selection guarding its length (see maxLengthGuard) is what holds this
|
|
18127
|
+
// one back, so max_length is what refuses it: same name, same message, same
|
|
18128
|
+
// `maxLengthMessage` to say it in the caller's own words. Read-only is only
|
|
18129
|
+
// how it is expressed on the item.
|
|
18130
|
+
const parent = field.parentUIStateController;
|
|
18131
|
+
if (parent?.isChildBlockedByMaxLengthGuard?.(field)) {
|
|
18132
|
+
return {
|
|
18133
|
+
name: MAX_LENGTH_CONSTRAINT.name,
|
|
18134
|
+
constraint: MAX_LENGTH_CONSTRAINT,
|
|
18135
|
+
message: naviI18n("constraint.guard.max_length.selection", {
|
|
18136
|
+
max: String(parent.props.maxLengthGuard),
|
|
18137
|
+
}),
|
|
18138
|
+
status: "info",
|
|
18139
|
+
ignoredByParents: true,
|
|
18140
|
+
};
|
|
18141
|
+
}
|
|
18142
|
+
|
|
18147
18143
|
// A readonly element does not block its parent from submitting — mirrors
|
|
18148
18144
|
// standard HTML form behaviour where readonly inputs are submitted as-is.
|
|
18149
18145
|
return {
|
|
@@ -21426,6 +21422,25 @@ const useUIGroupStateController = (
|
|
|
21426
21422
|
onActionError: (e) => {
|
|
21427
21423
|
controller.rules.validation.syncValidity(e, { report: true });
|
|
21428
21424
|
},
|
|
21425
|
+
// Whether `maxLengthGuard` stands between this child and the selection.
|
|
21426
|
+
// A guard on the gesture only: what the group already holds is left
|
|
21427
|
+
// alone, however long, and a selected child is never blocked — it must
|
|
21428
|
+
// stay takeable back. Read off the signal so the other children learn
|
|
21429
|
+
// about the group filling up and emptying again.
|
|
21430
|
+
isChildBlockedByMaxLengthGuard: (childUIStateController) => {
|
|
21431
|
+
const { maxLengthGuard } = controller.props;
|
|
21432
|
+
if (maxLengthGuard === undefined) {
|
|
21433
|
+
return false;
|
|
21434
|
+
}
|
|
21435
|
+
if (childUIStateController.uiState !== undefined) {
|
|
21436
|
+
return false;
|
|
21437
|
+
}
|
|
21438
|
+
const uiState = uiStateSignal.value;
|
|
21439
|
+
if (!Array.isArray(uiState)) {
|
|
21440
|
+
return false;
|
|
21441
|
+
}
|
|
21442
|
+
return uiState.length >= maxLengthGuard;
|
|
21443
|
+
},
|
|
21429
21444
|
findChildById: (searchId) => {
|
|
21430
21445
|
for (const c of childUIStateControllerArray) {
|
|
21431
21446
|
if (c.id === searchId) return c;
|
|
@@ -22779,6 +22794,10 @@ const useControlgroupProps = (props, {
|
|
|
22779
22794
|
// useful to children, not the the group itself
|
|
22780
22795
|
"required": undefined,
|
|
22781
22796
|
// useful to children, not the the group itself
|
|
22797
|
+
// How many items the group accepts, read by its controller and by the
|
|
22798
|
+
// children asking whether there is still room for them. Not an attribute
|
|
22799
|
+
// any element wears: a <fieldset maxlength> means nothing.
|
|
22800
|
+
"maxLength": undefined,
|
|
22782
22801
|
"onnavi_action_allowed": e => {
|
|
22783
22802
|
setActionRequester(e.detail.requester);
|
|
22784
22803
|
controlgroupProps.onnavi_action_allowed(e);
|
|
@@ -22961,7 +22980,12 @@ const useInteractiveProps = (props, {
|
|
|
22961
22980
|
// on its own say-so.
|
|
22962
22981
|
const loadingFromParent = Boolean(controlLoading && parentActionRequester === ref.current);
|
|
22963
22982
|
const loadingBase = loading || loadingFromParent;
|
|
22964
|
-
|
|
22983
|
+
// Read-only because the selection above guards its length
|
|
22984
|
+
// (`maxLengthGuard`) and this one would make it longer: it can be pointed
|
|
22985
|
+
// at, focused and pressed — and answers why (see readonly_constraint.js) —
|
|
22986
|
+
// but cannot be taken.
|
|
22987
|
+
const readOnlyFromParentMaxLengthGuard = Boolean(uiStateController.parentUIStateController?.isChildBlockedByMaxLengthGuard?.(uiStateController));
|
|
22988
|
+
const readOnlyBase = readOnly || controlReadOnly || loadingBase || readOnlyFromParentMaxLengthGuard || controlInfo.readOnlyUncontrolled;
|
|
22965
22989
|
const loadingResolved = loadingBase || actionStatus.loading;
|
|
22966
22990
|
const readOnlyResolved = readOnlyBase || actionStatus.loading;
|
|
22967
22991
|
// Both halves of "busy" that do not come from the bound action, kept apart
|
|
@@ -23295,7 +23319,7 @@ const getAssociatedLabels = element => {
|
|
|
23295
23319
|
return Array.from(element.labels);
|
|
23296
23320
|
};
|
|
23297
23321
|
|
|
23298
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
23322
|
+
installImportMetaCssBuild(import.meta);const css$W = /* css */`
|
|
23299
23323
|
@layer navi {
|
|
23300
23324
|
.navi_button {
|
|
23301
23325
|
--button-border-radius: var(--navi-control-border-radius);
|
|
@@ -23688,7 +23712,7 @@ installImportMetaCssBuild(import.meta);const css$V = /* css */`
|
|
|
23688
23712
|
}
|
|
23689
23713
|
`;
|
|
23690
23714
|
const ButtonUI = props => {
|
|
23691
|
-
import.meta.css = [css$
|
|
23715
|
+
import.meta.css = [css$W, "@jsenv/navi/src/control/input/button_ui.jsx"];
|
|
23692
23716
|
const {
|
|
23693
23717
|
ref,
|
|
23694
23718
|
// href/link
|
|
@@ -25902,7 +25926,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
25902
25926
|
* reaches the real container.
|
|
25903
25927
|
*/
|
|
25904
25928
|
let openLocalDialogCount = 0;
|
|
25905
|
-
const css$
|
|
25929
|
+
const css$V = /* css */`
|
|
25906
25930
|
@layer navi {
|
|
25907
25931
|
.navi_dialog {
|
|
25908
25932
|
/* Min gap between the dialog and the edges of its container. Written
|
|
@@ -26317,7 +26341,7 @@ const css$U = /* css */`
|
|
|
26317
26341
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
26318
26342
|
*/
|
|
26319
26343
|
const Dialog = props => {
|
|
26320
|
-
import.meta.css = [css$
|
|
26344
|
+
import.meta.css = [css$V, "@jsenv/navi/src/layout/dialog.jsx"];
|
|
26321
26345
|
if (props.openController) {
|
|
26322
26346
|
return jsx(ControlledDialog, {
|
|
26323
26347
|
...props
|
|
@@ -27150,7 +27174,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
27150
27174
|
* and applied.
|
|
27151
27175
|
*/
|
|
27152
27176
|
let openLocalPopoverCount = 0;
|
|
27153
|
-
const css$
|
|
27177
|
+
const css$U = /* css */`
|
|
27154
27178
|
@layer navi {
|
|
27155
27179
|
.navi_popover {
|
|
27156
27180
|
/* soft: user-configurable preferred max-height. Kept as a *default*
|
|
@@ -27525,7 +27549,7 @@ const css$T = /* css */`
|
|
|
27525
27549
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
27526
27550
|
*/
|
|
27527
27551
|
const Popover = props => {
|
|
27528
|
-
import.meta.css = [css$
|
|
27552
|
+
import.meta.css = [css$U, "@jsenv/navi/src/layout/popover.jsx"];
|
|
27529
27553
|
if (props.openController) {
|
|
27530
27554
|
return jsx(ControlledPopover, {
|
|
27531
27555
|
...props
|
|
@@ -28494,7 +28518,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
28494
28518
|
* event, and a caller replacing the body entirely then has one protocol to
|
|
28495
28519
|
* follow — `--navi-confirm` for yes, anything that closes for no.
|
|
28496
28520
|
*/
|
|
28497
|
-
const css$
|
|
28521
|
+
const css$T = /* css */`
|
|
28498
28522
|
/* The width lives on the body rather than on the popup, so that custom
|
|
28499
28523
|
content (which replaces this body entirely) sizes itself instead of
|
|
28500
28524
|
inheriting a ceiling meant for a sentence-long question. */
|
|
@@ -28631,7 +28655,7 @@ const ConfirmPopup = ({
|
|
|
28631
28655
|
onAnswer,
|
|
28632
28656
|
onClosed
|
|
28633
28657
|
}) => {
|
|
28634
|
-
import.meta.css = [css$
|
|
28658
|
+
import.meta.css = [css$T, "@jsenv/navi/src/action/confirm_popup.jsx"];
|
|
28635
28659
|
const {
|
|
28636
28660
|
mode,
|
|
28637
28661
|
confirmLabel,
|
|
@@ -28715,7 +28739,7 @@ const defaultBody = (message, {
|
|
|
28715
28739
|
});
|
|
28716
28740
|
};
|
|
28717
28741
|
|
|
28718
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
28742
|
+
installImportMetaCssBuild(import.meta);const css$S = /* css */`
|
|
28719
28743
|
.action_error {
|
|
28720
28744
|
margin-top: 0;
|
|
28721
28745
|
margin-bottom: 20px;
|
|
@@ -28740,7 +28764,7 @@ const ActionRenderer = ({
|
|
|
28740
28764
|
children,
|
|
28741
28765
|
disabled
|
|
28742
28766
|
}) => {
|
|
28743
|
-
import.meta.css = [css$
|
|
28767
|
+
import.meta.css = [css$S, "@jsenv/navi/src/action/action_renderer.jsx"];
|
|
28744
28768
|
if (action === undefined) {
|
|
28745
28769
|
throw new Error("ActionRenderer requires an action to render, but none was provided.");
|
|
28746
28770
|
}
|
|
@@ -32510,7 +32534,7 @@ const swapArrayItemByIndex = (array, indexA, indexB) => {
|
|
|
32510
32534
|
*/
|
|
32511
32535
|
const ensureDocumentStartViewTransition = () => {
|
|
32512
32536
|
if (!document.startViewTransition) {
|
|
32513
|
-
|
|
32537
|
+
const startViewTransitionPolyfill = (updateCallback) => {
|
|
32514
32538
|
updateCallback();
|
|
32515
32539
|
return {
|
|
32516
32540
|
updateCallbackDone: Promise.resolve(),
|
|
@@ -32519,11 +32543,41 @@ const ensureDocumentStartViewTransition = () => {
|
|
|
32519
32543
|
skipTransition: () => {},
|
|
32520
32544
|
};
|
|
32521
32545
|
};
|
|
32522
|
-
|
|
32523
|
-
|
|
32546
|
+
// Said out loud, because the difference matters to whoever needs the
|
|
32547
|
+
// transition itself rather than the change: there is no picture of the
|
|
32548
|
+
// state being left here, so nothing can be animated between the two — and
|
|
32549
|
+
// once this is installed, asking the document is no longer a way to know.
|
|
32550
|
+
startViewTransitionPolyfill.isPolyfill = true;
|
|
32551
|
+
document.startViewTransition = startViewTransitionPolyfill;
|
|
32552
|
+
}
|
|
32553
|
+
return startViewTransition$1;
|
|
32554
|
+
};
|
|
32555
|
+
|
|
32556
|
+
// A transition a finger is holding still (see route_travel.jsx): it has to be
|
|
32557
|
+
// let go of before any other one starts, and this is the only place that knows
|
|
32558
|
+
// a new one is about to. There is a single transition per document — starting
|
|
32559
|
+
// one SKIPS the one in flight — and the hold that keeps pictures under a finger
|
|
32560
|
+
// is written against whatever transition is running, because everything the
|
|
32561
|
+
// gesture must carry along (a trait under a tab row) belongs to that same
|
|
32562
|
+
// transition and has no name of its own here. Left on, it takes hold of the
|
|
32563
|
+
// transition that has just replaced ours: born paused, held by nobody, it never
|
|
32564
|
+
// finishes, and its pictures stand over a page that cannot be touched anymore.
|
|
32565
|
+
let releaseHeldViewTransition = null;
|
|
32566
|
+
const holdViewTransition = (release) => {
|
|
32567
|
+
releaseHeldViewTransition = release;
|
|
32568
|
+
return () => {
|
|
32569
|
+
if (releaseHeldViewTransition === release) {
|
|
32570
|
+
releaseHeldViewTransition = null;
|
|
32571
|
+
}
|
|
32572
|
+
};
|
|
32524
32573
|
};
|
|
32525
32574
|
|
|
32526
|
-
const startViewTransition = (updateCallback) => {
|
|
32575
|
+
const startViewTransition$1 = (updateCallback) => {
|
|
32576
|
+
if (releaseHeldViewTransition) {
|
|
32577
|
+
const release = releaseHeldViewTransition;
|
|
32578
|
+
releaseHeldViewTransition = null;
|
|
32579
|
+
release();
|
|
32580
|
+
}
|
|
32527
32581
|
const viewTransition = document.startViewTransition(updateCallback);
|
|
32528
32582
|
viewTransition.updateCallbackDone.catch(ignoreSkip);
|
|
32529
32583
|
viewTransition.ready.catch(ignoreSkip);
|
|
@@ -33722,6 +33776,29 @@ const updateDocumentState = (value) => {
|
|
|
33722
33776
|
documentStateSignal.value = value;
|
|
33723
33777
|
};
|
|
33724
33778
|
|
|
33779
|
+
/**
|
|
33780
|
+
* A navigation is ABOUT to be applied — said before its very first write.
|
|
33781
|
+
*
|
|
33782
|
+
* Everything else a router says arrives once the change is made: a route
|
|
33783
|
+
* announces that it matches, an action that it is running. That is too late for
|
|
33784
|
+
* anyone who needs the page as it stands BEFORE, and the browser's view
|
|
33785
|
+
* transitions are exactly that kind of reader — the picture they keep of the
|
|
33786
|
+
* page being left is taken at the next frame, and a render answering a signal
|
|
33787
|
+
* written a moment ago is already in the DOM by then (see route_travel.jsx).
|
|
33788
|
+
*
|
|
33789
|
+
* So this is the one moment where nothing has moved yet. It is published
|
|
33790
|
+
* synchronously, from the top of the navigation, and whoever listens runs
|
|
33791
|
+
* before the URL, the visited set, or any route has changed.
|
|
33792
|
+
*
|
|
33793
|
+
* The other end is published too, and for the same kind of reader: whoever
|
|
33794
|
+
* held something across the change and has nobody to hand it to gets a moment
|
|
33795
|
+
* to let go of it that does not depend on guessing how long the change takes.
|
|
33796
|
+
*/
|
|
33797
|
+
|
|
33798
|
+
|
|
33799
|
+
const [publishBeforeRouting, observeBeforeRouting] = createPubSub();
|
|
33800
|
+
const [publishAfterRouting, observeAfterRouting] = createPubSub();
|
|
33801
|
+
|
|
33725
33802
|
const setupBrowserIntegrationViaHistory = ({
|
|
33726
33803
|
applyActions,
|
|
33727
33804
|
applyRouting,
|
|
@@ -33773,6 +33850,19 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
33773
33850
|
|
|
33774
33851
|
let abortController = null;
|
|
33775
33852
|
const handleRoutingTask = (url, options) => {
|
|
33853
|
+
// Before anything is written: the visited set, the URL and every route are
|
|
33854
|
+
// about to change, and this is the last moment the page still stands as it
|
|
33855
|
+
// was. And after, whichever way the change went out — so that whoever took
|
|
33856
|
+
// something at the first announcement has a definite place to give it back.
|
|
33857
|
+
publishBeforeRouting({ url, ...options });
|
|
33858
|
+
try {
|
|
33859
|
+
return applyRoutingTask(url, options);
|
|
33860
|
+
} finally {
|
|
33861
|
+
publishAfterRouting({ url, ...options });
|
|
33862
|
+
}
|
|
33863
|
+
};
|
|
33864
|
+
|
|
33865
|
+
const applyRoutingTask = (url, options) => {
|
|
33776
33866
|
const isSameUrl = url === window.location.href;
|
|
33777
33867
|
const {
|
|
33778
33868
|
reason,
|
|
@@ -34342,6 +34432,30 @@ const Head = ({
|
|
|
34342
34432
|
* ```
|
|
34343
34433
|
*/
|
|
34344
34434
|
|
|
34435
|
+
const [publishRouteRender, observeRouteRender] = createPubSub();
|
|
34436
|
+
|
|
34437
|
+
/**
|
|
34438
|
+
* Keep every container showing the page it is showing, whatever the routes say.
|
|
34439
|
+
*
|
|
34440
|
+
* For a caller whose picture of a page is LIVE and must not follow the router:
|
|
34441
|
+
* a travel being undone shows the page it is going back to on both sides at
|
|
34442
|
+
* once if the router is allowed to swap under it (see route_travel.jsx). Only
|
|
34443
|
+
* the pages are held still — the rest of the document goes on rendering, which
|
|
34444
|
+
* is the whole reason this lives here rather than in Preact's own scheduler.
|
|
34445
|
+
*/
|
|
34446
|
+
let routeRenderFrozen = false;
|
|
34447
|
+
const routeRenderFrozenSignal = signal(0);
|
|
34448
|
+
const freezeRouteRender = () => {
|
|
34449
|
+
routeRenderFrozen = true;
|
|
34450
|
+
return () => {
|
|
34451
|
+
if (!routeRenderFrozen) {
|
|
34452
|
+
return;
|
|
34453
|
+
}
|
|
34454
|
+
routeRenderFrozen = false;
|
|
34455
|
+
// Read during every container's render, so letting go brings them all back.
|
|
34456
|
+
routeRenderFrozenSignal.value++;
|
|
34457
|
+
};
|
|
34458
|
+
};
|
|
34345
34459
|
const debug$1 = (...args) => {
|
|
34346
34460
|
{
|
|
34347
34461
|
return;
|
|
@@ -34362,9 +34476,51 @@ const Route = props => {
|
|
|
34362
34476
|
...props
|
|
34363
34477
|
});
|
|
34364
34478
|
};
|
|
34479
|
+
/**
|
|
34480
|
+
* The routes a tree of <Route> children is made of, in the order they are
|
|
34481
|
+
* written. Reading them is what turns a router into a row one can walk: "one
|
|
34482
|
+
* step that way" is a fact about the order the branches were declared in, and
|
|
34483
|
+
* nothing in a URL says it.
|
|
34484
|
+
*
|
|
34485
|
+
* The same walk the container does to find the active branch (collectBranches),
|
|
34486
|
+
* except that it keeps every leaf rather than the one that matches — and reads
|
|
34487
|
+
* no signal, so asking does not subscribe the asker to anything.
|
|
34488
|
+
*/
|
|
34489
|
+
const collectRoutes = children => {
|
|
34490
|
+
const routes = [];
|
|
34491
|
+
const visit = child => {
|
|
34492
|
+
if (!child || child === true || child === false) {
|
|
34493
|
+
return;
|
|
34494
|
+
}
|
|
34495
|
+
if (Array.isArray(child)) {
|
|
34496
|
+
for (const item of child) {
|
|
34497
|
+
visit(item);
|
|
34498
|
+
}
|
|
34499
|
+
return;
|
|
34500
|
+
}
|
|
34501
|
+
if (child.type !== Route) {
|
|
34502
|
+
return;
|
|
34503
|
+
}
|
|
34504
|
+
const {
|
|
34505
|
+
children: nodeChildren,
|
|
34506
|
+
route
|
|
34507
|
+
} = child.props;
|
|
34508
|
+
if (nodeChildren) {
|
|
34509
|
+
visit(nodeChildren);
|
|
34510
|
+
return;
|
|
34511
|
+
}
|
|
34512
|
+
if (route) {
|
|
34513
|
+
routes.push(route);
|
|
34514
|
+
}
|
|
34515
|
+
};
|
|
34516
|
+
visit(children);
|
|
34517
|
+
return routes;
|
|
34518
|
+
};
|
|
34519
|
+
|
|
34365
34520
|
// RouteContainer: traverses children statically per render, finds the active branch,
|
|
34366
34521
|
// and renders only that branch — or the fallback if nothing matches.
|
|
34367
|
-
// No
|
|
34522
|
+
// No contexts, no state of its own: it reads the route signals directly, and
|
|
34523
|
+
// its one effect only tells the outside what it has just done.
|
|
34368
34524
|
const RouteContainer = ({
|
|
34369
34525
|
id,
|
|
34370
34526
|
element,
|
|
@@ -34374,8 +34530,24 @@ const RouteContainer = ({
|
|
|
34374
34530
|
const {
|
|
34375
34531
|
activeBranch
|
|
34376
34532
|
} = collectBranches(children);
|
|
34377
|
-
|
|
34378
|
-
|
|
34533
|
+
|
|
34534
|
+
// Told to hold still: what is on screen stays on screen. Kept as the very
|
|
34535
|
+
// vnode that was rendered last time, which is how Preact is told there is
|
|
34536
|
+
// nothing to look at in that subtree.
|
|
34537
|
+
const frozen = routeRenderFrozenSignal.value >= 0 && routeRenderFrozen;
|
|
34538
|
+
const shownBranchRef = useRef(null);
|
|
34539
|
+
if (!frozen) {
|
|
34540
|
+
shownBranchRef.current = activeBranch;
|
|
34541
|
+
}
|
|
34542
|
+
const branch = shownBranchRef.current || activeBranch;
|
|
34543
|
+
|
|
34544
|
+
// The one effect here, and it says the only thing this component knows that
|
|
34545
|
+
// nobody outside can find out: the branch it chose is now in the DOM.
|
|
34546
|
+
useLayoutEffect(() => {
|
|
34547
|
+
publishRouteRender();
|
|
34548
|
+
});
|
|
34549
|
+
debug$1(`[container "${id}"] RENDER, active=${branch ? branch.type : "none"}`);
|
|
34550
|
+
const content = branch ? branch.node : null;
|
|
34379
34551
|
if (!content) {
|
|
34380
34552
|
return null;
|
|
34381
34553
|
}
|
|
@@ -34496,6 +34668,1219 @@ const RouteUI = ({
|
|
|
34496
34668
|
return element;
|
|
34497
34669
|
};
|
|
34498
34670
|
|
|
34671
|
+
installImportMetaCssBuild(import.meta);/**
|
|
34672
|
+
* Dragging from one route to the next, when the tabs of a page are URLs.
|
|
34673
|
+
*
|
|
34674
|
+
* A swipe shows two pages at once, and the router shows one: it mounts the
|
|
34675
|
+
* branch that matches the URL and nothing else, which is what makes a page
|
|
34676
|
+
* shareable, reloadable and findable in the history. Both are right, so the
|
|
34677
|
+
* second picture is not taken from the DOM — it is taken from the SCREEN. The
|
|
34678
|
+
* browser's view transitions keep a picture of the page being left, and this
|
|
34679
|
+
* hands that picture to the finger:
|
|
34680
|
+
*
|
|
34681
|
+
* the first pixel of drag the URL changes (replaced, not pushed) and
|
|
34682
|
+
* the browser freezes the page being left
|
|
34683
|
+
* while the finger moves the two pictures are dragged, the old one a
|
|
34684
|
+
* still, the new one live under it
|
|
34685
|
+
* let go the movement plays out to the end…
|
|
34686
|
+
* let go too early …or backwards, and the URL is put back
|
|
34687
|
+
*
|
|
34688
|
+
* So the URL leads and the picture follows, which is the opposite of what one
|
|
34689
|
+
* would write by hand and the only order the router allows: nothing is ever
|
|
34690
|
+
* mounted that does not match. The page being pulled in is therefore mounting
|
|
34691
|
+
* WHILE it is being dragged in — it arrives as its own loading state and fills
|
|
34692
|
+
* in under the finger, which is honest about what is happening (it is being
|
|
34693
|
+
* fetched) and is the only thing that can happen without a second router.
|
|
34694
|
+
*
|
|
34695
|
+
* What travels is decided at the first pixel, like the axis: a travel brings in
|
|
34696
|
+
* ONE neighbour, and turning the hand around mid-drag puts the current page
|
|
34697
|
+
* back rather than fetching the other side. A hand that walks a whole page
|
|
34698
|
+
* across and keeps going is not turning around though — it is asking for the
|
|
34699
|
+
* next one, and the gesture relays into a second travel without being let go
|
|
34700
|
+
* of (see onEdge).
|
|
34701
|
+
*
|
|
34702
|
+
* Anything else that must follow the gesture — the trait under a tab bar, a
|
|
34703
|
+
* header — follows by being NAMED, not by being told: give it a
|
|
34704
|
+
* `view-transition-name` of its own and the browser animates it from where it
|
|
34705
|
+
* was to where it is, on the same clock as the pages. That is why the tab row
|
|
34706
|
+
* can stay where it is, outside this box, and still move with the finger.
|
|
34707
|
+
*/
|
|
34708
|
+
const CAN_KEEP_PICTURE = Boolean(document.startViewTransition && !document.startViewTransition.isPolyfill);
|
|
34709
|
+
const startViewTransition = ensureDocumentStartViewTransition();
|
|
34710
|
+
const TRAVEL_ATTRIBUTE = "data-navi-route-travel";
|
|
34711
|
+
// While a finger holds the travel: the pictures stand still and go exactly
|
|
34712
|
+
// where it says (see the CSS, and scrubTravel).
|
|
34713
|
+
const HOLD_ATTRIBUTE = "data-navi-route-travel-held";
|
|
34714
|
+
// A travel a finger set off, for its whole life — including what plays out
|
|
34715
|
+
// after the finger is gone. It moves by another law than one asked for by a
|
|
34716
|
+
// press (see the CSS).
|
|
34717
|
+
const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
|
|
34718
|
+
// A travel that changed its mind about where it was going (see
|
|
34719
|
+
// redirectTravel). Only the pages can be aimed somewhere else: everything the
|
|
34720
|
+
// transition carries was measured once, at the start, against a destination
|
|
34721
|
+
// this travel is no longer going to.
|
|
34722
|
+
const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
|
|
34723
|
+
const css$R = /* css */`
|
|
34724
|
+
.navi_route_travel {
|
|
34725
|
+
position: relative;
|
|
34726
|
+
/* Named, so the page inside this box is a picture of its own during a
|
|
34727
|
+
transition rather than part of the one big picture the document takes:
|
|
34728
|
+
the two pages can then move past each other while everything else stays
|
|
34729
|
+
where it is. */
|
|
34730
|
+
view-transition-name: navi-route-travel;
|
|
34731
|
+
/* The gesture takes the axis the pages travel on and leaves the other one
|
|
34732
|
+
to the page, so a list still scrolls under the same finger. */
|
|
34733
|
+
touch-action: pan-y;
|
|
34734
|
+
}
|
|
34735
|
+
.navi_route_travel[data-axis="y"] {
|
|
34736
|
+
touch-action: pan-x;
|
|
34737
|
+
}
|
|
34738
|
+
|
|
34739
|
+
/* Only while a travel of OURS is playing: everything below changes how the
|
|
34740
|
+
document animates, and the document belongs to the application the rest of
|
|
34741
|
+
the time. */
|
|
34742
|
+
:root[${TRAVEL_ATTRIBUTE}] {
|
|
34743
|
+
/* The page around the box is NOT taken as a picture, against the browser's
|
|
34744
|
+
own default: an element that has been captured is not painted where it
|
|
34745
|
+
was and cannot be pointed at either — every press lands on the document
|
|
34746
|
+
root instead. Capturing the whole page therefore freezes it in both
|
|
34747
|
+
senses at once, and a tab row beside a travel is dead for the length of
|
|
34748
|
+
every travel: nothing highlights, the cursor is an arrow, a press on the
|
|
34749
|
+
tab one changed one's mind about goes nowhere.
|
|
34750
|
+
|
|
34751
|
+
Left live, the page around answers as it always did, and nothing shows
|
|
34752
|
+
through where the pages are: the box IS captured (it is named below), so
|
|
34753
|
+
it paints nothing of its own, and the two pictures cover its rectangle
|
|
34754
|
+
between them at every moment of the travel. */
|
|
34755
|
+
view-transition-name: none;
|
|
34756
|
+
|
|
34757
|
+
/* The pictures are looked at, never touched: they are drawn in the top
|
|
34758
|
+
layer, above everything, so a hand reaching for a page that is still
|
|
34759
|
+
sliding would land on the picture of it and the box below would never
|
|
34760
|
+
hear the press. Nothing here is interactive — what the finger is
|
|
34761
|
+
reaching for is the travel underneath, and it must reach it. */
|
|
34762
|
+
&::view-transition,
|
|
34763
|
+
&::view-transition-group(*),
|
|
34764
|
+
&::view-transition-image-pair(*),
|
|
34765
|
+
&::view-transition-old(*),
|
|
34766
|
+
&::view-transition-new(*) {
|
|
34767
|
+
pointer-events: none;
|
|
34768
|
+
}
|
|
34769
|
+
|
|
34770
|
+
&::view-transition-old(navi-route-travel),
|
|
34771
|
+
&::view-transition-new(navi-route-travel) {
|
|
34772
|
+
height: 100%;
|
|
34773
|
+
/* The default cross-fade, dropped: two pages sliding past each other are
|
|
34774
|
+
two solid things, and seeing through one to the other says they are the
|
|
34775
|
+
same page changing its mind. */
|
|
34776
|
+
mix-blend-mode: normal;
|
|
34777
|
+
}
|
|
34778
|
+
/* The pages are cut at the edge of the box they travel in. Said HERE and
|
|
34779
|
+
nowhere else: these pictures are drawn in the top layer, so no overflow
|
|
34780
|
+
on any element of the document — not the box's own, not a frame around
|
|
34781
|
+
it — can reach them. Without it a page being pulled in is seen sliding
|
|
34782
|
+
across whatever sits beside the box. */
|
|
34783
|
+
&::view-transition-group(navi-route-travel),
|
|
34784
|
+
&::view-transition-image-pair(navi-route-travel) {
|
|
34785
|
+
overflow: clip;
|
|
34786
|
+
}
|
|
34787
|
+
&::view-transition-group(navi-route-travel) {
|
|
34788
|
+
animation-duration: var(--navi-route-travel-duration, 300ms);
|
|
34789
|
+
}
|
|
34790
|
+
}
|
|
34791
|
+
|
|
34792
|
+
/* Held by a finger: nothing moves on its own, and where the pictures stand is
|
|
34793
|
+
set by hand (scrubTravel). In CSS rather than paused in JS because JS
|
|
34794
|
+
cannot pause what does not exist yet: a transition is ready several frames
|
|
34795
|
+
after it is asked for — a navigation and a render later — and those frames
|
|
34796
|
+
are the beginning of the gesture. Played at their own pace, a quick swipe
|
|
34797
|
+
would be over before it was ever taken in hand, which is exactly what one
|
|
34798
|
+
sees: the page arriving lands at once instead of following the thumb. */
|
|
34799
|
+
:root[${HOLD_ATTRIBUTE}] {
|
|
34800
|
+
&::view-transition-group(*),
|
|
34801
|
+
&::view-transition-old(*),
|
|
34802
|
+
&::view-transition-new(*) {
|
|
34803
|
+
animation-play-state: paused;
|
|
34804
|
+
}
|
|
34805
|
+
}
|
|
34806
|
+
|
|
34807
|
+
/* Longhands, never the \`animation\` shorthand: the shorthand also writes
|
|
34808
|
+
animation-play-state, so it would set these back to running and undo the
|
|
34809
|
+
hold above — a finger would then watch the pages travel on their own. */
|
|
34810
|
+
:root[${TRAVEL_ATTRIBUTE}] {
|
|
34811
|
+
&::view-transition-old(navi-route-travel),
|
|
34812
|
+
&::view-transition-new(navi-route-travel) {
|
|
34813
|
+
animation-duration: var(--navi-route-travel-duration, 300ms);
|
|
34814
|
+
animation-timing-function: ease;
|
|
34815
|
+
animation-fill-mode: both;
|
|
34816
|
+
}
|
|
34817
|
+
}
|
|
34818
|
+
|
|
34819
|
+
/* A travel that turned around takes its pages with it and nothing else. Every
|
|
34820
|
+
other thing the transition carries — a bar under a tab row, a header — was
|
|
34821
|
+
PHOTOGRAPHED when the transition began: where it stood, and where it was
|
|
34822
|
+
going to stand. Both are fixed, and the second one is now a place nobody is
|
|
34823
|
+
going to. Worse, the thing itself has moved on in the live page (the bar is
|
|
34824
|
+
already under the tab one is heading for), so the picture and the thing are
|
|
34825
|
+
in two places at once — and two bars is what one sees.
|
|
34826
|
+
|
|
34827
|
+
So the pictures of everything that is not the pages are dropped, and those
|
|
34828
|
+
things are simply left where they are, live. A jump rather than a slide, on
|
|
34829
|
+
the one gesture that cannot have both. */
|
|
34830
|
+
:root[${TURNED_ATTRIBUTE}] {
|
|
34831
|
+
&::view-transition-group(*) {
|
|
34832
|
+
display: none;
|
|
34833
|
+
}
|
|
34834
|
+
/* …except the pages, which are what a travel is about. Listed after, so it
|
|
34835
|
+
wins on order rather than on a specificity war. */
|
|
34836
|
+
&::view-transition-group(navi-route-travel) {
|
|
34837
|
+
display: block;
|
|
34838
|
+
}
|
|
34839
|
+
}
|
|
34840
|
+
|
|
34841
|
+
/* Under a finger, the pace IS the finger: an eased travel would run ahead of
|
|
34842
|
+
it in the middle of the gesture and lag behind it at the ends, and what one
|
|
34843
|
+
feels then is the page leaving on its own rather than being pushed. The
|
|
34844
|
+
curve of the movement is the hand's, and it is already in the pull. Kept
|
|
34845
|
+
linear once it is let go of too: changing the curve of an animation that is
|
|
34846
|
+
halfway through moves the picture without anything having moved. */
|
|
34847
|
+
:root[${DRAGGED_ATTRIBUTE}] {
|
|
34848
|
+
&::view-transition-group(navi-route-travel),
|
|
34849
|
+
&::view-transition-old(navi-route-travel),
|
|
34850
|
+
&::view-transition-new(navi-route-travel) {
|
|
34851
|
+
animation-timing-function: linear;
|
|
34852
|
+
}
|
|
34853
|
+
}
|
|
34854
|
+
:root[${TRAVEL_ATTRIBUTE}="forward"] {
|
|
34855
|
+
&::view-transition-old(navi-route-travel) {
|
|
34856
|
+
animation-name: navi-route-travel-leave-towards-start;
|
|
34857
|
+
}
|
|
34858
|
+
&::view-transition-new(navi-route-travel) {
|
|
34859
|
+
animation-name: navi-route-travel-enter-from-end;
|
|
34860
|
+
}
|
|
34861
|
+
}
|
|
34862
|
+
:root[${TRAVEL_ATTRIBUTE}="back"] {
|
|
34863
|
+
&::view-transition-old(navi-route-travel) {
|
|
34864
|
+
animation-name: navi-route-travel-leave-towards-end;
|
|
34865
|
+
}
|
|
34866
|
+
&::view-transition-new(navi-route-travel) {
|
|
34867
|
+
animation-name: navi-route-travel-enter-from-start;
|
|
34868
|
+
}
|
|
34869
|
+
}
|
|
34870
|
+
|
|
34871
|
+
@keyframes navi-route-travel-leave-towards-start {
|
|
34872
|
+
from {
|
|
34873
|
+
translate: 0 0;
|
|
34874
|
+
}
|
|
34875
|
+
to {
|
|
34876
|
+
translate: -100% 0;
|
|
34877
|
+
}
|
|
34878
|
+
}
|
|
34879
|
+
@keyframes navi-route-travel-enter-from-end {
|
|
34880
|
+
from {
|
|
34881
|
+
translate: 100% 0;
|
|
34882
|
+
}
|
|
34883
|
+
to {
|
|
34884
|
+
translate: 0 0;
|
|
34885
|
+
}
|
|
34886
|
+
}
|
|
34887
|
+
@keyframes navi-route-travel-leave-towards-end {
|
|
34888
|
+
from {
|
|
34889
|
+
translate: 0 0;
|
|
34890
|
+
}
|
|
34891
|
+
to {
|
|
34892
|
+
translate: 100% 0;
|
|
34893
|
+
}
|
|
34894
|
+
}
|
|
34895
|
+
@keyframes navi-route-travel-enter-from-start {
|
|
34896
|
+
from {
|
|
34897
|
+
translate: -100% 0;
|
|
34898
|
+
}
|
|
34899
|
+
to {
|
|
34900
|
+
translate: 0 0;
|
|
34901
|
+
}
|
|
34902
|
+
}
|
|
34903
|
+
`;
|
|
34904
|
+
|
|
34905
|
+
/**
|
|
34906
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
34907
|
+
* routes?: Array<object>,
|
|
34908
|
+
* axis?: "x"|"y",
|
|
34909
|
+
* travelByDrag?: boolean,
|
|
34910
|
+
* onTravel?: (detail: {route: object, cause: string}) => void|Promise<void>,
|
|
34911
|
+
* }>}
|
|
34912
|
+
* @param {Array<object>} [props.routes] - the tabs, in the order they are shown.
|
|
34913
|
+
* Read from the <Route> children by default, in the order they are written:
|
|
34914
|
+
* the router already holds that list, and asking a caller to write it twice is
|
|
34915
|
+
* asking for the two to disagree. Pass it to say another order, or when the
|
|
34916
|
+
* pages are not children of this box.
|
|
34917
|
+
* @param {"x"|"y"} [props.axis="x"] - which way the pages are laid out.
|
|
34918
|
+
* @param {boolean} [props.travelByDrag=true] - whether a pointer dragging the
|
|
34919
|
+
* page travels. Off where the gesture belongs to the content.
|
|
34920
|
+
* @param {(detail: {route: object, cause: "drag"|"wheel"|"revert"}) => void|Promise<void>} [props.onTravel]
|
|
34921
|
+
* - how to go to a route. The default REPLACES the current history entry
|
|
34922
|
+
* rather than pushing one: a swipe is how one browses a page, not a place one
|
|
34923
|
+
* aimed at, and three swipes back and forth must not bury the way out of the
|
|
34924
|
+
* page under six entries. A tab pressed is the other case and pushes, which
|
|
34925
|
+
* is what its <Link> already does.
|
|
34926
|
+
*
|
|
34927
|
+
* The pages are cut at the edge of this box while they travel, which is written
|
|
34928
|
+
* on the transition's own pseudo-elements — no overflow of the document reaches
|
|
34929
|
+
* pictures drawn in the top layer. It needs nothing of the browser beyond view
|
|
34930
|
+
* transitions themselves: a browser without them (Firefox) navigates without the
|
|
34931
|
+
* movement, and the gesture applies its change on release instead of dragging a
|
|
34932
|
+
* picture that does not exist.
|
|
34933
|
+
*
|
|
34934
|
+
* While a travel plays, the rest of the page is taken as a picture too — this
|
|
34935
|
+
* box asks for `view-transition-name: root` back for that time, so an
|
|
34936
|
+
* application that opts the document out for its own transitions gets its rule
|
|
34937
|
+
* back the moment the travel is over.
|
|
34938
|
+
*/
|
|
34939
|
+
const RouteTravel = ({
|
|
34940
|
+
routes: routesProp,
|
|
34941
|
+
axis = "x",
|
|
34942
|
+
travelByDrag = true,
|
|
34943
|
+
onTravel = ({
|
|
34944
|
+
route
|
|
34945
|
+
}) => route.redirectTo(),
|
|
34946
|
+
className,
|
|
34947
|
+
children,
|
|
34948
|
+
...rest
|
|
34949
|
+
}) => {
|
|
34950
|
+
import.meta.css = [css$R, "@jsenv/navi/src/nav/route_travel.jsx"];
|
|
34951
|
+
const elementRef = useRef();
|
|
34952
|
+
const gestureRef = useRef(null);
|
|
34953
|
+
// The travel in hand: the transition keeping the picture of the page being
|
|
34954
|
+
// left, the animations the finger drives, and what to do with them once the
|
|
34955
|
+
// browser has them ready. Null when no page is on its way anywhere.
|
|
34956
|
+
const travelRef = useRef(null);
|
|
34957
|
+
// The route this box has ASKED for and is still waiting to see arrive.
|
|
34958
|
+
// Routing is asynchronous: a travel's own navigation lands well after the
|
|
34959
|
+
// travel decided anything about it — sometimes after the travel was undone —
|
|
34960
|
+
// and read back as "the route changed" it would start a second travel nobody
|
|
34961
|
+
// asked for, over pictures that are already showing something else.
|
|
34962
|
+
const routeAskedForRef = useRef(null);
|
|
34963
|
+
// What a press stopped in flight, until the gesture says what it is about.
|
|
34964
|
+
const caughtAtPressRef = useRef(null);
|
|
34965
|
+
// The latest way to answer a gesture, for a watcher that outlives every
|
|
34966
|
+
// render (see the wheel effect below).
|
|
34967
|
+
const travelHandlersRef = useRef(null);
|
|
34968
|
+
const pointerDownRef = useRef(null);
|
|
34969
|
+
const routesFromChildren = useMemo(() => collectRoutes(children), [children]);
|
|
34970
|
+
const routes = routesProp || routesFromChildren;
|
|
34971
|
+
|
|
34972
|
+
// Which page is on screen, read from the routes themselves: every one of them
|
|
34973
|
+
// is read, so this re-renders when any of them starts or stops matching.
|
|
34974
|
+
let currentIndex = -1;
|
|
34975
|
+
for (let i = 0; i < routes.length; i++) {
|
|
34976
|
+
if (routes[i].matchingSignal.value) {
|
|
34977
|
+
currentIndex = i;
|
|
34978
|
+
}
|
|
34979
|
+
}
|
|
34980
|
+
// The page that was on screen when the change now happening was asked for:
|
|
34981
|
+
// a travel is between two of them, and by the time anything renders the first
|
|
34982
|
+
// one is already gone. Written after each render (below), so a subscriber
|
|
34983
|
+
// reading it — they all run before Preact flushes — reads the one being left.
|
|
34984
|
+
const currentIndexRef = useRef(currentIndex);
|
|
34985
|
+
|
|
34986
|
+
// One travel, whoever asked for it: a finger, a tab pressed, the browser's
|
|
34987
|
+
// own back button. What differs is only who moves it — the finger drives it
|
|
34988
|
+
// frame by frame (`scrub`), everything else lets it play.
|
|
34989
|
+
const beginTravel = ({
|
|
34990
|
+
route,
|
|
34991
|
+
fromRoute,
|
|
34992
|
+
direction,
|
|
34993
|
+
scrub,
|
|
34994
|
+
change
|
|
34995
|
+
}) => {
|
|
34996
|
+
const travel = {
|
|
34997
|
+
route,
|
|
34998
|
+
// The page this set off from, kept rather than looked up again: the URL
|
|
34999
|
+
// changes at the first pixel, so a moment later nothing on screen
|
|
35000
|
+
// remembers where it started.
|
|
35001
|
+
fromRoute,
|
|
35002
|
+
direction,
|
|
35003
|
+
scrub,
|
|
35004
|
+
ratio: 0,
|
|
35005
|
+
// The animations of the pictures, once the browser has them. Held still
|
|
35006
|
+
// by CSS until then, so an empty hand here costs nothing: there is
|
|
35007
|
+
// nothing to stop, only nowhere to put them yet.
|
|
35008
|
+
animations: null,
|
|
35009
|
+
ended: false
|
|
35010
|
+
};
|
|
35011
|
+
travelRef.current = travel;
|
|
35012
|
+
document.documentElement.setAttribute(TRAVEL_ATTRIBUTE, direction);
|
|
35013
|
+
if (scrub) {
|
|
35014
|
+
holdPictures(travel);
|
|
35015
|
+
document.documentElement.setAttribute(DRAGGED_ATTRIBUTE, "");
|
|
35016
|
+
}
|
|
35017
|
+
routeAskedForRef.current = route;
|
|
35018
|
+
// The hold a navigation already took, if this travel is the answer to one:
|
|
35019
|
+
// taking another would be taking a hold on a page that is holding still.
|
|
35020
|
+
const releaseRendering = renderingHeldForRouting || holdRendering();
|
|
35021
|
+
renderingHeldForRouting = null;
|
|
35022
|
+
// The picture the browser is about to take must be of the page that was
|
|
35023
|
+
// asked for, and a route matching is not yet a page rendered.
|
|
35024
|
+
const viewTransition = startViewTransition(() => whileRouteRenders(route, async () => {
|
|
35025
|
+
releaseRendering();
|
|
35026
|
+
if (change) {
|
|
35027
|
+
await change();
|
|
35028
|
+
}
|
|
35029
|
+
}));
|
|
35030
|
+
travel.viewTransition = viewTransition;
|
|
35031
|
+
if (scrub) {
|
|
35032
|
+
// Said only now: the release has to have something to let go of, and the
|
|
35033
|
+
// transition did not exist a line above.
|
|
35034
|
+
travel.dropHold = holdViewTransition(() => {
|
|
35035
|
+
viewTransition.skipTransition();
|
|
35036
|
+
endTravel(travel);
|
|
35037
|
+
});
|
|
35038
|
+
}
|
|
35039
|
+
// Another transition starting SKIPS this one — there is only ever one in a
|
|
35040
|
+
// document. That must end the travel here and now, and above all lift the
|
|
35041
|
+
// hold: the hold is written in CSS against whatever transition is running
|
|
35042
|
+
// (it has to be, so that everything named — a trait under a tab row —
|
|
35043
|
+
// follows the same finger), so left on it would take hold of the transition
|
|
35044
|
+
// that has just replaced ours. Born paused, with nobody holding it, that
|
|
35045
|
+
// one never finishes: its pictures stand over a page that cannot be
|
|
35046
|
+
// touched anymore.
|
|
35047
|
+
viewTransition.finished.catch(() => {
|
|
35048
|
+
// A transition that fails before it ever calls back leaves the page held:
|
|
35049
|
+
// whoever asked for the hold gives it back, here as everywhere else.
|
|
35050
|
+
releaseRendering();
|
|
35051
|
+
endTravel(travel);
|
|
35052
|
+
});
|
|
35053
|
+
if (!scrub) {
|
|
35054
|
+
// Nobody is holding it: it plays as any transition does, and is over when
|
|
35055
|
+
// the browser says so.
|
|
35056
|
+
viewTransition.finished.then(() => {
|
|
35057
|
+
endTravel(travel);
|
|
35058
|
+
}, ignoreSkipped);
|
|
35059
|
+
return travel;
|
|
35060
|
+
}
|
|
35061
|
+
// The pictures exist from `ready` on, and the finger may have moved a long
|
|
35062
|
+
// way by then — held at their start meanwhile, so this is where they catch
|
|
35063
|
+
// up with it rather than where they set off.
|
|
35064
|
+
viewTransition.ready.then(() => {
|
|
35065
|
+
if (travel === travelRef.current) {
|
|
35066
|
+
scrubTravel(travel, travel.ratio);
|
|
35067
|
+
}
|
|
35068
|
+
}, ignoreSkipped);
|
|
35069
|
+
return travel;
|
|
35070
|
+
};
|
|
35071
|
+
|
|
35072
|
+
// A page change nobody here asked for: a tab pressed, a key, the back button.
|
|
35073
|
+
// The transition is started from the route's own announcement rather than
|
|
35074
|
+
// from a render, because a render is one flush too late — by then the DOM
|
|
35075
|
+
// holds the new page and the picture of the old one cannot be taken anymore.
|
|
35076
|
+
useLayoutEffect(() => {
|
|
35077
|
+
const unsubscribes = routes.map((route, index) => route.subscribeStatus(({
|
|
35078
|
+
matching
|
|
35079
|
+
}) => {
|
|
35080
|
+
if (!matching) {
|
|
35081
|
+
return;
|
|
35082
|
+
}
|
|
35083
|
+
// A page this box asked for itself — a travel's own navigation, or one
|
|
35084
|
+
// it had given up waiting on: what arrives here is the answer to a
|
|
35085
|
+
// question already answered, not somebody going somewhere.
|
|
35086
|
+
if (routeAskedForRef.current === route) {
|
|
35087
|
+
routeAskedForRef.current = null;
|
|
35088
|
+
currentIndexRef.current = index;
|
|
35089
|
+
return;
|
|
35090
|
+
}
|
|
35091
|
+
// Somewhere else arrived first: whatever this box was still waiting for
|
|
35092
|
+
// is not coming, or no longer means anything. Forgotten here rather
|
|
35093
|
+
// than kept, or the next press on that very tab would be taken for the
|
|
35094
|
+
// late answer to a question nobody remembers asking.
|
|
35095
|
+
routeAskedForRef.current = null;
|
|
35096
|
+
// Asked for a page while one was already on its way: the travel in
|
|
35097
|
+
// flight is the answer, aimed somewhere else. Starting a second one on
|
|
35098
|
+
// top would leave this one's pictures to be dropped mid-slide.
|
|
35099
|
+
if (travelRef.current) {
|
|
35100
|
+
currentIndexRef.current = index;
|
|
35101
|
+
retargetTravel(travelRef.current, route);
|
|
35102
|
+
return;
|
|
35103
|
+
}
|
|
35104
|
+
const fromIndex = currentIndexRef.current;
|
|
35105
|
+
currentIndexRef.current = index;
|
|
35106
|
+
if (fromIndex === -1 || fromIndex === index) {
|
|
35107
|
+
// Arriving from outside this row (or not moving at all): there is no
|
|
35108
|
+
// pair of pages to show, so there is nothing to travel between.
|
|
35109
|
+
return;
|
|
35110
|
+
}
|
|
35111
|
+
beginTravel({
|
|
35112
|
+
route,
|
|
35113
|
+
fromRoute: routes[fromIndex],
|
|
35114
|
+
direction: index > fromIndex ? "forward" : "back",
|
|
35115
|
+
scrub: false
|
|
35116
|
+
});
|
|
35117
|
+
}));
|
|
35118
|
+
return () => {
|
|
35119
|
+
for (const unsubscribe of unsubscribes) {
|
|
35120
|
+
unsubscribe();
|
|
35121
|
+
}
|
|
35122
|
+
};
|
|
35123
|
+
}, [routes]);
|
|
35124
|
+
|
|
35125
|
+
// Rendering is held for the length of a navigation, so that whatever picture
|
|
35126
|
+
// this box is about to take is of the page being LEFT (see holdRendering).
|
|
35127
|
+
// Held from before the navigation's first write, because by the time a route
|
|
35128
|
+
// announces that it matches, Preact has already been told and the render is
|
|
35129
|
+
// queued — a hold taken then is a hold taken too late.
|
|
35130
|
+
useLayoutEffect(() => {
|
|
35131
|
+
const stopWatchingStart = observeBeforeRouting(() => {
|
|
35132
|
+
renderingHeldForRouting = holdRendering();
|
|
35133
|
+
});
|
|
35134
|
+
// Nobody may have had a picture to take: this navigation is not always one
|
|
35135
|
+
// this box travels, and a page held for a change it does not animate is a
|
|
35136
|
+
// page that stutters for nothing. Whoever wanted it took it over
|
|
35137
|
+
// (beginTravel) while the change was being applied, and left nothing here.
|
|
35138
|
+
const stopWatchingEnd = observeAfterRouting(() => {
|
|
35139
|
+
const release = renderingHeldForRouting;
|
|
35140
|
+
renderingHeldForRouting = null;
|
|
35141
|
+
if (release) {
|
|
35142
|
+
release();
|
|
35143
|
+
}
|
|
35144
|
+
});
|
|
35145
|
+
return () => {
|
|
35146
|
+
stopWatchingStart();
|
|
35147
|
+
stopWatchingEnd();
|
|
35148
|
+
};
|
|
35149
|
+
}, []);
|
|
35150
|
+
|
|
35151
|
+
// What the next announcement will compare itself against: written after the
|
|
35152
|
+
// render that shows it, so it is always the page one is looking at.
|
|
35153
|
+
useLayoutEffect(() => {
|
|
35154
|
+
currentIndexRef.current = currentIndex;
|
|
35155
|
+
}, [currentIndex]);
|
|
35156
|
+
|
|
35157
|
+
// Let go of far enough: the movement carries on from under the finger, at its
|
|
35158
|
+
// own pace, to the end.
|
|
35159
|
+
const finishTravel = travel => {
|
|
35160
|
+
releaseHold();
|
|
35161
|
+
travel.viewTransition.finished.then(() => endTravel(travel), () => endTravel(travel));
|
|
35162
|
+
};
|
|
35163
|
+
|
|
35164
|
+
// Let go of too early: the pages go back the way they came, and the URL with
|
|
35165
|
+
// them. The way back is not a travel of its own — the same animations are run
|
|
35166
|
+
// backwards, and the page that was left is put back under the picture BEFORE
|
|
35167
|
+
// the picture is dropped, so the two are the same thing at the moment they
|
|
35168
|
+
// are swapped and nothing is seen changing.
|
|
35169
|
+
const revertTravel = travel => {
|
|
35170
|
+
travel.reverting = true;
|
|
35171
|
+
// A revert somebody ASKED for — a press on the tab this travel came from —
|
|
35172
|
+
// has a problem the others do not: that page is already back, and the
|
|
35173
|
+
// picture being brought in is LIVE, so it shows the page one is going back
|
|
35174
|
+
// to. Both sides then show the same thing and the way back is invisible:
|
|
35175
|
+
// one presses, and one is simply there.
|
|
35176
|
+
//
|
|
35177
|
+
// So the pages are held where they are until the pictures have finished
|
|
35178
|
+
// going back. Only the pages: unlike the hold a navigation takes to have
|
|
35179
|
+
// its picture taken, nothing is being photographed here — everything else
|
|
35180
|
+
// may go on rendering, and a tab row beside the box keeps answering.
|
|
35181
|
+
const releaseRendering = freezeRouteRender();
|
|
35182
|
+
const animations = travelAnimations(travel);
|
|
35183
|
+
// The way back is paid for in DISTANCE, not in time. The way in is eased:
|
|
35184
|
+
// at half of its TIME the pictures have covered ~80% of their distance, so
|
|
35185
|
+
// a travel caught "half-way" by the eye has barely begun by the clock.
|
|
35186
|
+
// Rewound at -1 it plays those few milliseconds back through the steep end
|
|
35187
|
+
// of the curve — nearly the whole visible distance collapses into two
|
|
35188
|
+
// frames, and what one sees is a snap, not a return. So the pictures are
|
|
35189
|
+
// walked home over `how far they LOOK from home`, at the travel's own
|
|
35190
|
+
// pace, each animation at the rate that gets it there in that time.
|
|
35191
|
+
const wallTime = revertWalkTime(animations);
|
|
35192
|
+
for (const animation of animations) {
|
|
35193
|
+
const timeLeft = animation.currentTime;
|
|
35194
|
+
const rate = wallTime > 17 && timeLeft > 0 ? -(timeLeft / wallTime) : -1;
|
|
35195
|
+
// updatePlaybackRate, never the playbackRate setter: these animations
|
|
35196
|
+
// run on the compositor, and the setter is a non-seamless change there —
|
|
35197
|
+
// on screen the pictures jump straight to their end state while the
|
|
35198
|
+
// Animation object ticks backwards unseen. What one sees then is the
|
|
35199
|
+
// travel SNAPPING home instead of returning, and no reading of
|
|
35200
|
+
// getAnimations() will say so: only the compositor knows, and
|
|
35201
|
+
// updatePlaybackRate is how a new rate is handed to it in flight.
|
|
35202
|
+
animation.updatePlaybackRate(rate);
|
|
35203
|
+
}
|
|
35204
|
+
releaseHold(travel);
|
|
35205
|
+
const backAtTheStart = animations.length ? Promise.all(animations.map(animation => animation.finished.catch(() => {}))) :
|
|
35206
|
+
// Nothing to run backwards — a transition the browser skipped, or one
|
|
35207
|
+
// that never became ready. There is no picture to undo either, so the
|
|
35208
|
+
// way back is the state alone.
|
|
35209
|
+
Promise.resolve();
|
|
35210
|
+
backAtTheStart.then(async () => {
|
|
35211
|
+
try {
|
|
35212
|
+
routeAskedForRef.current = travel.fromRoute;
|
|
35213
|
+
// The page that was left is put back UNDER the picture before the
|
|
35214
|
+
// picture is dropped, so the two are the same thing at the moment they
|
|
35215
|
+
// are swapped: that only holds once the page is really back.
|
|
35216
|
+
if (travel.fromRoute.matchingSignal.peek()) {
|
|
35217
|
+
// It never left: the press that set this revert off put it back
|
|
35218
|
+
// there, and the pages have been held where they were until now.
|
|
35219
|
+
// Nothing to ask for, and nothing to wait for — waiting anyway is a
|
|
35220
|
+
// render that never comes and a page frozen under its own pictures.
|
|
35221
|
+
releaseRendering();
|
|
35222
|
+
} else {
|
|
35223
|
+
await whileRouteRenders(travel.fromRoute, () => onTravel({
|
|
35224
|
+
route: travel.fromRoute,
|
|
35225
|
+
cause: "revert"
|
|
35226
|
+
}));
|
|
35227
|
+
}
|
|
35228
|
+
travel.viewTransition.skipTransition();
|
|
35229
|
+
} finally {
|
|
35230
|
+
releaseRendering();
|
|
35231
|
+
// A travel ENDS, whatever happened on the way back: put the state back,
|
|
35232
|
+
// fail to drop the picture, be interrupted by something else — the one
|
|
35233
|
+
// thing that must not happen is a travel that stays "in flight"
|
|
35234
|
+
// forever. Nothing would lift the hold, the pictures would stand where
|
|
35235
|
+
// they are over a page that cannot be touched, and every gesture after
|
|
35236
|
+
// this one would find the box busy.
|
|
35237
|
+
endTravel(travel);
|
|
35238
|
+
}
|
|
35239
|
+
});
|
|
35240
|
+
};
|
|
35241
|
+
|
|
35242
|
+
// Taken back in hand: the pictures stop where they are and answer the finger
|
|
35243
|
+
// again (see the CSS hold).
|
|
35244
|
+
const holdTravel = travel => {
|
|
35245
|
+
if (travel.ended || travel.reverting) {
|
|
35246
|
+
return;
|
|
35247
|
+
}
|
|
35248
|
+
travel.scrub = true;
|
|
35249
|
+
holdPictures(travel);
|
|
35250
|
+
// Whatever asks for a transition next takes ours away (there is one per
|
|
35251
|
+
// document): it must find this one already let go of, or it inherits a hold
|
|
35252
|
+
// nobody is holding.
|
|
35253
|
+
travel.dropHold = holdViewTransition(() => {
|
|
35254
|
+
travel.viewTransition?.skipTransition();
|
|
35255
|
+
endTravel(travel);
|
|
35256
|
+
});
|
|
35257
|
+
};
|
|
35258
|
+
|
|
35259
|
+
// The same travel, aimed at another page. The still it starts from does not
|
|
35260
|
+
// change — only what is being brought in against it, and that one is LIVE:
|
|
35261
|
+
// pointing the router elsewhere is all it takes for the picture to show that
|
|
35262
|
+
// page instead.
|
|
35263
|
+
const redirectTravel = (travel, route, direction) => {
|
|
35264
|
+
travel.route = route;
|
|
35265
|
+
// Everything the transition carries that is NOT the pages was measured
|
|
35266
|
+
// against a destination this travel is no longer going to (see the CSS).
|
|
35267
|
+
document.documentElement.setAttribute(TURNED_ATTRIBUTE, "");
|
|
35268
|
+
if (direction === travel.direction) {
|
|
35269
|
+
// Same way, another page: the pictures in hand are already the right
|
|
35270
|
+
// pair, and nothing has to move.
|
|
35271
|
+
return;
|
|
35272
|
+
}
|
|
35273
|
+
travel.direction = direction;
|
|
35274
|
+
travel.ratio = 0;
|
|
35275
|
+
// The other way round is another pair of keyframes, and naming another
|
|
35276
|
+
// animation builds another Animation: whatever was collected answers to
|
|
35277
|
+
// nobody now. They start again from the beginning, which is where a travel
|
|
35278
|
+
// that turns around is.
|
|
35279
|
+
travel.animations = null;
|
|
35280
|
+
document.documentElement.setAttribute(TRAVEL_ATTRIBUTE, direction);
|
|
35281
|
+
};
|
|
35282
|
+
|
|
35283
|
+
// Somebody asked for a page while one was on its way. Where they asked for
|
|
35284
|
+
// decides what that means.
|
|
35285
|
+
const retargetTravel = (travel, route) => {
|
|
35286
|
+
if (travel.scrub || travel.reverting || travel.ended || travel.noPicture) {
|
|
35287
|
+
// A hand is holding the pages, or they are already on their way back:
|
|
35288
|
+
// either way this travel's end is decided by somebody else.
|
|
35289
|
+
return;
|
|
35290
|
+
}
|
|
35291
|
+
if (route === travel.route) {
|
|
35292
|
+
// Already on its way there.
|
|
35293
|
+
return;
|
|
35294
|
+
}
|
|
35295
|
+
if (route === travel.fromRoute) {
|
|
35296
|
+
// Back where it set off from: that is not another travel, it is this one
|
|
35297
|
+
// undone — the same pictures, run backwards.
|
|
35298
|
+
revertTravel(travel);
|
|
35299
|
+
return;
|
|
35300
|
+
}
|
|
35301
|
+
const fromIndex = routes.indexOf(travel.fromRoute);
|
|
35302
|
+
const toIndex = routes.indexOf(route);
|
|
35303
|
+
if (fromIndex === -1 || toIndex === -1) {
|
|
35304
|
+
return;
|
|
35305
|
+
}
|
|
35306
|
+
redirectTravel(travel, route, toIndex > fromIndex ? "forward" : "back");
|
|
35307
|
+
};
|
|
35308
|
+
const endTravel = travel => {
|
|
35309
|
+
if (travel.ended) {
|
|
35310
|
+
return;
|
|
35311
|
+
}
|
|
35312
|
+
travel.ended = true;
|
|
35313
|
+
travel.dropHold?.();
|
|
35314
|
+
travel.dropHold = null;
|
|
35315
|
+
// Its own hold, always — whether or not this travel is still the current
|
|
35316
|
+
// one. Nobody else will lift it.
|
|
35317
|
+
releaseHold(travel);
|
|
35318
|
+
if (travelRef.current === travel) {
|
|
35319
|
+
travelRef.current = null;
|
|
35320
|
+
document.documentElement.removeAttribute(TRAVEL_ATTRIBUTE);
|
|
35321
|
+
document.documentElement.removeAttribute(DRAGGED_ATTRIBUTE);
|
|
35322
|
+
document.documentElement.removeAttribute(TURNED_ATTRIBUTE);
|
|
35323
|
+
}
|
|
35324
|
+
};
|
|
35325
|
+
|
|
35326
|
+
// What a gesture is about, whichever hand made it: a thumb dragging the page
|
|
35327
|
+
// and two fingers pushing it sideways on a trackpad ask for the same travel,
|
|
35328
|
+
// so they are answered by the same three callbacks and only the reading of
|
|
35329
|
+
// the input differs (see drag_to_travel.js).
|
|
35330
|
+
const boxSizeOnAxis = () => {
|
|
35331
|
+
const box = elementRef.current.getBoundingClientRect();
|
|
35332
|
+
return axis === "x" ? box.width : box.height;
|
|
35333
|
+
};
|
|
35334
|
+
const travelHandlers = {
|
|
35335
|
+
onStart: ({
|
|
35336
|
+
sign,
|
|
35337
|
+
target
|
|
35338
|
+
}) => {
|
|
35339
|
+
const size = boxSizeOnAxis();
|
|
35340
|
+
const travelInFlight = travelRef.current;
|
|
35341
|
+
if (travelInFlight) {
|
|
35342
|
+
// A travel is already playing, and a second one cannot be started on
|
|
35343
|
+
// top of it: there is one picture of the page being left, and it is
|
|
35344
|
+
// taken. So the gesture takes over THIS travel instead of asking for
|
|
35345
|
+
// another — which is what a hand reaching for a page still sliding is
|
|
35346
|
+
// asking for anyway. It is refused only when there is nothing to take
|
|
35347
|
+
// over (no picture at all).
|
|
35348
|
+
//
|
|
35349
|
+
// Never given up, even then: a gesture handed back to the browser is a
|
|
35350
|
+
// page that rocks under a travel that is already moving.
|
|
35351
|
+
// A travel being undone is not up for grabs either: it is already on
|
|
35352
|
+
// its way back and its end is decided. Held again mid-revert, its
|
|
35353
|
+
// animations would never finish — and the wait for them never resolves,
|
|
35354
|
+
// so the pictures stay where they are, over a page that cannot be
|
|
35355
|
+
// touched anymore.
|
|
35356
|
+
if (travelInFlight.noPicture || travelInFlight.ended || travelInFlight.reverting) {
|
|
35357
|
+
return {
|
|
35358
|
+
size,
|
|
35359
|
+
travelBack: false,
|
|
35360
|
+
travelOn: false
|
|
35361
|
+
};
|
|
35362
|
+
}
|
|
35363
|
+
caughtAtPressRef.current = null;
|
|
35364
|
+
holdTravel(travelInFlight);
|
|
35365
|
+
// Where the pictures stand right now, said as a pull: what the finger
|
|
35366
|
+
// continues from, so nothing jumps when it takes them over.
|
|
35367
|
+
const ratio = ratioOfTravel(travelInFlight);
|
|
35368
|
+
travelInFlight.ratio = ratio;
|
|
35369
|
+
const pulledSign = travelInFlight.direction === "back" ? 1 : -1;
|
|
35370
|
+
return {
|
|
35371
|
+
size,
|
|
35372
|
+
slack: ratio * size * pulledSign,
|
|
35373
|
+
// One box, the one being travelled. Either of its ends can be walked
|
|
35374
|
+
// out of and the hand carries on into the next page, but that is a
|
|
35375
|
+
// travel of its own and the gesture asks for it when it gets there
|
|
35376
|
+
// (see onEdge) — from here there is one page on its way.
|
|
35377
|
+
travelBack: travelInFlight.direction === "back",
|
|
35378
|
+
travelOn: travelInFlight.direction === "forward"
|
|
35379
|
+
};
|
|
35380
|
+
}
|
|
35381
|
+
// Dragging the page towards the end of the axis brings in what is
|
|
35382
|
+
// BEFORE it, the way pushing a sheet to the right reveals its left.
|
|
35383
|
+
const route = sign > 0 ? routes[currentIndex - 1] : routes[currentIndex + 1];
|
|
35384
|
+
if (!route || !size || scrollRoomTowards(target, elementRef.current, axis, sign)) {
|
|
35385
|
+
return false;
|
|
35386
|
+
}
|
|
35387
|
+
if (CAN_KEEP_PICTURE) {
|
|
35388
|
+
beginTravel({
|
|
35389
|
+
route,
|
|
35390
|
+
fromRoute: routes[currentIndex],
|
|
35391
|
+
direction: sign > 0 ? "back" : "forward",
|
|
35392
|
+
scrub: true,
|
|
35393
|
+
change: () => onTravel({
|
|
35394
|
+
route,
|
|
35395
|
+
cause: "drag"
|
|
35396
|
+
})
|
|
35397
|
+
});
|
|
35398
|
+
} else {
|
|
35399
|
+
travelRef.current = {
|
|
35400
|
+
route,
|
|
35401
|
+
noPicture: true,
|
|
35402
|
+
ended: false
|
|
35403
|
+
};
|
|
35404
|
+
}
|
|
35405
|
+
return {
|
|
35406
|
+
size,
|
|
35407
|
+
travelBack: sign > 0,
|
|
35408
|
+
travelOn: sign < 0
|
|
35409
|
+
};
|
|
35410
|
+
},
|
|
35411
|
+
onPull: ({
|
|
35412
|
+
progress
|
|
35413
|
+
}) => {
|
|
35414
|
+
const travel = travelRef.current;
|
|
35415
|
+
if (!travel || travel.noPicture) {
|
|
35416
|
+
return;
|
|
35417
|
+
}
|
|
35418
|
+
// Only what goes the way the gesture set off: a hand turning around
|
|
35419
|
+
// mid-drag is putting the page back, and there is no second picture to
|
|
35420
|
+
// show it anything else.
|
|
35421
|
+
const ratio = travel.direction === "back" ? progress : -progress;
|
|
35422
|
+
travel.ratio = ratio > 0 ? ratio : 0;
|
|
35423
|
+
scrubTravel(travel, travel.ratio);
|
|
35424
|
+
},
|
|
35425
|
+
// A page walked all the way to one of its ends and the finger still going:
|
|
35426
|
+
// what it asks for is the page past that end, and it has said so by not
|
|
35427
|
+
// stopping. Which end decides how it is answered, and the two are not the
|
|
35428
|
+
// same amount of work.
|
|
35429
|
+
onEdge: ({
|
|
35430
|
+
sign
|
|
35431
|
+
}) => {
|
|
35432
|
+
const travel = travelRef.current;
|
|
35433
|
+
if (!travel || travel.noPicture || travel.ended || travel.reverting || !CAN_KEEP_PICTURE) {
|
|
35434
|
+
return false;
|
|
35435
|
+
}
|
|
35436
|
+
// Where the page in hand is coming from, said the way the gesture says
|
|
35437
|
+
// it: dragging towards the end of the axis brings in what comes BEFORE.
|
|
35438
|
+
const pulledSign = travel.direction === "back" ? 1 : -1;
|
|
35439
|
+
const direction = sign > 0 ? "back" : "forward";
|
|
35440
|
+
if (sign === pulledSign) {
|
|
35441
|
+
// Walked whole. What the pictures show is the page that has arrived,
|
|
35442
|
+
// and the one leaving is gone: there is no pair left to travel with, so
|
|
35443
|
+
// the next travel needs pictures of its own — a navigation, a render, a
|
|
35444
|
+
// snapshot, and for those few frames the page does not follow the
|
|
35445
|
+
// finger before catching up with it (see `ready` in beginTravel). At
|
|
35446
|
+
// the start of a gesture that gap is invisible, the hand has barely
|
|
35447
|
+
// moved; here the hand is at full speed, and this is what it costs.
|
|
35448
|
+
//
|
|
35449
|
+
// Where we are is what THIS travel was bringing in — the URL changed at
|
|
35450
|
+
// the first pixel, so `currentIndex` belongs to a render this gesture
|
|
35451
|
+
// is older than.
|
|
35452
|
+
const fromIndex = routes.indexOf(travel.route);
|
|
35453
|
+
const route = direction === "back" ? routes[fromIndex - 1] : routes[fromIndex + 1];
|
|
35454
|
+
if (fromIndex === -1 || !route) {
|
|
35455
|
+
return false;
|
|
35456
|
+
}
|
|
35457
|
+
// At their very end before they are let go of: what ends the travel in
|
|
35458
|
+
// hand is the next transition starting (there is one per document, and
|
|
35459
|
+
// the funnel skips ours), and a picture skipped short of its end is a
|
|
35460
|
+
// page seen jumping the last few pixels.
|
|
35461
|
+
scrubTravel(travel, 1);
|
|
35462
|
+
travel.ratio = 1;
|
|
35463
|
+
beginTravel({
|
|
35464
|
+
route,
|
|
35465
|
+
fromRoute: travel.route,
|
|
35466
|
+
direction,
|
|
35467
|
+
scrub: true,
|
|
35468
|
+
change: () => onTravel({
|
|
35469
|
+
route,
|
|
35470
|
+
cause: "drag"
|
|
35471
|
+
})
|
|
35472
|
+
});
|
|
35473
|
+
return {
|
|
35474
|
+
size: boxSizeOnAxis(),
|
|
35475
|
+
travelBack: sign > 0,
|
|
35476
|
+
travelOn: sign < 0
|
|
35477
|
+
};
|
|
35478
|
+
}
|
|
35479
|
+
// Walked back to where it began, and out the other side. Nothing has to
|
|
35480
|
+
// be built here: the pictures in hand are ALREADY the pair this new
|
|
35481
|
+
// travel needs — the still of the page it starts from is the same one,
|
|
35482
|
+
// and the picture being brought in is live, so pointing the router at the
|
|
35483
|
+
// other neighbour is enough for it to show that one instead. The travel
|
|
35484
|
+
// turns around where it stands, on the same transition and under the same
|
|
35485
|
+
// hand, and there is no gap at all.
|
|
35486
|
+
const fromIndex = routes.indexOf(travel.fromRoute);
|
|
35487
|
+
const route = direction === "back" ? routes[fromIndex - 1] : routes[fromIndex + 1];
|
|
35488
|
+
if (fromIndex === -1 || !route) {
|
|
35489
|
+
return false;
|
|
35490
|
+
}
|
|
35491
|
+
redirectTravel(travel, route, direction);
|
|
35492
|
+
routeAskedForRef.current = route;
|
|
35493
|
+
onTravel({
|
|
35494
|
+
route,
|
|
35495
|
+
cause: "drag"
|
|
35496
|
+
});
|
|
35497
|
+
return {
|
|
35498
|
+
size: boxSizeOnAxis(),
|
|
35499
|
+
travelBack: sign > 0,
|
|
35500
|
+
travelOn: sign < 0
|
|
35501
|
+
};
|
|
35502
|
+
},
|
|
35503
|
+
onEnd: ({
|
|
35504
|
+
travels
|
|
35505
|
+
}) => {
|
|
35506
|
+
gestureRef.current = null;
|
|
35507
|
+
caughtAtPressRef.current = null;
|
|
35508
|
+
const travel = travelRef.current;
|
|
35509
|
+
if (!travel) {
|
|
35510
|
+
// The travel this gesture was holding ended under it. Nothing left to
|
|
35511
|
+
// decide, but the hold is this gesture's own doing and nobody else will
|
|
35512
|
+
// take it off — a hold nobody lifts is a page nobody can touch.
|
|
35513
|
+
releaseHold();
|
|
35514
|
+
return;
|
|
35515
|
+
}
|
|
35516
|
+
if (travel.noPicture) {
|
|
35517
|
+
travelRef.current = null;
|
|
35518
|
+
if (travels) {
|
|
35519
|
+
onTravel({
|
|
35520
|
+
route: travel.route,
|
|
35521
|
+
cause: "drag"
|
|
35522
|
+
});
|
|
35523
|
+
}
|
|
35524
|
+
return;
|
|
35525
|
+
}
|
|
35526
|
+
if (travels) {
|
|
35527
|
+
finishTravel(travel);
|
|
35528
|
+
return;
|
|
35529
|
+
}
|
|
35530
|
+
revertTravel(travel);
|
|
35531
|
+
}
|
|
35532
|
+
};
|
|
35533
|
+
const onPointerDown = pointerDownEvent => {
|
|
35534
|
+
if (!travelByDrag || gestureRef.current) {
|
|
35535
|
+
return;
|
|
35536
|
+
}
|
|
35537
|
+
// Touching something that is moving STOPS it, right there, before the
|
|
35538
|
+
// gesture has said anything about itself. Waiting for the first pixels that
|
|
35539
|
+
// decide an axis would let the pages travel on under a finger that has
|
|
35540
|
+
// already landed on them, which is the one moment a hand expects to be
|
|
35541
|
+
// obeyed without asking. If the press turns out to be nothing, the travel
|
|
35542
|
+
// is let go of again and carries on (see onGiveUp).
|
|
35543
|
+
const travelToCatch = travelRef.current;
|
|
35544
|
+
if (travelToCatch && !travelToCatch.noPicture && !travelToCatch.ended && !travelToCatch.reverting) {
|
|
35545
|
+
holdTravel(travelToCatch);
|
|
35546
|
+
caughtAtPressRef.current = travelToCatch;
|
|
35547
|
+
}
|
|
35548
|
+
// A travel already playing is not a reason to refuse the press: a hand
|
|
35549
|
+
// reaching for a page that is still sliding is reaching for THAT page, and
|
|
35550
|
+
// the gesture takes it over (see onStart).
|
|
35551
|
+
if (currentIndex === -1) {
|
|
35552
|
+
return;
|
|
35553
|
+
}
|
|
35554
|
+
const gesture = startDragToTravel(pointerDownEvent, {
|
|
35555
|
+
element: elementRef.current,
|
|
35556
|
+
axes: axis,
|
|
35557
|
+
// Caught in flight: the hand is already in the gesture (see above), so it
|
|
35558
|
+
// is answered from its first pixel, on the axis the pages travel.
|
|
35559
|
+
immediate: caughtAtPressRef.current ? axis : false,
|
|
35560
|
+
...travelHandlers,
|
|
35561
|
+
onGiveUp: () => {
|
|
35562
|
+
gestureRef.current = null;
|
|
35563
|
+
// A press that never became a gesture: whatever it stopped goes on its
|
|
35564
|
+
// way, from where the finger caught it.
|
|
35565
|
+
const caught = caughtAtPressRef.current;
|
|
35566
|
+
caughtAtPressRef.current = null;
|
|
35567
|
+
if (caught && !caught.ended) {
|
|
35568
|
+
releaseHold(caught);
|
|
35569
|
+
}
|
|
35570
|
+
}
|
|
35571
|
+
});
|
|
35572
|
+
gestureRef.current = gesture;
|
|
35573
|
+
};
|
|
35574
|
+
|
|
35575
|
+
// A press that lands ON the box while a travel is playing does not reach it:
|
|
35576
|
+
// the browser's transition covers the page and the press is delivered to the
|
|
35577
|
+
// document root instead, whatever the pictures are told about pointer events.
|
|
35578
|
+
// So while a travel plays, the press is caught at the document and handed to
|
|
35579
|
+
// the box when it fell inside it — which is where the hand thinks it pressed.
|
|
35580
|
+
pointerDownRef.current = onPointerDown;
|
|
35581
|
+
useLayoutEffect(() => {
|
|
35582
|
+
const onDocumentPointerDown = pointerDownEvent => {
|
|
35583
|
+
if (!travelRef.current) {
|
|
35584
|
+
return;
|
|
35585
|
+
}
|
|
35586
|
+
const boxElement = elementRef.current;
|
|
35587
|
+
if (!boxElement || boxElement.contains(pointerDownEvent.target)) {
|
|
35588
|
+
// It got there on its own.
|
|
35589
|
+
return;
|
|
35590
|
+
}
|
|
35591
|
+
const {
|
|
35592
|
+
left,
|
|
35593
|
+
right,
|
|
35594
|
+
top,
|
|
35595
|
+
bottom
|
|
35596
|
+
} = boxElement.getBoundingClientRect();
|
|
35597
|
+
const {
|
|
35598
|
+
clientX,
|
|
35599
|
+
clientY
|
|
35600
|
+
} = pointerDownEvent;
|
|
35601
|
+
if (clientX < left || clientX > right || clientY < top || clientY > bottom) {
|
|
35602
|
+
return;
|
|
35603
|
+
}
|
|
35604
|
+
pointerDownRef.current(pointerDownEvent);
|
|
35605
|
+
};
|
|
35606
|
+
document.addEventListener("pointerdown", onDocumentPointerDown, true);
|
|
35607
|
+
return () => {
|
|
35608
|
+
document.removeEventListener("pointerdown", onDocumentPointerDown, true);
|
|
35609
|
+
};
|
|
35610
|
+
}, []);
|
|
35611
|
+
|
|
35612
|
+
// A wheel pushing the box sideways asks for a PAGE, not for a place between
|
|
35613
|
+
// two: one push, one neighbour — the same thing a tab pressed asks for, and
|
|
35614
|
+
// it plays at its own pace rather than under a hand (see watchWheelTravel).
|
|
35615
|
+
const travelOneStep = sign => {
|
|
35616
|
+
const travelInFlight = travelRef.current;
|
|
35617
|
+
if (travelInFlight?.scrub) {
|
|
35618
|
+
// A hand is holding the pages. They are its until it lets go.
|
|
35619
|
+
return;
|
|
35620
|
+
}
|
|
35621
|
+
// Where the box is going, which is not where it is: a step asked for while
|
|
35622
|
+
// a travel plays is the page after the one on its way.
|
|
35623
|
+
const fromRoute = travelInFlight ? travelInFlight.route : routes[currentIndex];
|
|
35624
|
+
const fromIndex = routes.indexOf(fromRoute);
|
|
35625
|
+
if (fromIndex === -1) {
|
|
35626
|
+
return;
|
|
35627
|
+
}
|
|
35628
|
+
const route = sign > 0 ? routes[fromIndex - 1] : routes[fromIndex + 1];
|
|
35629
|
+
if (!route) {
|
|
35630
|
+
return;
|
|
35631
|
+
}
|
|
35632
|
+
if (travelInFlight) {
|
|
35633
|
+
// Finished where it stands before the next one sets off: what ends it is
|
|
35634
|
+
// that transition starting, and a picture dropped short of its end is a
|
|
35635
|
+
// page seen jumping the last few pixels.
|
|
35636
|
+
scrubTravel(travelInFlight, 1);
|
|
35637
|
+
travelInFlight.ratio = 1;
|
|
35638
|
+
}
|
|
35639
|
+
beginTravel({
|
|
35640
|
+
route,
|
|
35641
|
+
fromRoute,
|
|
35642
|
+
direction: sign > 0 ? "back" : "forward",
|
|
35643
|
+
scrub: false,
|
|
35644
|
+
change: () => onTravel({
|
|
35645
|
+
route,
|
|
35646
|
+
cause: "wheel"
|
|
35647
|
+
})
|
|
35648
|
+
});
|
|
35649
|
+
};
|
|
35650
|
+
|
|
35651
|
+
// Reached through a ref, and the watcher is never rebuilt for it: a travel
|
|
35652
|
+
// CHANGES the current page, so anything listening on `currentIndex` would be
|
|
35653
|
+
// torn down halfway through the very gesture that is moving it.
|
|
35654
|
+
travelHandlersRef.current = travelHandlers;
|
|
35655
|
+
const travelOneStepRef = useRef(null);
|
|
35656
|
+
travelOneStepRef.current = travelOneStep;
|
|
35657
|
+
useLayoutEffect(() => {
|
|
35658
|
+
if (!travelByDrag) {
|
|
35659
|
+
return undefined;
|
|
35660
|
+
}
|
|
35661
|
+
return watchWheelTravel(elementRef.current, {
|
|
35662
|
+
axes: axis,
|
|
35663
|
+
onStep: ({
|
|
35664
|
+
sign
|
|
35665
|
+
}) => travelOneStepRef.current(sign)
|
|
35666
|
+
});
|
|
35667
|
+
}, [travelByDrag, axis]);
|
|
35668
|
+
return jsx("div", {
|
|
35669
|
+
...rest,
|
|
35670
|
+
ref: elementRef,
|
|
35671
|
+
className: className ? `navi_route_travel ${className}` : "navi_route_travel",
|
|
35672
|
+
"data-axis": axis
|
|
35673
|
+
// What travels here, and on which axis: read by the shared gesture
|
|
35674
|
+
// stylesheet, which keeps this box's scrolling from spilling onto the
|
|
35675
|
+
// page (see drag_to_travel.js).
|
|
35676
|
+
,
|
|
35677
|
+
|
|
35678
|
+
"data-drag-travel": travelByDrag ? axis : undefined,
|
|
35679
|
+
onPointerDown: onPointerDown,
|
|
35680
|
+
children: children
|
|
35681
|
+
});
|
|
35682
|
+
};
|
|
35683
|
+
|
|
35684
|
+
// Nobody holds the pictures anymore: whatever they were told (a time to stand
|
|
35685
|
+
// at, a direction to run in) is what they carry on from.
|
|
35686
|
+
// Which travel is keeping the pictures still, if any. The hold belongs to the
|
|
35687
|
+
// travel that took it and only that one may give it back: a travel ending after
|
|
35688
|
+
// another has taken over must not lift a hold it no longer owns, and — the way
|
|
35689
|
+
// this went wrong — a travel whose end comes once something else has replaced
|
|
35690
|
+
// it must still lift its OWN. A hold left behind is a page frozen under
|
|
35691
|
+
// pictures nobody is holding.
|
|
35692
|
+
let travelHoldingPictures = null;
|
|
35693
|
+
const holdPictures = travel => {
|
|
35694
|
+
travelHoldingPictures = travel;
|
|
35695
|
+
document.documentElement.setAttribute(HOLD_ATTRIBUTE, "");
|
|
35696
|
+
};
|
|
35697
|
+
const releaseHold = travel => {
|
|
35698
|
+
if (travel && travelHoldingPictures !== travel) {
|
|
35699
|
+
return;
|
|
35700
|
+
}
|
|
35701
|
+
travelHoldingPictures = null;
|
|
35702
|
+
document.documentElement.removeAttribute(HOLD_ATTRIBUTE);
|
|
35703
|
+
};
|
|
35704
|
+
|
|
35705
|
+
// The browser does not take the picture of the page being left when a
|
|
35706
|
+
// transition is ASKED for — it takes it at the next frame, just before running
|
|
35707
|
+
// the update callback. Preact renders sooner than that, in a microtask: so a
|
|
35708
|
+
// change nobody here asked for (a tab pressed, the back button) has already
|
|
35709
|
+
// reached the DOM when the picture is taken, and the picture is of the page
|
|
35710
|
+
// ARRIVING. Both sides of the travel then show it, and one watches a page slide
|
|
35711
|
+
// onto itself.
|
|
35712
|
+
//
|
|
35713
|
+
// So what Preact has queued waits until the update callback, which is the
|
|
35714
|
+
// moment the API is built around — the change belongs inside it. The whole
|
|
35715
|
+
// document is held, for the one frame the browser needs: it is about to be
|
|
35716
|
+
// frozen under a picture anyway.
|
|
35717
|
+
let renderingHold = null;
|
|
35718
|
+
// The hold a navigation took on its way in, until a travel takes it over or the
|
|
35719
|
+
// navigation turns out to be one nobody here animates.
|
|
35720
|
+
let renderingHeldForRouting = null;
|
|
35721
|
+
const holdRendering = () => {
|
|
35722
|
+
if (renderingHold) {
|
|
35723
|
+
return renderingHold.release;
|
|
35724
|
+
}
|
|
35725
|
+
const debounceRenderingBefore = options.debounceRendering;
|
|
35726
|
+
const hold = {
|
|
35727
|
+
render: null,
|
|
35728
|
+
release: () => {
|
|
35729
|
+
// Only the hold that is still standing may be given back: a travel
|
|
35730
|
+
// ending after another has taken over must not let go of what it does
|
|
35731
|
+
// not hold.
|
|
35732
|
+
if (renderingHold !== hold) {
|
|
35733
|
+
return;
|
|
35734
|
+
}
|
|
35735
|
+
renderingHold = null;
|
|
35736
|
+
options.debounceRendering = debounceRenderingBefore;
|
|
35737
|
+
const {
|
|
35738
|
+
render
|
|
35739
|
+
} = hold;
|
|
35740
|
+
hold.render = null;
|
|
35741
|
+
if (render) {
|
|
35742
|
+
render();
|
|
35743
|
+
}
|
|
35744
|
+
}
|
|
35745
|
+
};
|
|
35746
|
+
renderingHold = hold;
|
|
35747
|
+
options.debounceRendering = render => {
|
|
35748
|
+
hold.render = render;
|
|
35749
|
+
};
|
|
35750
|
+
return hold.release;
|
|
35751
|
+
};
|
|
35752
|
+
|
|
35753
|
+
// The animations of the pictures, asked for again until there are some: they
|
|
35754
|
+
// come into existence with the transition, several frames after it was asked
|
|
35755
|
+
// for, and the gesture has already begun by then. Kept once found — the set
|
|
35756
|
+
// does not change for the length of one travel.
|
|
35757
|
+
const travelAnimations = travel => {
|
|
35758
|
+
if (travel.animations) {
|
|
35759
|
+
return travel.animations;
|
|
35760
|
+
}
|
|
35761
|
+
const animations = [];
|
|
35762
|
+
for (const animation of document.getAnimations()) {
|
|
35763
|
+
const pseudoElement = animation.effect?.pseudoElement;
|
|
35764
|
+
if (pseudoElement && pseudoElement.startsWith("::view-transition")) {
|
|
35765
|
+
animations.push(animation);
|
|
35766
|
+
}
|
|
35767
|
+
}
|
|
35768
|
+
if (animations.length) {
|
|
35769
|
+
travel.animations = animations;
|
|
35770
|
+
}
|
|
35771
|
+
return animations;
|
|
35772
|
+
};
|
|
35773
|
+
|
|
35774
|
+
// How far a travel has come, read off the pictures themselves rather than off
|
|
35775
|
+
// what the last gesture wrote: one let go of is still moving, and a hand
|
|
35776
|
+
// reaching for it must find it where it IS.
|
|
35777
|
+
const ratioOfTravel = travel => {
|
|
35778
|
+
const animations = travelAnimations(travel);
|
|
35779
|
+
// The pages' own animation, not the first that comes: everything the travel
|
|
35780
|
+
// carries along is animated too (a trait under a tab row, the page behind),
|
|
35781
|
+
// each with a duration of its own. A time read on one of those and turned
|
|
35782
|
+
// into a fraction of ANOTHER lands anywhere — over 1 more often than not,
|
|
35783
|
+
// which reads as a travel already over and jumps the pictures to their end.
|
|
35784
|
+
const animation = animations.find(candidate => candidate.effect?.pseudoElement?.includes("navi-route-travel")) || animations[0];
|
|
35785
|
+
if (!animation) {
|
|
35786
|
+
return travel.ratio;
|
|
35787
|
+
}
|
|
35788
|
+
const timing = animation.effect.getComputedTiming();
|
|
35789
|
+
const duration = timing.delay + timing.activeDuration;
|
|
35790
|
+
if (!duration) {
|
|
35791
|
+
return travel.ratio;
|
|
35792
|
+
}
|
|
35793
|
+
return animation.currentTime / duration;
|
|
35794
|
+
};
|
|
35795
|
+
|
|
35796
|
+
// CSS `ease`, evaluated: time in, distance out. Solved numerically because
|
|
35797
|
+
// the curve is parametric — two cubics sharing a parameter, with no closed
|
|
35798
|
+
// form for one against the other. Twenty halvings put the answer well under
|
|
35799
|
+
// a pixel of a screen-wide travel.
|
|
35800
|
+
const CSS_EASE = [0.25, 0.1, 0.25, 1];
|
|
35801
|
+
const bezierAxis = (s, a, b) => 3 * (1 - s) * (1 - s) * s * a + 3 * (1 - s) * s * s * b + s * s * s;
|
|
35802
|
+
const easedProgress = (x, [x1, y1, x2, y2]) => {
|
|
35803
|
+
let low = 0;
|
|
35804
|
+
let high = 1;
|
|
35805
|
+
for (let i = 0; i < 20; i++) {
|
|
35806
|
+
const mid = (low + high) / 2;
|
|
35807
|
+
if (bezierAxis(mid, x1, x2) < x) {
|
|
35808
|
+
low = mid;
|
|
35809
|
+
} else {
|
|
35810
|
+
high = mid;
|
|
35811
|
+
}
|
|
35812
|
+
}
|
|
35813
|
+
return bezierAxis((low + high) / 2, y1, y2);
|
|
35814
|
+
};
|
|
35815
|
+
|
|
35816
|
+
// How long the way back should take: the distance the pictures visibly are
|
|
35817
|
+
// from home, converted to time at the travel's own pace. The distance is
|
|
35818
|
+
// computed from the clock THROUGH the easing curve, never read off the
|
|
35819
|
+
// pseudo-elements: getComputedStyle on them answers with the un-animated
|
|
35820
|
+
// value — the animated one lives on the compositor, where no reading from
|
|
35821
|
+
// here reaches (the same trap as the playbackRate setter above).
|
|
35822
|
+
const revertWalkTime = animations => {
|
|
35823
|
+
const animation = animations.find(candidate => candidate.effect?.pseudoElement?.includes("navi-route-travel"));
|
|
35824
|
+
if (!animation) {
|
|
35825
|
+
return 0;
|
|
35826
|
+
}
|
|
35827
|
+
const timing = animation.effect.getComputedTiming();
|
|
35828
|
+
const duration = timing.delay + timing.activeDuration;
|
|
35829
|
+
if (!duration) {
|
|
35830
|
+
return 0;
|
|
35831
|
+
}
|
|
35832
|
+
const temporal = animation.currentTime / duration;
|
|
35833
|
+
// The easing sits on the keyframes, where a CSS animation's
|
|
35834
|
+
// animation-timing-function ends up. "ease" is what our travels play;
|
|
35835
|
+
// anything else (linear under a finger, see the DRAGGED attribute) maps
|
|
35836
|
+
// time to distance one for one.
|
|
35837
|
+
const easing = animation.effect.getKeyframes()[0]?.easing;
|
|
35838
|
+
const visibleRatio = easing === "ease" ? easedProgress(temporal, CSS_EASE) : temporal;
|
|
35839
|
+
return visibleRatio * duration;
|
|
35840
|
+
};
|
|
35841
|
+
|
|
35842
|
+
// Where the two pictures stand, said as a moment in the movement they would
|
|
35843
|
+
// have played on their own: the browser knows how they move (it is written in
|
|
35844
|
+
// CSS), so the finger only has to say how far in. They are held still by CSS
|
|
35845
|
+
// while this lasts, so a time written here is a place they stay at.
|
|
35846
|
+
const scrubTravel = (travel, ratio) => {
|
|
35847
|
+
for (const animation of travelAnimations(travel)) {
|
|
35848
|
+
const timing = animation.effect.getComputedTiming();
|
|
35849
|
+
const duration = timing.delay + timing.activeDuration;
|
|
35850
|
+
animation.currentTime = ratio * duration;
|
|
35851
|
+
}
|
|
35852
|
+
};
|
|
35853
|
+
|
|
35854
|
+
// A route change, carried out and then waited for until the page it selects is
|
|
35855
|
+
// really on screen. The container doing the swapping is the only one who knows
|
|
35856
|
+
// when that is (observeRouteRender): a route matching is a signal changing, and
|
|
35857
|
+
// how many passes Preact takes to answer it is its own business.
|
|
35858
|
+
//
|
|
35859
|
+
// Nothing is waited for when the change did not take — a route refused, a
|
|
35860
|
+
// redirect somewhere else. There is no page on its way then, and this runs
|
|
35861
|
+
// inside the callback of a view transition: the browser has stopped rendering
|
|
35862
|
+
// and is waiting on this very promise to take its picture, so a wait that never
|
|
35863
|
+
// ends is a page frozen under a transition that never became ready.
|
|
35864
|
+
const whileRouteRenders = async (route, change) => {
|
|
35865
|
+
let stopListening;
|
|
35866
|
+
const rendered = new Promise(resolve => {
|
|
35867
|
+
// Listened for before the change, or a render landing while the change is
|
|
35868
|
+
// being awaited is a render nobody heard.
|
|
35869
|
+
stopListening = observeRouteRender(resolve);
|
|
35870
|
+
});
|
|
35871
|
+
try {
|
|
35872
|
+
await change();
|
|
35873
|
+
if (route.matchingSignal.peek()) {
|
|
35874
|
+
await rendered;
|
|
35875
|
+
}
|
|
35876
|
+
} finally {
|
|
35877
|
+
stopListening();
|
|
35878
|
+
}
|
|
35879
|
+
};
|
|
35880
|
+
|
|
35881
|
+
// A transition skipped by another one starting is an outcome, not a failure.
|
|
35882
|
+
const ignoreSkipped = () => {};
|
|
35883
|
+
|
|
34499
35884
|
const routeAction = (
|
|
34500
35885
|
routeOrRoutes,
|
|
34501
35886
|
action,
|
|
@@ -36456,6 +37841,13 @@ const useIsVisited = (url) => {
|
|
|
36456
37841
|
*/
|
|
36457
37842
|
const BinderItemContext = createContext(null);
|
|
36458
37843
|
|
|
37844
|
+
/**
|
|
37845
|
+
* What a <Link> learns from the <Nav> around it: where to draw the bar that
|
|
37846
|
+
* says "you are here", and the name under which the browser is to recognise
|
|
37847
|
+
* that bar from one page to the next (see nav.jsx).
|
|
37848
|
+
*/
|
|
37849
|
+
const NavContext = createContext(null);
|
|
37850
|
+
|
|
36459
37851
|
/*
|
|
36460
37852
|
* Custom hook to apply semi-transparent color when an element should be dimmed.
|
|
36461
37853
|
*
|
|
@@ -36754,15 +38146,15 @@ installImportMetaCssBuild(import.meta);const css$O = /* css */`
|
|
|
36754
38146
|
margin-left: -0.1em;
|
|
36755
38147
|
}
|
|
36756
38148
|
|
|
36757
|
-
&[data-
|
|
38149
|
+
&[data-variant="text"] {
|
|
36758
38150
|
--link-color: unset;
|
|
36759
38151
|
--link-text-decoration: none;
|
|
36760
38152
|
}
|
|
36761
|
-
&[data-
|
|
38153
|
+
&[data-variant="icon"] {
|
|
36762
38154
|
--link-color: unset;
|
|
36763
38155
|
--link-text-decoration: none;
|
|
36764
38156
|
}
|
|
36765
|
-
&[data-
|
|
38157
|
+
&[data-variant="tab"] {
|
|
36766
38158
|
--link-background-hover: color-mix(
|
|
36767
38159
|
in srgb,
|
|
36768
38160
|
var(--link-background, transparent),
|
|
@@ -36899,15 +38291,15 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
36899
38291
|
* (`navi_value`); defaults to `href`.
|
|
36900
38292
|
* @param {boolean} [props.current] - Forces the "current" state on (otherwise
|
|
36901
38293
|
* derived from the href/route).
|
|
36902
|
-
* @param {"text"|"icon"|"tab"} [props.
|
|
36903
|
-
* (`data-
|
|
38294
|
+
* @param {"text"|"icon"|"tab"} [props.variant] - Visual variant
|
|
38295
|
+
* (`data-variant`); `"text"`/`"icon"` drop the link color/underline,
|
|
36904
38296
|
* `"tab"` renders a tab-like affordance.
|
|
36905
38297
|
* @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - A
|
|
36906
38298
|
* bar drawn on the given edge (or bottom when `true`) while current.
|
|
36907
38299
|
* @param {boolean} [props.currentEffectBold] - Bold the text while current
|
|
36908
38300
|
* (reserving the bold width so layout doesn't shift).
|
|
36909
38301
|
* @param {boolean} [props.currentEffectShadow] - Inset-shadow effect while
|
|
36910
|
-
* current (used with `
|
|
38302
|
+
* current (used with `variant="tab"`).
|
|
36911
38303
|
* @param {boolean|import("ignore:preact").ComponentChild} [props.startIcon] - Icon
|
|
36912
38304
|
* placed before the text.
|
|
36913
38305
|
* @param {boolean|import("ignore:preact").ComponentChild} [props.endIcon] - Icon
|
|
@@ -36976,7 +38368,7 @@ const LinkPlain = props => {
|
|
|
36976
38368
|
anchor,
|
|
36977
38369
|
value = href,
|
|
36978
38370
|
// visual
|
|
36979
|
-
|
|
38371
|
+
variant,
|
|
36980
38372
|
current,
|
|
36981
38373
|
currentIndicator,
|
|
36982
38374
|
currentEffectBold,
|
|
@@ -36993,6 +38385,7 @@ const LinkPlain = props => {
|
|
|
36993
38385
|
props.id = href.slice(1);
|
|
36994
38386
|
}
|
|
36995
38387
|
const selectionContext = useContext(SelectionContext);
|
|
38388
|
+
const nav = useContext(NavContext);
|
|
36996
38389
|
const visited = useIsVisited(href);
|
|
36997
38390
|
const {
|
|
36998
38391
|
selection,
|
|
@@ -37093,8 +38486,19 @@ const LinkPlain = props => {
|
|
|
37093
38486
|
const innerChildren = children || (hrefFallback ? href : children);
|
|
37094
38487
|
const startIconEl = startIcon;
|
|
37095
38488
|
const endIconEl = innerEndIcon;
|
|
37096
|
-
|
|
37097
|
-
|
|
38489
|
+
|
|
38490
|
+
// Where the bar goes: said here, or once for the whole row by the <Nav>
|
|
38491
|
+
// around this link.
|
|
38492
|
+
const currentIndicatorAsked = currentIndicator ?? nav?.currentIndicator;
|
|
38493
|
+
const currentIndicatorPosition = currentIndicatorAsked === true ? "bottom" : currentIndicatorAsked;
|
|
38494
|
+
const currentIndicatorEl = currentIndicatorPosition === "left" || currentIndicatorPosition === "right" || currentIndicatorPosition === "top" || currentIndicatorPosition === "bottom" ? jsx(LinkCurrentIndicator
|
|
38495
|
+
// Only the bar one can actually see carries the row's name, because a
|
|
38496
|
+
// name belongs to one element at a time and every tab holds a bar. The
|
|
38497
|
+
// browser then has the same thing in two places from one page to the
|
|
38498
|
+
// next, and moves it — which is the whole of "the bar slides".
|
|
38499
|
+
, {
|
|
38500
|
+
viewTransitionName: innerCurrent ? nav?.indicatorName : null
|
|
38501
|
+
}) : null;
|
|
37098
38502
|
const {
|
|
37099
38503
|
onClick,
|
|
37100
38504
|
preventDefault
|
|
@@ -37104,9 +38508,24 @@ const LinkPlain = props => {
|
|
|
37104
38508
|
color: anchor && !innerChildren ? "inherit" : undefined,
|
|
37105
38509
|
...controlRootProps,
|
|
37106
38510
|
...controlHostProps,
|
|
38511
|
+
// Everything this component reads for itself is taken off the way out:
|
|
38512
|
+
// what is left goes on the element, and a prop that means something here
|
|
38513
|
+
// means nothing to an <a>. Written one by one rather than pulled out of
|
|
38514
|
+
// props with a rest, because props is also what the control layer above
|
|
38515
|
+
// was handed.
|
|
37107
38516
|
preventDefault: undefined,
|
|
37108
38517
|
anchor: undefined,
|
|
37109
38518
|
revealOnInteraction: undefined,
|
|
38519
|
+
variant: undefined,
|
|
38520
|
+
current: undefined,
|
|
38521
|
+
currentIndicator: undefined,
|
|
38522
|
+
currentEffectBold: undefined,
|
|
38523
|
+
currentEffectShadow: undefined,
|
|
38524
|
+
blankTargetIcon: undefined,
|
|
38525
|
+
anchorIcon: undefined,
|
|
38526
|
+
startIcon: undefined,
|
|
38527
|
+
endIcon: undefined,
|
|
38528
|
+
hrefFallback: undefined,
|
|
37110
38529
|
onClick: e => {
|
|
37111
38530
|
onClick?.(e);
|
|
37112
38531
|
if (preventDefault) {
|
|
@@ -37135,7 +38554,7 @@ const LinkPlain = props => {
|
|
|
37135
38554
|
// Visual
|
|
37136
38555
|
,
|
|
37137
38556
|
|
|
37138
|
-
"data-
|
|
38557
|
+
"data-variant": variant,
|
|
37139
38558
|
"data-current-effect-bold": currentEffectBold ? "" : undefined,
|
|
37140
38559
|
"data-current-effect-shadow": currentEffectShadow ? "" : undefined,
|
|
37141
38560
|
"data-current-indicator-position": currentIndicatorPosition,
|
|
@@ -37156,20 +38575,23 @@ const LinkPlain = props => {
|
|
|
37156
38575
|
children: [startIconEl, innerChildren, endIconEl]
|
|
37157
38576
|
});
|
|
37158
38577
|
};
|
|
37159
|
-
const LinkCurrentIndicator = (
|
|
38578
|
+
const LinkCurrentIndicator = ({
|
|
38579
|
+
viewTransitionName
|
|
38580
|
+
}) => {
|
|
37160
38581
|
return jsx("span", {
|
|
37161
|
-
className: "navi_current_indicator"
|
|
38582
|
+
className: "navi_current_indicator",
|
|
38583
|
+
style: viewTransitionName ? {
|
|
38584
|
+
viewTransitionName
|
|
38585
|
+
} : undefined
|
|
37162
38586
|
});
|
|
37163
38587
|
};
|
|
37164
38588
|
markAsOutsideTextFlow(LinkCurrentIndicator);
|
|
37165
38589
|
|
|
37166
|
-
const NavContext = createContext();
|
|
37167
|
-
createContext();
|
|
37168
|
-
|
|
37169
38590
|
installImportMetaCssBuild(import.meta);/**
|
|
37170
38591
|
* TabList component with support for horizontal and vertical layouts
|
|
37171
38592
|
* https://dribbble.com/search/tabs
|
|
37172
38593
|
*/
|
|
38594
|
+
let navCount = 0;
|
|
37173
38595
|
const css$N = /* css */`
|
|
37174
38596
|
@layer navi {
|
|
37175
38597
|
.navi_nav {
|
|
@@ -37334,6 +38756,22 @@ const NavStyleCSSVars = {
|
|
|
37334
38756
|
paddingLeft: "--nav-padding-left",
|
|
37335
38757
|
background: "--nav-background"
|
|
37336
38758
|
};
|
|
38759
|
+
/**
|
|
38760
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
38761
|
+
* currentIndicator?: boolean|"top"|"bottom"|"left"|"right",
|
|
38762
|
+
* currentIndicatorSlides?: boolean,
|
|
38763
|
+
* }>}
|
|
38764
|
+
* @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - the
|
|
38765
|
+
* bar that says which tab one is on, said once here rather than on every
|
|
38766
|
+
* `<Link>`. A link may still say otherwise for itself.
|
|
38767
|
+
* @param {boolean} [props.currentIndicatorSlides=true] - whether that bar
|
|
38768
|
+
* travels from the tab it was under to the tab it is under now, instead of
|
|
38769
|
+
* going out on one and coming back on the other. It does so by being NAMED,
|
|
38770
|
+
* which is all the browser needs: any change played as a view transition
|
|
38771
|
+
* animates it on the same clock as everything else in that transition. Inside
|
|
38772
|
+
* a `RouteTravel` that means it follows the pages, and the thumb dragging
|
|
38773
|
+
* them, without either of them being told about the other.
|
|
38774
|
+
*/
|
|
37337
38775
|
const Nav = ({
|
|
37338
38776
|
children,
|
|
37339
38777
|
spacing,
|
|
@@ -37341,11 +38779,23 @@ const Nav = ({
|
|
|
37341
38779
|
expand,
|
|
37342
38780
|
expandX,
|
|
37343
38781
|
linkBorderRadiusInherit,
|
|
38782
|
+
currentIndicator,
|
|
38783
|
+
currentIndicatorSlides = true,
|
|
37344
38784
|
panelPosition,
|
|
37345
38785
|
// "before" or "after": which side the panel sits on, turning the nav into folder tabs
|
|
37346
38786
|
...props
|
|
37347
38787
|
}) => {
|
|
37348
38788
|
import.meta.css = [css$N, "@jsenv/navi/src/nav/link/nav.jsx"];
|
|
38789
|
+
const indicatorNameRef = useRef(null);
|
|
38790
|
+
if (indicatorNameRef.current === null) {
|
|
38791
|
+
indicatorNameRef.current = `navi-nav-indicator-${++navCount}`;
|
|
38792
|
+
}
|
|
38793
|
+
const navContextValue = useMemo(() => ({
|
|
38794
|
+
currentIndicator,
|
|
38795
|
+
// Read by the link that is current, and by it alone: a name belongs to
|
|
38796
|
+
// one element at a time, and the bar exists in every tab.
|
|
38797
|
+
indicatorName: currentIndicatorSlides ? indicatorNameRef.current : null
|
|
38798
|
+
}), [currentIndicator, currentIndicatorSlides]);
|
|
37349
38799
|
children = toChildArray(children);
|
|
37350
38800
|
return jsx(Box, {
|
|
37351
38801
|
as: "nav",
|
|
@@ -37362,7 +38812,7 @@ const Nav = ({
|
|
|
37362
38812
|
...props,
|
|
37363
38813
|
styleCSSVars: NavStyleCSSVars,
|
|
37364
38814
|
children: jsx(NavContext.Provider, {
|
|
37365
|
-
value:
|
|
38815
|
+
value: navContextValue,
|
|
37366
38816
|
children: children
|
|
37367
38817
|
})
|
|
37368
38818
|
});
|
|
@@ -39725,8 +41175,8 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
|
|
|
39725
41175
|
--color-mix: var(--color-mix-light);
|
|
39726
41176
|
}
|
|
39727
41177
|
|
|
39728
|
-
/* Checkbox
|
|
39729
|
-
&[data-
|
|
41178
|
+
/* Checkbox variant */
|
|
41179
|
+
&[data-variant="checkbox"] {
|
|
39730
41180
|
.navi_checkbox_marker {
|
|
39731
41181
|
width: 100%;
|
|
39732
41182
|
height: 100%;
|
|
@@ -39756,8 +41206,8 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
|
|
|
39756
41206
|
}
|
|
39757
41207
|
}
|
|
39758
41208
|
|
|
39759
|
-
/* Switch
|
|
39760
|
-
&[data-
|
|
41209
|
+
/* Switch variant */
|
|
41210
|
+
&[data-variant="switch"] {
|
|
39761
41211
|
--switch-outer-width: calc(var(--switch-width) + var(--switch-padding));
|
|
39762
41212
|
--margin: var(--switch-margin);
|
|
39763
41213
|
--width: var(--switch-outer-width);
|
|
@@ -39789,7 +41239,7 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
|
|
|
39789
41239
|
border-color: transparent;
|
|
39790
41240
|
}
|
|
39791
41241
|
|
|
39792
|
-
&[data-
|
|
41242
|
+
&[data-variant="icon"] {
|
|
39793
41243
|
--margin: 0;
|
|
39794
41244
|
--width: auto;
|
|
39795
41245
|
--height: auto;
|
|
@@ -39798,7 +41248,7 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
|
|
|
39798
41248
|
border: none;
|
|
39799
41249
|
}
|
|
39800
41250
|
|
|
39801
|
-
&[data-
|
|
41251
|
+
&[data-variant="button"] {
|
|
39802
41252
|
--margin: 0;
|
|
39803
41253
|
--width: auto;
|
|
39804
41254
|
--height: auto;
|
|
@@ -39875,7 +41325,7 @@ const InputCheckboxFieldInterface = props => {
|
|
|
39875
41325
|
const {
|
|
39876
41326
|
icon,
|
|
39877
41327
|
switch: switchProp,
|
|
39878
|
-
|
|
41328
|
+
variant = icon ? "icon" : switchProp ? "switch" : "checkbox",
|
|
39879
41329
|
// "checkbox", "switch", "icon", "button"
|
|
39880
41330
|
accentColor
|
|
39881
41331
|
} = props;
|
|
@@ -39889,13 +41339,13 @@ const InputCheckboxFieldInterface = props => {
|
|
|
39889
41339
|
elementSelector: ".navi_checkbox_accent_probe"
|
|
39890
41340
|
});
|
|
39891
41341
|
let visualVnode;
|
|
39892
|
-
if (
|
|
41342
|
+
if (variant === "icon" || icon) {
|
|
39893
41343
|
visualVnode = jsx("div", {
|
|
39894
41344
|
className: "navi_checkbox_icon",
|
|
39895
41345
|
"aria-hidden": "true",
|
|
39896
41346
|
children: Array.isArray(icon) ? icon[checked ? 1 : 0] : icon
|
|
39897
41347
|
});
|
|
39898
|
-
} else if (
|
|
41348
|
+
} else if (variant === "switch") {
|
|
39899
41349
|
visualVnode = jsx(SwitchUI, {});
|
|
39900
41350
|
} else {
|
|
39901
41351
|
visualVnode = jsx(Box, {
|
|
@@ -39916,16 +41366,16 @@ const InputCheckboxFieldInterface = props => {
|
|
|
39916
41366
|
// (passsing any custom width/height would auto disable aspectRatio forced by the square prop)
|
|
39917
41367
|
,
|
|
39918
41368
|
|
|
39919
|
-
square:
|
|
41369
|
+
square: variant === "button" ? true : undefined,
|
|
39920
41370
|
...checkboxRootProps,
|
|
39921
41371
|
ref: boxRef,
|
|
39922
|
-
|
|
41372
|
+
variant: undefined,
|
|
39923
41373
|
switch: undefined,
|
|
39924
41374
|
icon: undefined,
|
|
39925
|
-
"data-
|
|
41375
|
+
"data-variant": variant,
|
|
39926
41376
|
baseClassName: "navi_checkbox",
|
|
39927
41377
|
pseudoStateSelector: ".navi_control_input",
|
|
39928
|
-
styleCSSVars:
|
|
41378
|
+
styleCSSVars: variant === "switch" ? CheckboxSwitchStyleCSSVars : variant === "button" ? CheckboxButtonStyleCSSVars : CheckboxStyleCSSVars,
|
|
39929
41379
|
basePseudoState: basePseudoState,
|
|
39930
41380
|
pseudoClasses: CheckboxPseudoClasses,
|
|
39931
41381
|
pseudoElements: CheckboxPseudoElements,
|
|
@@ -40500,8 +41950,8 @@ installImportMetaCssBuild(import.meta);const css$E = /* css */`
|
|
|
40500
41950
|
}
|
|
40501
41951
|
}
|
|
40502
41952
|
|
|
40503
|
-
/* Radio
|
|
40504
|
-
&[data-
|
|
41953
|
+
/* Radio variant */
|
|
41954
|
+
&[data-variant="radio"] {
|
|
40505
41955
|
display: inline-flex;
|
|
40506
41956
|
align-items: center;
|
|
40507
41957
|
justify-content: center;
|
|
@@ -40548,16 +41998,16 @@ installImportMetaCssBuild(import.meta);const css$E = /* css */`
|
|
|
40548
41998
|
}
|
|
40549
41999
|
}
|
|
40550
42000
|
|
|
40551
|
-
/* Icon
|
|
40552
|
-
&[data-
|
|
42001
|
+
/* Icon variant */
|
|
42002
|
+
&[data-variant="icon"] {
|
|
40553
42003
|
--width: auto;
|
|
40554
42004
|
--height: auto;
|
|
40555
42005
|
--outline-offset: 2px;
|
|
40556
42006
|
--outline-width: 2px;
|
|
40557
42007
|
}
|
|
40558
42008
|
|
|
40559
|
-
/* Button
|
|
40560
|
-
&[data-
|
|
42009
|
+
/* Button variant */
|
|
42010
|
+
&[data-variant="button"] {
|
|
40561
42011
|
--margin: 0;
|
|
40562
42012
|
--outline-offset: 0px;
|
|
40563
42013
|
--width: auto;
|
|
@@ -40662,18 +42112,18 @@ const InputRadioHeadless = props => {
|
|
|
40662
42112
|
...radioHostProps
|
|
40663
42113
|
});
|
|
40664
42114
|
};
|
|
40665
|
-
const
|
|
42115
|
+
const VARIANT_SET = new Set(["icon", "button", "radio"]);
|
|
40666
42116
|
const InputRadioFieldInterface = props => {
|
|
40667
42117
|
import.meta.css = [css$E, "@jsenv/navi/src/control/input/input_radio.jsx"];
|
|
40668
42118
|
const [radioRootProps, radioHostProps] = useCheckableProps(props);
|
|
40669
42119
|
const {
|
|
40670
42120
|
icon,
|
|
40671
|
-
|
|
42121
|
+
variant
|
|
40672
42122
|
} = props;
|
|
40673
|
-
let
|
|
40674
|
-
if (
|
|
40675
|
-
console.warn(`InputRadio: unsupported
|
|
40676
|
-
|
|
42123
|
+
let variantResolved = variant || (icon ? "icon" : "radio");
|
|
42124
|
+
if (variant && !VARIANT_SET.has(variant)) {
|
|
42125
|
+
console.warn(`InputRadio: unsupported variant "${variant}". Falling back to "radio".`);
|
|
42126
|
+
variantResolved = "radio";
|
|
40677
42127
|
}
|
|
40678
42128
|
const {
|
|
40679
42129
|
basePseudoState,
|
|
@@ -40685,10 +42135,10 @@ const InputRadioFieldInterface = props => {
|
|
|
40685
42135
|
elementSelector: ".navi_radio_accent_probe"
|
|
40686
42136
|
});
|
|
40687
42137
|
let visualVNode;
|
|
40688
|
-
if (
|
|
42138
|
+
if (variantResolved === "icon" || icon) {
|
|
40689
42139
|
visualVNode = Array.isArray(icon) ? icon[checked ? 1 : 0] : icon;
|
|
40690
42140
|
} else {
|
|
40691
|
-
//
|
|
42141
|
+
// variantResolved === "radio"
|
|
40692
42142
|
visualVNode = jsx(RadioSvg, {});
|
|
40693
42143
|
}
|
|
40694
42144
|
return jsxs(Box, {
|
|
@@ -40697,16 +42147,16 @@ const InputRadioFieldInterface = props => {
|
|
|
40697
42147
|
// (passsing any custom width/height would auto disable aspectRatio forced by the square prop)
|
|
40698
42148
|
,
|
|
40699
42149
|
|
|
40700
|
-
square:
|
|
42150
|
+
square: variantResolved === "button" ? true : undefined,
|
|
40701
42151
|
...radioRootProps,
|
|
40702
42152
|
ref: boxRef,
|
|
40703
42153
|
icon: undefined,
|
|
40704
|
-
|
|
40705
|
-
"data-
|
|
42154
|
+
variant: undefined,
|
|
42155
|
+
"data-variant": variantResolved,
|
|
40706
42156
|
baseClassName: "navi_radio",
|
|
40707
42157
|
pseudoStateSelector: ".navi_control_input",
|
|
40708
42158
|
basePseudoState: basePseudoState,
|
|
40709
|
-
styleCSSVars:
|
|
42159
|
+
styleCSSVars: variantResolved === "button" ? RadioButtonStyleCSSVars : RadioStyleCSSVars,
|
|
40710
42160
|
pseudoClasses: RadioPseudoClasses,
|
|
40711
42161
|
pseudoElements: RadioPseudoElements,
|
|
40712
42162
|
children: [jsx("span", {
|
|
@@ -43687,7 +45137,9 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
43687
45137
|
* carries on to the one being pulled in or puts the current one back — the
|
|
43688
45138
|
* gesture decides, not the distance alone. It walks ONE AXIS, chosen from the
|
|
43689
45139
|
* first few pixels: a diagonal would ask for two travels at once and only one
|
|
43690
|
-
* slide can arrive.
|
|
45140
|
+
* slide can arrive. What a gesture IS — how far it has to go, who else may
|
|
45141
|
+
* claim it, what letting go says — is read in drag_to_travel.js; what is here is
|
|
45142
|
+
* where the slides stand while it happens.
|
|
43691
45143
|
*
|
|
43692
45144
|
* The slides live INSIDE the box, which is what makes this work for a popup: a
|
|
43693
45145
|
* dialog and a popover are both promoted to the browser's top layer, so no
|
|
@@ -43696,6 +45148,18 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
43696
45148
|
* it is the same component in the document, in a dialog or in a popover.
|
|
43697
45149
|
*/
|
|
43698
45150
|
const css$A = /* css */`
|
|
45151
|
+
/* Where the picture stands relative to the slide that is current, in boxes
|
|
45152
|
+
(see paintTravelProgress). Declared, so that it is a NUMBER the browser can
|
|
45153
|
+
interpolate: the trait an indicator draws has to travel with the slides,
|
|
45154
|
+
and an undeclared custom property only ever jumps from one value to the
|
|
45155
|
+
next. Inherited, so anything drawn inside the box can read it, and 0 by
|
|
45156
|
+
default — at rest there is nothing to lean towards. */
|
|
45157
|
+
@property --slide-travel-progress {
|
|
45158
|
+
syntax: "<number>";
|
|
45159
|
+
inherits: true;
|
|
45160
|
+
initial-value: 0;
|
|
45161
|
+
}
|
|
45162
|
+
|
|
43699
45163
|
/* Every slide in the same grid cell: the box then measures itself on the
|
|
43700
45164
|
LARGEST of them, in both directions, without anything being measured by
|
|
43701
45165
|
hand — which is also why nothing here resizes as the slides change. Each
|
|
@@ -43854,6 +45318,19 @@ const ratioOfOneTravel = (track, from, to, targetBefore) => {
|
|
|
43854
45318
|
// A translate ("-100% 0%", "-260px 0px", "none") as two numbers of pixels.
|
|
43855
45319
|
// Percentages are the size of the box, which is what a translate resolves them
|
|
43856
45320
|
// against — so an offset written either way can be measured against another.
|
|
45321
|
+
// Where the track stands right now, in pixels, read off the box it draws in
|
|
45322
|
+
// rather than off the value that moves it: mid-animation the browser reports
|
|
45323
|
+
// that value as a calc() of a percentage and a length ("calc(-43% - 119px)"),
|
|
45324
|
+
// which no simple parse survives — read as 0, it puts the gesture a whole
|
|
45325
|
+
// travel away from what the eye is looking at.
|
|
45326
|
+
const trackOffsetPx = (track, containerElement) => {
|
|
45327
|
+
const trackRect = track.getBoundingClientRect();
|
|
45328
|
+
const containerRect = containerElement.getBoundingClientRect();
|
|
45329
|
+
return {
|
|
45330
|
+
x: trackRect.x - containerRect.x,
|
|
45331
|
+
y: trackRect.y - containerRect.y
|
|
45332
|
+
};
|
|
45333
|
+
};
|
|
43857
45334
|
const offsetToPx = (offset, box) => {
|
|
43858
45335
|
if (!offset || offset === "none") {
|
|
43859
45336
|
return {
|
|
@@ -43888,62 +45365,29 @@ const hurryTravel = animation => {
|
|
|
43888
45365
|
animation.playbackRate = rate > HURRY_RATE_MAX ? HURRY_RATE_MAX : rate;
|
|
43889
45366
|
};
|
|
43890
45367
|
|
|
43891
|
-
//
|
|
43892
|
-
//
|
|
43893
|
-
|
|
43894
|
-
|
|
43895
|
-
|
|
43896
|
-
|
|
43897
|
-
|
|
43898
|
-
|
|
43899
|
-
|
|
43900
|
-
|
|
43901
|
-
|
|
43902
|
-
const
|
|
43903
|
-
|
|
43904
|
-
|
|
43905
|
-
// the gesture is answered (something moves) while saying there is nothing that
|
|
43906
|
-
// way. Let go and it comes back — a wall one can lean on, never walk through.
|
|
43907
|
-
const DRAG_RESISTANCE = 0.3;
|
|
43908
|
-
// What a drag must not start on: something that reads the pointer itself. A
|
|
43909
|
-
// button or a link is not in the list — dragging from one travels, and the
|
|
43910
|
-
// click it would have made is swallowed on the way out (see onDragEnd).
|
|
43911
|
-
const DRAG_EXCLUDED_SELECTOR = ["input", "textarea", "select", '[contenteditable=""]', '[contenteditable="true"]', "[data-no-slide-drag]"].join(",");
|
|
43912
|
-
|
|
43913
|
-
// A scroller between the pointer and the slide it is in, with room left the way
|
|
43914
|
-
// the gesture goes: it gets the gesture, and the slides stay where they are —
|
|
43915
|
-
// dragging a row that scrolls sideways scrolls that row, and only a row with
|
|
43916
|
-
// nowhere left to go hands the travel over.
|
|
43917
|
-
const scrollRoomTowards = (fromElement, stopElement, axis, sign) => {
|
|
43918
|
-
let element = fromElement;
|
|
43919
|
-
while (element && element !== stopElement && element.nodeType === 1) {
|
|
43920
|
-
const size = axis === "x" ? element.clientWidth : element.clientHeight;
|
|
43921
|
-
const scrollSize = axis === "x" ? element.scrollWidth : element.scrollHeight;
|
|
43922
|
-
if (scrollSize > size + 1) {
|
|
43923
|
-
const {
|
|
43924
|
-
overflowX,
|
|
43925
|
-
overflowY
|
|
43926
|
-
} = getComputedStyle(element);
|
|
43927
|
-
const overflow = axis === "x" ? overflowX : overflowY;
|
|
43928
|
-
if (overflow === "auto" || overflow === "scroll") {
|
|
43929
|
-
const position = axis === "x" ? element.scrollLeft : element.scrollTop;
|
|
43930
|
-
// Dragging the content one way reveals what is on the other side of
|
|
43931
|
-
// it: to the right means going back up the scroll.
|
|
43932
|
-
const room = sign > 0 ? position : scrollSize - size - position;
|
|
43933
|
-
if (room > 1) {
|
|
43934
|
-
return true;
|
|
43935
|
-
}
|
|
43936
|
-
}
|
|
45368
|
+
// Which axes an option opens, from a prop that says either yes/no or the axes
|
|
45369
|
+
// themselves — and never more than the map has: a `travelByScroll="y"` on a row
|
|
45370
|
+
// of slides opens nothing, because there is nothing that way to open.
|
|
45371
|
+
const axesAllowedBy = (option, mapAxes) => {
|
|
45372
|
+
if (!option || !mapAxes) {
|
|
45373
|
+
return null;
|
|
45374
|
+
}
|
|
45375
|
+
if (option === true) {
|
|
45376
|
+
return mapAxes;
|
|
45377
|
+
}
|
|
45378
|
+
let allowed = "";
|
|
45379
|
+
for (const axis of mapAxes) {
|
|
45380
|
+
if (option.includes(axis)) {
|
|
45381
|
+
allowed += axis;
|
|
43937
45382
|
}
|
|
43938
|
-
element = element.parentElement;
|
|
43939
45383
|
}
|
|
43940
|
-
return
|
|
45384
|
+
return allowed || null;
|
|
43941
45385
|
};
|
|
43942
45386
|
|
|
43943
45387
|
// Which axes the map has anything on, read from the layout alone: it is what
|
|
43944
45388
|
// says which way a touch may travel, and a touch is answered before any of the
|
|
43945
45389
|
// DOM below has been looked at.
|
|
43946
|
-
const
|
|
45390
|
+
const travelAxesOf = layout => {
|
|
43947
45391
|
if (typeof layout === "string") {
|
|
43948
45392
|
return layout === "column" ? "y" : "x";
|
|
43949
45393
|
}
|
|
@@ -44052,6 +45496,26 @@ const durationToMs = duration => {
|
|
|
44052
45496
|
}
|
|
44053
45497
|
return String(duration).trimEnd().endsWith("ms") ? number : number * 1000;
|
|
44054
45498
|
};
|
|
45499
|
+
|
|
45500
|
+
// What asked for a travel, read off the event that carried it: the hand said
|
|
45501
|
+
// "somewhere over there" (a gesture one browses with), or it said a name (a tab
|
|
45502
|
+
// pressed, a key, a command — a place aimed at). Nothing at all when the travel
|
|
45503
|
+
// came from code, which has no interaction to speak of.
|
|
45504
|
+
const causeOfEvent = event => {
|
|
45505
|
+
if (!event) {
|
|
45506
|
+
return "code";
|
|
45507
|
+
}
|
|
45508
|
+
const {
|
|
45509
|
+
type
|
|
45510
|
+
} = event;
|
|
45511
|
+
if (type === "pointerup" || type === "pointercancel") {
|
|
45512
|
+
return "drag";
|
|
45513
|
+
}
|
|
45514
|
+
if (type === "keydown" || type === "keyup") {
|
|
45515
|
+
return "keyboard";
|
|
45516
|
+
}
|
|
45517
|
+
return "command";
|
|
45518
|
+
};
|
|
44055
45519
|
const readArea = slideElement => slideElement.getAttribute("data-slide-area") || slideElement.id || "";
|
|
44056
45520
|
|
|
44057
45521
|
/**
|
|
@@ -44079,7 +45543,21 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
|
|
|
44079
45543
|
* it says where one starts, not where one is — say `current` for that.
|
|
44080
45544
|
* Without it the first slide is the one shown, the way a stack of pages opens
|
|
44081
45545
|
* on its first page.
|
|
44082
|
-
* @param {(area: string) => void} [props.onCurrentChange]
|
|
45546
|
+
* @param {(area: string, detail: {cause: "drag"|"keyboard"|"command"|"code", event: Event}) => void|false|Promise<void|false>} [props.onCurrentChange]
|
|
45547
|
+
* - the slide being shown has changed. `cause` says what asked for it, which
|
|
45548
|
+
* is what tells a place browsed past from a place aimed at: a caller writing
|
|
45549
|
+
* this into the URL pushes a history entry for a tab that was pressed and
|
|
45550
|
+
* replaces the current one for a slide that was dragged, so three swipes back
|
|
45551
|
+
* and forth do not bury the way out of the page.
|
|
45552
|
+
* Answer `false` to REFUSE the change and the slide goes back where it came
|
|
45553
|
+
* from — a guard that says no, a session that is gone. A promise refuses it
|
|
45554
|
+
* late, once whatever it had to ask has answered; the travel plays meanwhile
|
|
45555
|
+
* and is undone if the answer is no.
|
|
45556
|
+
* @param {"now"|"rest"} [props.commit="now"] - when the change is told.
|
|
45557
|
+
* "rest" waits for the travel to be over, and lets the container hold the
|
|
45558
|
+
* slide it is going to meanwhile: the picture moves with the finger and the
|
|
45559
|
+
* caller is told once, at the end. For a change that costs something or shows
|
|
45560
|
+
* somewhere — the URL, a server — and cannot be asked for per frame.
|
|
44083
45561
|
* @param {boolean} [props.loop] - the slides are a window over something
|
|
44084
45562
|
* endless (days, months, a carousel) rather than places one stays at. A
|
|
44085
45563
|
* travel plays as usual and then the window comes back to the slide it rests
|
|
@@ -44092,17 +45570,28 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
|
|
|
44092
45570
|
* one that just travelled there. Called once the travel is over, and in the
|
|
44093
45571
|
* same render as the return to rest — anything later shows the old content
|
|
44094
45572
|
* for a frame.
|
|
44095
|
-
*
|
|
44096
|
-
*
|
|
44097
|
-
*
|
|
44098
|
-
*
|
|
44099
|
-
*
|
|
44100
|
-
*
|
|
44101
|
-
*
|
|
44102
|
-
*
|
|
44103
|
-
*
|
|
44104
|
-
*
|
|
44105
|
-
*
|
|
45573
|
+
* Each way of travelling can be shut off, or narrowed to one axis: `true`
|
|
45574
|
+
* (every axis the map has), `false`, `"x"`, `"y"`, `"xy"`.
|
|
45575
|
+
*
|
|
45576
|
+
* @param {boolean|"x"|"y"|"xy"} [props.travelByKeyboard=true] - whether the
|
|
45577
|
+
* arrows (and Home/End) walk the map. On by default: a map one can see is a
|
|
45578
|
+
* map one expects to walk. Off when the arrows mean something else where
|
|
45579
|
+
* these slides are — a list of choices one moves through, a picker whose
|
|
45580
|
+
* screens are steps rather than places — so the keys keep the meaning the
|
|
45581
|
+
* content gives them, and travelling stays something one asks for (a button,
|
|
45582
|
+
* a command).
|
|
45583
|
+
* @param {boolean|"x"|"y"|"xy"} [props.travelByDrag=true] - whether a pointer
|
|
45584
|
+
* dragging the slides travels. On by default: slides side by side are
|
|
45585
|
+
* something one expects to push around with a thumb. Off where the gesture
|
|
45586
|
+
* belongs to the content (a canvas one draws on, a map one pans), or where
|
|
45587
|
+
* the slides are steps of a form rather than a row one browses.
|
|
45588
|
+
* @param {boolean|"x"|"y"|"xy"} [props.travelByScroll="x"] - whether a wheel
|
|
45589
|
+
* pushing the box travels, one slide per push. Sideways only by default,
|
|
45590
|
+
* because a page is hardly ever scrollable that way: a sideways scroll over
|
|
45591
|
+
* the box can only have been meant for the box. Down the page it is the
|
|
45592
|
+
* page's OWN gesture, and taking it would move a slide under someone who was
|
|
45593
|
+
* scrolling the document — so a map that travels vertically has to be told
|
|
45594
|
+
* (`travelByScroll` / `"y"` / `"xy"`) before a wheel moves it.
|
|
44106
45595
|
* @param {string} [props.duration="300ms"] - how long a slide change takes.
|
|
44107
45596
|
*/
|
|
44108
45597
|
const SlideContainer = ({
|
|
@@ -44110,10 +45599,12 @@ const SlideContainer = ({
|
|
|
44110
45599
|
current: currentProp,
|
|
44111
45600
|
defaultCurrent,
|
|
44112
45601
|
onCurrentChange,
|
|
45602
|
+
commit = "now",
|
|
44113
45603
|
loop,
|
|
44114
45604
|
onLoop,
|
|
44115
45605
|
travelByKeyboard = true,
|
|
44116
45606
|
travelByDrag = true,
|
|
45607
|
+
travelByScroll = "x",
|
|
44117
45608
|
duration = "300ms",
|
|
44118
45609
|
children,
|
|
44119
45610
|
...rest
|
|
@@ -44127,6 +45618,15 @@ const SlideContainer = ({
|
|
|
44127
45618
|
// The AREA of the slide being shown, not its rank: a rank would be wrong the
|
|
44128
45619
|
// moment a slide appears before it, and there is nothing to renumber here.
|
|
44129
45620
|
const [currentAreaState, setCurrentAreaState] = useState(defaultCurrent);
|
|
45621
|
+
// The slide this container is travelling to while its controller has not been
|
|
45622
|
+
// told yet (commit="rest"): for the length of that travel the container is
|
|
45623
|
+
// ahead of whoever holds `current`, and this is where it keeps its own answer
|
|
45624
|
+
// — dropped as soon as the controller has caught up, or the change was
|
|
45625
|
+
// refused.
|
|
45626
|
+
const [provisionalArea, setProvisionalArea] = useState(null);
|
|
45627
|
+
// The change waiting for the travel to be over: what to tell the caller, and
|
|
45628
|
+
// what to put back if they refuse it.
|
|
45629
|
+
const commitAtRestRef = useRef(null);
|
|
44130
45630
|
// Where the window is while it rolls, and nothing more: a looping container
|
|
44131
45631
|
// rests where it rested before (below), so this is the travel itself rather
|
|
44132
45632
|
// than a change of slide.
|
|
@@ -44180,9 +45680,21 @@ const SlideContainer = ({
|
|
|
44180
45680
|
// track rests: a slide let go of halfway carries on from under the finger.
|
|
44181
45681
|
// Read and dropped by the layout effect, which is the one drawing it.
|
|
44182
45682
|
const travelFromRef = useRef(null);
|
|
44183
|
-
|
|
45683
|
+
// The same fact for the indicator (--slide-travel-progress): how far the
|
|
45684
|
+
// picture is from the slide ARRIVING when the travel starts, in boxes. Null
|
|
45685
|
+
// for a travel nobody dragged, where a whole box is what is left to close.
|
|
45686
|
+
const travelProgressFromRef = useRef(null);
|
|
45687
|
+
const progressAnimationRef = useRef(null);
|
|
45688
|
+
const current = rollingArea ?? provisionalArea ?? currentProp ?? currentAreaState;
|
|
44184
45689
|
const vertical = layout === "column";
|
|
44185
|
-
|
|
45690
|
+
// What the map has, and what each way of asking is allowed to use of it.
|
|
45691
|
+
const mapAxes = travelAxesOf(layout);
|
|
45692
|
+
const dragAxes = axesAllowedBy(travelByDrag, mapAxes);
|
|
45693
|
+
const scrollAxes = axesAllowedBy(travelByScroll, mapAxes);
|
|
45694
|
+
const keyboardAxes = axesAllowedBy(travelByKeyboard, mapAxes);
|
|
45695
|
+
// What must not spill onto the page behind the box: every axis a gesture of
|
|
45696
|
+
// ours can take, whichever gesture it is.
|
|
45697
|
+
const travelAxes = dragAxes && scrollAxes ? axesAllowedBy(`${dragAxes}${scrollAxes}`, mapAxes) : dragAxes || scrollAxes;
|
|
44186
45698
|
// Which required slides have been answered (see Slide's own `required`). Held
|
|
44187
45699
|
// here rather than in each slide because answering one says something about
|
|
44188
45700
|
// the others: the steps after it were answered about a state that has just
|
|
@@ -44201,6 +45713,19 @@ const SlideContainer = ({
|
|
|
44201
45713
|
// one before it wrote, not what the last render saw.
|
|
44202
45714
|
const cameFromRef = useRef({});
|
|
44203
45715
|
|
|
45716
|
+
// The controller has caught up with the slide the container went to on its
|
|
45717
|
+
// own (commit="rest"): there are no longer two answers to give, so the
|
|
45718
|
+
// container gives its own up rather than holding a copy that can go stale.
|
|
45719
|
+
useLayoutEffect(() => {
|
|
45720
|
+
if (provisionalArea === null) {
|
|
45721
|
+
return;
|
|
45722
|
+
}
|
|
45723
|
+
const heldOutside = currentProp ?? currentAreaState;
|
|
45724
|
+
if (heldOutside === provisionalArea) {
|
|
45725
|
+
setProvisionalArea(null);
|
|
45726
|
+
}
|
|
45727
|
+
}, [provisionalArea, currentProp, currentAreaState]);
|
|
45728
|
+
|
|
44204
45729
|
// The travel is given back as soon as the picture it must not animate has
|
|
44205
45730
|
// been painted: one frame with it off is all it takes.
|
|
44206
45731
|
useLayoutEffect(() => {
|
|
@@ -44297,6 +45822,9 @@ const SlideContainer = ({
|
|
|
44297
45822
|
}
|
|
44298
45823
|
stageRef.current = null;
|
|
44299
45824
|
trackAnimationRef.current = null;
|
|
45825
|
+
// At rest the picture IS the current slide, whatever the last gesture wrote
|
|
45826
|
+
// there: an indicator has nothing left to lean towards.
|
|
45827
|
+
paintTravelProgress(0);
|
|
44300
45828
|
for (const slideElement of slideElements) {
|
|
44301
45829
|
const {
|
|
44302
45830
|
x,
|
|
@@ -44320,6 +45848,17 @@ const SlideContainer = ({
|
|
|
44320
45848
|
const offset = `${-x * 100}% ${-y * 100}%`;
|
|
44321
45849
|
offsetRef.current = offset;
|
|
44322
45850
|
track.style.setProperty("--slide-container-offset", offset);
|
|
45851
|
+
// Arrived, so the change can be told (commit="rest"): the picture is at
|
|
45852
|
+
// rest and whatever the caller does with it — write the URL, ask a server —
|
|
45853
|
+
// costs the gesture nothing anymore.
|
|
45854
|
+
const commitAtRest = commitAtRestRef.current;
|
|
45855
|
+
if (commitAtRest && commitAtRest.area === currentArea) {
|
|
45856
|
+
commitAtRestRef.current = null;
|
|
45857
|
+
answerCurrentChange(onCurrentChange(commitAtRest.area, {
|
|
45858
|
+
cause: commitAtRest.cause,
|
|
45859
|
+
event: commitAtRest.event
|
|
45860
|
+
}), commitAtRest.leftArea);
|
|
45861
|
+
}
|
|
44323
45862
|
};
|
|
44324
45863
|
|
|
44325
45864
|
// Everything positional is decided here, from the DOM, once per render: where
|
|
@@ -44352,6 +45891,9 @@ const SlideContainer = ({
|
|
|
44352
45891
|
stageRef.current = null;
|
|
44353
45892
|
}
|
|
44354
45893
|
let stage = stageRef.current;
|
|
45894
|
+
// Which way this travel goes, kept for the indicator: a whole box lies
|
|
45895
|
+
// between the picture and the slide arriving, on the axis it walks.
|
|
45896
|
+
let travelStep = null;
|
|
44355
45897
|
const drawnArea = stage ? stage.area : drawnAreaRef.current;
|
|
44356
45898
|
const travelStarts = !noTravel && durationMs > 0 && drawnArea !== undefined && drawnArea !== currentArea && slideElements.some(slideElement => readArea(slideElement) === drawnArea);
|
|
44357
45899
|
if (travelStarts) {
|
|
@@ -44363,6 +45905,7 @@ const SlideContainer = ({
|
|
|
44363
45905
|
x: Math.sign(realPlaceOf(currentArea).x - realPlaceOf(drawnArea).x),
|
|
44364
45906
|
y: Math.sign(realPlaceOf(currentArea).y - realPlaceOf(drawnArea).y)
|
|
44365
45907
|
};
|
|
45908
|
+
travelStep = step;
|
|
44366
45909
|
// Kept, not replaced: the slides a chain of quick presses has already
|
|
44367
45910
|
// left behind are still trailing off screen, and taking them off stage
|
|
44368
45911
|
// now would blink them out mid-travel.
|
|
@@ -44477,6 +46020,11 @@ const SlideContainer = ({
|
|
|
44477
46020
|
duration: durationMs * travelRatio,
|
|
44478
46021
|
easing
|
|
44479
46022
|
});
|
|
46023
|
+
// The trait travels with the slides: from where the gesture left it when
|
|
46024
|
+
// there was one, from a whole box away when the travel was asked for.
|
|
46025
|
+
const progressFrom = travelProgressFromRef.current ?? (travelStep ? travelStep.x || travelStep.y : 0);
|
|
46026
|
+
travelProgressFromRef.current = null;
|
|
46027
|
+
animateTravelProgress(progressFrom, durationMs * travelRatio, easing);
|
|
44480
46028
|
// Presses still waiting behind this one: it is already late, so it is
|
|
44481
46029
|
// sent home at once rather than played out at the pace of someone who
|
|
44482
46030
|
// has stopped pressing. Someone pressing → four times is asking to be
|
|
@@ -44667,11 +46215,62 @@ const SlideContainer = ({
|
|
|
44667
46215
|
};
|
|
44668
46216
|
return true;
|
|
44669
46217
|
}
|
|
46218
|
+
const leftArea = readArea(currentElement);
|
|
44670
46219
|
setCurrentAreaState(area);
|
|
44671
|
-
onCurrentChange
|
|
46220
|
+
if (!onCurrentChange) {
|
|
46221
|
+
return true;
|
|
46222
|
+
}
|
|
46223
|
+
// What asked for this, read off the interaction rather than carried down
|
|
46224
|
+
// from every caller: it is a fact about the event, and the event is here.
|
|
46225
|
+
// A caller writing the change somewhere that keeps a trace — the URL, a
|
|
46226
|
+
// history — needs it to know whether a place was aimed at or browsed past.
|
|
46227
|
+
const cause = causeOfEvent(event);
|
|
46228
|
+
if (commit === "rest") {
|
|
46229
|
+
// The travel first, the change once it is over: a caller putting it
|
|
46230
|
+
// somewhere expensive or visible (the URL, the address bar, a server)
|
|
46231
|
+
// must not be asked for it sixty times a second, and the picture must not
|
|
46232
|
+
// wait for it either. The container holds the slide it is travelling to
|
|
46233
|
+
// until the answer comes — being ahead of its controller for the length of
|
|
46234
|
+
// one travel is the whole point.
|
|
46235
|
+
setProvisionalArea(area);
|
|
46236
|
+
commitAtRestRef.current = {
|
|
46237
|
+
area,
|
|
46238
|
+
leftArea,
|
|
46239
|
+
cause,
|
|
46240
|
+
event
|
|
46241
|
+
};
|
|
46242
|
+
return true;
|
|
46243
|
+
}
|
|
46244
|
+
answerCurrentChange(onCurrentChange(area, {
|
|
46245
|
+
cause,
|
|
46246
|
+
event
|
|
46247
|
+
}), leftArea);
|
|
44672
46248
|
return true;
|
|
44673
46249
|
};
|
|
44674
46250
|
|
|
46251
|
+
// What a caller says back about a change it was told about: nothing, or a
|
|
46252
|
+
// refusal. `false` refuses it — a guard that says no, a session that is gone —
|
|
46253
|
+
// and a promise refuses it late, once whatever it had to ask has answered. A
|
|
46254
|
+
// refused change is undone here, so what one sees never disagrees with what
|
|
46255
|
+
// the caller holds: the slide goes back where it came from.
|
|
46256
|
+
const answerCurrentChange = (answer, leftArea) => {
|
|
46257
|
+
if (answer === false) {
|
|
46258
|
+
goBackToRefusedArea(leftArea);
|
|
46259
|
+
return;
|
|
46260
|
+
}
|
|
46261
|
+
if (answer && typeof answer.then === "function") {
|
|
46262
|
+
answer.then(value => {
|
|
46263
|
+
if (value === false) {
|
|
46264
|
+
goBackToRefusedArea(leftArea);
|
|
46265
|
+
}
|
|
46266
|
+
});
|
|
46267
|
+
}
|
|
46268
|
+
};
|
|
46269
|
+
const goBackToRefusedArea = leftArea => {
|
|
46270
|
+
setProvisionalArea(null);
|
|
46271
|
+
setCurrentAreaState(leftArea);
|
|
46272
|
+
};
|
|
46273
|
+
|
|
44675
46274
|
// The press kept during a roll, taken once the window rests and the travel is
|
|
44676
46275
|
// given back (noTravel off): by direction when there was one, so it is read
|
|
44677
46276
|
// against the map as it is NOW — the content moved one step under the window
|
|
@@ -44842,6 +46441,59 @@ const SlideContainer = ({
|
|
|
44842
46441
|
const y = drag.baseOffset.y + drag.pull.y;
|
|
44843
46442
|
drag.offset = `${x}px ${y}px`;
|
|
44844
46443
|
track.style.setProperty("--slide-container-offset", drag.offset);
|
|
46444
|
+
paintTravelProgress(drag.progress, drag.areaPulled);
|
|
46445
|
+
};
|
|
46446
|
+
|
|
46447
|
+
// Where the picture stands relative to the slide that is CURRENT, in boxes:
|
|
46448
|
+
// 0 on it, +1 one whole box before it, -1 one box after. Written on the
|
|
46449
|
+
// container so an indicator drawn inside the box — a tab bar, a dot row, a
|
|
46450
|
+
// trait — follows the finger in CSS alone, with nothing measured and no
|
|
46451
|
+
// render per frame. Said about the current slide rather than about the
|
|
46452
|
+
// gesture, so the number stays continuous when the travel commits and the
|
|
46453
|
+
// current slide changes under it.
|
|
46454
|
+
const paintTravelProgress = (progress, area) => {
|
|
46455
|
+
const containerEl = containerRef.current;
|
|
46456
|
+
if (!containerEl) {
|
|
46457
|
+
return;
|
|
46458
|
+
}
|
|
46459
|
+
if (!progress) {
|
|
46460
|
+
containerEl.style.removeProperty("--slide-travel-progress");
|
|
46461
|
+
containerEl.removeAttribute("data-slide-travel-to");
|
|
46462
|
+
return;
|
|
46463
|
+
}
|
|
46464
|
+
containerEl.style.setProperty("--slide-travel-progress", progress);
|
|
46465
|
+
if (area) {
|
|
46466
|
+
containerEl.setAttribute("data-slide-travel-to", area);
|
|
46467
|
+
} else {
|
|
46468
|
+
containerEl.removeAttribute("data-slide-travel-to");
|
|
46469
|
+
}
|
|
46470
|
+
};
|
|
46471
|
+
|
|
46472
|
+
// The indicator, brought home at the pace of the travel it belongs to: the
|
|
46473
|
+
// same duration and the same easing as the track, so the trait and the slides
|
|
46474
|
+
// are one movement. The value it lands on is the one nothing writes (0), so
|
|
46475
|
+
// the animation is left to fall away on its own.
|
|
46476
|
+
const animateTravelProgress = (from, durationMs, easing) => {
|
|
46477
|
+
const containerEl = containerRef.current;
|
|
46478
|
+
progressAnimationRef.current?.cancel();
|
|
46479
|
+
progressAnimationRef.current = null;
|
|
46480
|
+
paintTravelProgress(0);
|
|
46481
|
+
if (!containerEl || !from || !durationMs) {
|
|
46482
|
+
return;
|
|
46483
|
+
}
|
|
46484
|
+
progressAnimationRef.current = containerEl.animate([{
|
|
46485
|
+
"--slide-travel-progress": from
|
|
46486
|
+
}, {
|
|
46487
|
+
"--slide-travel-progress": 0
|
|
46488
|
+
}], {
|
|
46489
|
+
duration: durationMs,
|
|
46490
|
+
easing
|
|
46491
|
+
});
|
|
46492
|
+
progressAnimationRef.current.finished.then(() => {
|
|
46493
|
+
progressAnimationRef.current = null;
|
|
46494
|
+
}, () => {
|
|
46495
|
+
// cancelled by the next travel — that one says where the trait goes
|
|
46496
|
+
});
|
|
44845
46497
|
};
|
|
44846
46498
|
|
|
44847
46499
|
// The two slides the gesture can bring in, placed one box either side of the
|
|
@@ -44907,9 +46559,11 @@ const SlideContainer = ({
|
|
|
44907
46559
|
trackAnimationRef.current = null;
|
|
44908
46560
|
track.style.setProperty("--slide-container-offset", restOffset);
|
|
44909
46561
|
if (!durationMs || !pulled) {
|
|
46562
|
+
paintTravelProgress(0);
|
|
44910
46563
|
settleTravel();
|
|
44911
46564
|
return;
|
|
44912
46565
|
}
|
|
46566
|
+
animateTravelProgress(drag.progress, durationMs * (pulled / size), "ease-out");
|
|
44913
46567
|
const animation = track.animate([{
|
|
44914
46568
|
translate: drag.offset
|
|
44915
46569
|
}, {
|
|
@@ -44923,265 +46577,311 @@ const SlideContainer = ({
|
|
|
44923
46577
|
// cancelled by a travel asked for since — that one carries the stage on
|
|
44924
46578
|
});
|
|
44925
46579
|
};
|
|
44926
|
-
|
|
44927
|
-
|
|
44928
|
-
|
|
44929
|
-
|
|
46580
|
+
|
|
46581
|
+
// A travel that is playing when a gesture arrives is STOPPED where it stands,
|
|
46582
|
+
// before the gesture has said anything about itself. Not at the first pixels
|
|
46583
|
+
// that decide an axis: over those the slides go on at their own speed under a
|
|
46584
|
+
// hand already resting on them, and when the gesture finally takes them they
|
|
46585
|
+
// are pinned to a hand moving at a quite different pace — the slide does not
|
|
46586
|
+
// jump, it stops dead, which is what one reads as a jolt and as "it got away
|
|
46587
|
+
// from me".
|
|
46588
|
+
// A gesture that turns out to be nothing lets the travel carry on from where
|
|
46589
|
+
// it was caught (see onGiveUp).
|
|
46590
|
+
const catchTravelInFlight = () => {
|
|
46591
|
+
const trackElement = trackRef.current;
|
|
46592
|
+
const travelCaught = trackAnimationRef.current;
|
|
46593
|
+
if (!travelCaught || travelCaught.playState !== "running") {
|
|
46594
|
+
return null;
|
|
44930
46595
|
}
|
|
44931
|
-
|
|
44932
|
-
|
|
44933
|
-
|
|
44934
|
-
|
|
44935
|
-
|
|
44936
|
-
|
|
44937
|
-
|
|
44938
|
-
|
|
44939
|
-
|
|
44940
|
-
|
|
44941
|
-
|
|
44942
|
-
|
|
44943
|
-
|
|
44944
|
-
|
|
44945
|
-
|
|
44946
|
-
|
|
44947
|
-
|
|
44948
|
-
|
|
44949
|
-
|
|
44950
|
-
|
|
44951
|
-
|
|
44952
|
-
|
|
44953
|
-
|
|
44954
|
-
|
|
44955
|
-
|
|
44956
|
-
|
|
44957
|
-
|
|
44958
|
-
|
|
44959
|
-
|
|
44960
|
-
|
|
44961
|
-
|
|
44962
|
-
|
|
44963
|
-
|
|
44964
|
-
|
|
44965
|
-
|
|
44966
|
-
|
|
44967
|
-
|
|
44968
|
-
|
|
46596
|
+
const onScreenPx = trackOffsetPx(trackElement, containerRef.current);
|
|
46597
|
+
const offsetOnScreen = `${onScreenPx.x}px ${onScreenPx.y}px`;
|
|
46598
|
+
travelCaught.cancel();
|
|
46599
|
+
// Where it was, held: cancelling an animation puts the track back on the
|
|
46600
|
+
// value underneath it, which is the far end of the travel — the very jump
|
|
46601
|
+
// this is about.
|
|
46602
|
+
trackElement.style.setProperty("--slide-container-offset", offsetOnScreen);
|
|
46603
|
+
return {
|
|
46604
|
+
trackElement,
|
|
46605
|
+
onScreenPx,
|
|
46606
|
+
offsetOnScreen,
|
|
46607
|
+
offsetTarget: offsetRef.current
|
|
46608
|
+
};
|
|
46609
|
+
};
|
|
46610
|
+
// Which way a caught travel was going: a hand reaching for something moving
|
|
46611
|
+
// has no axis left to decide, and a first pixel of tremor read as one gives
|
|
46612
|
+
// the gesture up — and lets go of what it just caught.
|
|
46613
|
+
const axisOfCaughtTravel = caught => {
|
|
46614
|
+
const boxRect = caught.trackElement.getBoundingClientRect();
|
|
46615
|
+
const targetPx = offsetToPx(caught.offsetTarget, boxRect);
|
|
46616
|
+
const towardsX = Math.abs(targetPx.x - caught.onScreenPx.x);
|
|
46617
|
+
const towardsY = Math.abs(targetPx.y - caught.onScreenPx.y);
|
|
46618
|
+
return towardsX >= towardsY ? "x" : "y";
|
|
46619
|
+
};
|
|
46620
|
+
|
|
46621
|
+
// What a travel gesture does to the slides, whoever asked for it: a pointer
|
|
46622
|
+
// dragging the box and a wheel pushing it sideways ask for the same travel,
|
|
46623
|
+
// so they are answered by the same callbacks and only the reading of the
|
|
46624
|
+
// input differs (see drag_to_travel.js). The rules of the gesture are read
|
|
46625
|
+
// there, the geometry of the slides here.
|
|
46626
|
+
const createTravelHandlers = caughtAtStart => {
|
|
46627
|
+
let caughtTravel = caughtAtStart;
|
|
46628
|
+
// The travel in hand, as the slides see it: where the one being dragged
|
|
46629
|
+
// stands, what is either side of it, and how far the gesture has taken it.
|
|
46630
|
+
const drag = {
|
|
46631
|
+
axis: null,
|
|
46632
|
+
area: null,
|
|
46633
|
+
areaBack: null,
|
|
46634
|
+
areaOn: null,
|
|
46635
|
+
areaPulled: null,
|
|
46636
|
+
box: null,
|
|
46637
|
+
basePlace: null,
|
|
46638
|
+
baseOffset: null,
|
|
46639
|
+
pull: {
|
|
44969
46640
|
x: 0,
|
|
44970
46641
|
y: 0
|
|
44971
|
-
}
|
|
44972
|
-
|
|
44973
|
-
|
|
44974
|
-
|
|
44975
|
-
};
|
|
44976
|
-
// Where the track IS, taken over from whatever was playing: a travel
|
|
44977
|
-
// grabbed mid-flight carries on from under the finger, so the animation
|
|
44978
|
-
// is dropped and its position kept.
|
|
44979
|
-
const onScreen = trackAnimationRef.current?.playState === "running" ? offsetToPx(getComputedStyle(track).translate, box) : baseOffset;
|
|
44980
|
-
trackAnimationRef.current?.cancel();
|
|
44981
|
-
trackAnimationRef.current = null;
|
|
44982
|
-
drag.axis = axis;
|
|
44983
|
-
drag.areaBack = areaBack;
|
|
44984
|
-
drag.areaOn = areaOn;
|
|
44985
|
-
drag.area = area;
|
|
44986
|
-
drag.box = box;
|
|
44987
|
-
drag.basePlace = basePlace;
|
|
44988
|
-
drag.baseOffset = baseOffset;
|
|
44989
|
-
// Where the track was when the gesture took it over — nowhere, unless it
|
|
44990
|
-
// was travelling. Every pull is measured from it.
|
|
44991
|
-
drag.slack = {
|
|
44992
|
-
x: onScreen.x - baseOffset.x,
|
|
44993
|
-
y: onScreen.y - baseOffset.y
|
|
44994
|
-
};
|
|
44995
|
-
drag.pull = {
|
|
44996
|
-
...drag.slack
|
|
44997
|
-
};
|
|
44998
|
-
// The pixels spent deciding are not pulled back: the slide starts moving
|
|
44999
|
-
// from where the finger is now, so it follows it exactly rather than
|
|
45000
|
-
// jumping the threshold it just crossed.
|
|
45001
|
-
drag.startX = pointerMoveEvent.clientX;
|
|
45002
|
-
drag.startY = pointerMoveEvent.clientY;
|
|
45003
|
-
stageDrag(drag);
|
|
45004
|
-
drag.lastPosition = axis === "x" ? pointerMoveEvent.clientX : pointerMoveEvent.clientY;
|
|
45005
|
-
drag.lastTime = pointerMoveEvent.timeStamp;
|
|
45006
|
-
containerRef.current.toggleAttribute("data-slide-dragging", true);
|
|
45007
|
-
// Every move from here on, wherever the finger wanders — off the box, off
|
|
45008
|
-
// the window — and the release with it.
|
|
45009
|
-
containerRef.current.setPointerCapture(drag.pointerId);
|
|
45010
|
-
}
|
|
45011
|
-
const {
|
|
45012
|
-
axis
|
|
45013
|
-
} = drag;
|
|
45014
|
-
const size = axis === "x" ? drag.box.width : drag.box.height;
|
|
45015
|
-
const moved = axis === "x" ? pointerMoveEvent.clientX - drag.startX : pointerMoveEvent.clientY - drag.startY;
|
|
45016
|
-
// Measured from where the gesture took the track over, never from what the
|
|
45017
|
-
// move before it painted: the resistance below would otherwise be applied
|
|
45018
|
-
// again to a value it has already shrunk, and a finger going nowhere would
|
|
45019
|
-
// see the slide creep back on its own.
|
|
45020
|
-
let pulled = drag.slack[axis] + moved;
|
|
45021
|
-
// Which slide is being pulled in: dragging the track to the right brings in
|
|
45022
|
-
// the one on the left, which is the one BEFORE it.
|
|
45023
|
-
const areaPulled = pulled > 0 ? drag.areaBack : drag.areaOn;
|
|
45024
|
-
if (!areaPulled) {
|
|
45025
|
-
pulled *= DRAG_RESISTANCE;
|
|
45026
|
-
}
|
|
45027
|
-
if (pulled > size) {
|
|
45028
|
-
pulled = size;
|
|
45029
|
-
} else if (pulled < -size) {
|
|
45030
|
-
pulled = -size;
|
|
45031
|
-
}
|
|
45032
|
-
drag.pull = {
|
|
45033
|
-
...drag.pull,
|
|
45034
|
-
[axis]: pulled
|
|
45035
|
-
};
|
|
45036
|
-
paintDrag();
|
|
45037
|
-
// How fast the hand is going, so that letting go says something a distance
|
|
45038
|
-
// cannot: a short flick travels, a long slow drag put back does not.
|
|
45039
|
-
const position = axis === "x" ? pointerMoveEvent.clientX : pointerMoveEvent.clientY;
|
|
45040
|
-
const elapsed = pointerMoveEvent.timeStamp - drag.lastTime;
|
|
45041
|
-
if (elapsed > 0) {
|
|
45042
|
-
const instant = (position - drag.lastPosition) / elapsed;
|
|
45043
|
-
drag.velocity = drag.velocity * 0.4 + instant * 0.6;
|
|
45044
|
-
}
|
|
45045
|
-
drag.lastPosition = position;
|
|
45046
|
-
drag.lastTime = pointerMoveEvent.timeStamp;
|
|
45047
|
-
};
|
|
45048
|
-
const onDragEnd = pointerEvent => {
|
|
45049
|
-
const drag = dragRef.current;
|
|
45050
|
-
if (!drag || pointerEvent.pointerId !== drag.pointerId) {
|
|
45051
|
-
return;
|
|
45052
|
-
}
|
|
45053
|
-
drag.stop();
|
|
45054
|
-
dragRef.current = null;
|
|
45055
|
-
containerRef.current?.removeAttribute("data-slide-dragging");
|
|
45056
|
-
if (!drag.axis) {
|
|
45057
|
-
// A press that never became a gesture: nothing was staged, nothing moved.
|
|
45058
|
-
return;
|
|
45059
|
-
}
|
|
45060
|
-
// The click the browser makes of a press that travelled: swallowed, or
|
|
45061
|
-
// letting go over a button would press it. Capture, so it never reaches
|
|
45062
|
-
// what it landed on, and dropped right after in case none comes.
|
|
45063
|
-
const swallowClick = clickEvent => {
|
|
45064
|
-
clickEvent.stopPropagation();
|
|
45065
|
-
clickEvent.preventDefault();
|
|
46642
|
+
},
|
|
46643
|
+
progress: 0,
|
|
46644
|
+
offset: null,
|
|
46645
|
+
gesture: null
|
|
45066
46646
|
};
|
|
45067
|
-
|
|
45068
|
-
|
|
45069
|
-
|
|
45070
|
-
|
|
45071
|
-
|
|
45072
|
-
|
|
45073
|
-
})
|
|
45074
|
-
|
|
45075
|
-
|
|
45076
|
-
|
|
45077
|
-
|
|
45078
|
-
|
|
45079
|
-
|
|
45080
|
-
|
|
45081
|
-
|
|
45082
|
-
|
|
45083
|
-
|
|
45084
|
-
|
|
45085
|
-
|
|
45086
|
-
|
|
45087
|
-
|
|
45088
|
-
|
|
45089
|
-
|
|
45090
|
-
|
|
45091
|
-
|
|
45092
|
-
|
|
45093
|
-
|
|
45094
|
-
|
|
45095
|
-
|
|
45096
|
-
|
|
45097
|
-
|
|
45098
|
-
|
|
45099
|
-
|
|
45100
|
-
|
|
45101
|
-
|
|
45102
|
-
|
|
45103
|
-
|
|
45104
|
-
|
|
45105
|
-
|
|
45106
|
-
|
|
45107
|
-
|
|
45108
|
-
|
|
45109
|
-
|
|
45110
|
-
|
|
46647
|
+
return {
|
|
46648
|
+
drag,
|
|
46649
|
+
onStart: ({
|
|
46650
|
+
axis,
|
|
46651
|
+
sign,
|
|
46652
|
+
target
|
|
46653
|
+
}) => {
|
|
46654
|
+
const areaBack = axis === "x" ? areaTowards(-1, 0) : areaTowards(0, -1);
|
|
46655
|
+
const areaOn = axis === "x" ? areaTowards(1, 0) : areaTowards(0, 1);
|
|
46656
|
+
// Everything positional is read HERE rather than when the pointer
|
|
46657
|
+
// landed: the travel that was playing then may have arrived since, and
|
|
46658
|
+
// it is what the slides are doing at the moment the gesture takes them
|
|
46659
|
+
// over that the gesture must carry on from.
|
|
46660
|
+
const track = trackRef.current;
|
|
46661
|
+
const {
|
|
46662
|
+
slideElements,
|
|
46663
|
+
placeOf
|
|
46664
|
+
} = readMap();
|
|
46665
|
+
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
|
|
46666
|
+
const box = track.getBoundingClientRect();
|
|
46667
|
+
if (!areaBack && !areaOn || !currentElement || !box.width || !box.height ||
|
|
46668
|
+
// Something else with a better claim on the gesture: a scroller
|
|
46669
|
+
// between the finger and the slide, with room left that way.
|
|
46670
|
+
scrollRoomTowards(target, currentElement, axis, sign)) {
|
|
46671
|
+
return false;
|
|
46672
|
+
}
|
|
46673
|
+
const area = readArea(currentElement);
|
|
46674
|
+
// Where the slide being dragged stands: where the stage put it while a
|
|
46675
|
+
// travel is playing, its place on the map otherwise.
|
|
46676
|
+
const stage = stageRef.current;
|
|
46677
|
+
const basePlace = stage?.placeByArea.get(area) || placeOf.get(area) || {
|
|
46678
|
+
x: 0,
|
|
46679
|
+
y: 0
|
|
46680
|
+
};
|
|
46681
|
+
const baseOffset = {
|
|
46682
|
+
x: -basePlace.x * box.width,
|
|
46683
|
+
y: -basePlace.y * box.height
|
|
46684
|
+
};
|
|
46685
|
+
// Where the track IS: a travel grabbed mid-flight was already stopped
|
|
46686
|
+
// where the eye saw it, at the press — this only reads that place, so
|
|
46687
|
+
// the gesture starts from it rather than from where the map rests.
|
|
46688
|
+
const onScreen = caughtTravel ? caughtTravel.onScreenPx : baseOffset;
|
|
46689
|
+
caughtTravel = null;
|
|
46690
|
+
trackAnimationRef.current?.cancel();
|
|
46691
|
+
trackAnimationRef.current = null;
|
|
46692
|
+
progressAnimationRef.current?.cancel();
|
|
46693
|
+
progressAnimationRef.current = null;
|
|
46694
|
+
drag.axis = axis;
|
|
46695
|
+
drag.areaBack = areaBack;
|
|
46696
|
+
drag.areaOn = areaOn;
|
|
46697
|
+
drag.area = area;
|
|
46698
|
+
drag.box = box;
|
|
46699
|
+
drag.basePlace = basePlace;
|
|
46700
|
+
drag.baseOffset = baseOffset;
|
|
46701
|
+
const size = axis === "x" ? box.width : box.height;
|
|
46702
|
+
// Where the track was when the gesture took it over — nowhere, unless
|
|
46703
|
+
// it was travelling. Every pull is measured from it.
|
|
46704
|
+
const slack = axis === "x" ? onScreen.x - baseOffset.x : onScreen.y - baseOffset.y;
|
|
46705
|
+
drag.pull = {
|
|
46706
|
+
x: 0,
|
|
46707
|
+
y: 0,
|
|
46708
|
+
[axis]: slack
|
|
46709
|
+
};
|
|
46710
|
+
drag.progress = slack / size;
|
|
46711
|
+
stageDrag(drag);
|
|
46712
|
+
containerRef.current.toggleAttribute("data-slide-dragging", true);
|
|
46713
|
+
// From here the box is busy, whichever input asked: a wheel gesture and
|
|
46714
|
+
// a press must not both be moving the same track.
|
|
46715
|
+
dragRef.current = drag;
|
|
46716
|
+
return {
|
|
46717
|
+
size,
|
|
46718
|
+
slack,
|
|
46719
|
+
travelBack: Boolean(areaBack),
|
|
46720
|
+
travelOn: Boolean(areaOn)
|
|
46721
|
+
};
|
|
46722
|
+
},
|
|
46723
|
+
onPull: ({
|
|
46724
|
+
axis,
|
|
46725
|
+
pulled,
|
|
46726
|
+
progress
|
|
46727
|
+
}) => {
|
|
46728
|
+
drag.pull = {
|
|
46729
|
+
...drag.pull,
|
|
46730
|
+
[axis]: pulled
|
|
46731
|
+
};
|
|
46732
|
+
drag.progress = progress;
|
|
46733
|
+
drag.areaPulled = pulled > 0 ? drag.areaBack : drag.areaOn;
|
|
46734
|
+
paintDrag();
|
|
46735
|
+
},
|
|
46736
|
+
onEnd: ({
|
|
46737
|
+
axis,
|
|
46738
|
+
sign,
|
|
46739
|
+
travels,
|
|
46740
|
+
event
|
|
46741
|
+
}) => {
|
|
46742
|
+
dragRef.current = null;
|
|
46743
|
+
containerRef.current?.removeAttribute("data-slide-dragging");
|
|
46744
|
+
if (!travels) {
|
|
46745
|
+
returnToRest(drag);
|
|
46746
|
+
return;
|
|
46747
|
+
}
|
|
46748
|
+
// Where the slide is being left, for the travel to depart from instead
|
|
46749
|
+
// of from the map.
|
|
46750
|
+
travelFromRef.current = drag.offset;
|
|
46751
|
+
// …and where the indicator is being left, said about the slide that is
|
|
46752
|
+
// ARRIVING: the picture is `sign` of a box short of it, and that is
|
|
46753
|
+
// what the travel about to be drawn has to close.
|
|
46754
|
+
travelProgressFromRef.current = drag.progress - sign;
|
|
46755
|
+
const moved = axis === "x" ? move(-sign, 0, event) : move(0, -sign, event);
|
|
46756
|
+
if (!moved) {
|
|
46757
|
+
// Nowhere to go after all — a slide holding on to the user
|
|
46758
|
+
// (preventNav), or a caller that refused the change.
|
|
46759
|
+
travelFromRef.current = null;
|
|
46760
|
+
travelProgressFromRef.current = null;
|
|
46761
|
+
returnToRest(drag);
|
|
46762
|
+
return;
|
|
46763
|
+
}
|
|
46764
|
+
// A container whose `current` is held outside and was not moved:
|
|
46765
|
+
// nothing rendered, so nothing drew the travel and the track is still
|
|
46766
|
+
// under where the finger left it. One frame is all it takes to know.
|
|
46767
|
+
requestAnimationFrame(() => {
|
|
46768
|
+
if (travelFromRef.current) {
|
|
46769
|
+
travelFromRef.current = null;
|
|
46770
|
+
travelProgressFromRef.current = null;
|
|
46771
|
+
returnToRest(drag);
|
|
46772
|
+
}
|
|
46773
|
+
});
|
|
46774
|
+
},
|
|
46775
|
+
onGiveUp: () => {
|
|
46776
|
+
dragRef.current = null;
|
|
46777
|
+
// A press that never became a gesture: what it stopped goes on its way,
|
|
46778
|
+
// from where the finger caught it and over what is left of the travel.
|
|
46779
|
+
if (!caughtTravel) {
|
|
46780
|
+
return;
|
|
46781
|
+
}
|
|
46782
|
+
const {
|
|
46783
|
+
trackElement: trackCaught,
|
|
46784
|
+
offsetOnScreen,
|
|
46785
|
+
offsetTarget,
|
|
46786
|
+
onScreenPx: caughtOnScreenPx
|
|
46787
|
+
} = caughtTravel;
|
|
46788
|
+
caughtTravel = null;
|
|
46789
|
+
if (offsetOnScreen === offsetTarget) {
|
|
46790
|
+
settleTravel();
|
|
46791
|
+
return;
|
|
46792
|
+
}
|
|
46793
|
+
const durationMs = durationToMs(duration);
|
|
46794
|
+
// What is LEFT of it, at the pace it had: a travel caught nine tenths
|
|
46795
|
+
// of the way there and let go of does not start its duration again.
|
|
46796
|
+
const boxRect = trackCaught.getBoundingClientRect();
|
|
46797
|
+
const targetPx = offsetToPx(offsetTarget, boxRect);
|
|
46798
|
+
const leftToCover = Math.abs(drag.axis === "y" ? targetPx.y - caughtOnScreenPx.y : targetPx.x - caughtOnScreenPx.x);
|
|
46799
|
+
const size = drag.axis === "y" ? boxRect.height : boxRect.width;
|
|
46800
|
+
const travelRatio = size ? leftToCover / size : 1;
|
|
46801
|
+
trackCaught.style.setProperty("--slide-container-offset", offsetTarget);
|
|
46802
|
+
trackAnimationRef.current = trackCaught.animate([{
|
|
46803
|
+
translate: offsetOnScreen
|
|
46804
|
+
}, {
|
|
46805
|
+
translate: offsetTarget
|
|
46806
|
+
}], {
|
|
46807
|
+
duration: durationMs * travelRatio,
|
|
46808
|
+
easing: "ease-out"
|
|
46809
|
+
});
|
|
46810
|
+
trackAnimationRef.current.finished.then(settleTravel, () => {
|
|
46811
|
+
// taken over by a travel asked for since
|
|
46812
|
+
});
|
|
45111
46813
|
}
|
|
45112
|
-
}
|
|
46814
|
+
};
|
|
45113
46815
|
};
|
|
46816
|
+
|
|
46817
|
+
// Whether a gesture may begin at all: one travel at a time, and a window
|
|
46818
|
+
// mid-roll has nothing to drag yet — it is on its way somewhere and the
|
|
46819
|
+
// content that goes with it has not moved (see goToArea).
|
|
46820
|
+
const canStartTravel = () => dragAxes && !dragRef.current && !rollingRef.current;
|
|
45114
46821
|
const startDrag = pointerDownEvent => {
|
|
45115
|
-
if (!
|
|
46822
|
+
if (!canStartTravel()) {
|
|
45116
46823
|
return;
|
|
45117
46824
|
}
|
|
45118
|
-
|
|
45119
|
-
|
|
45120
|
-
|
|
46825
|
+
const caughtTravel = catchTravelInFlight();
|
|
46826
|
+
const caughtAxis = caughtTravel ? axisOfCaughtTravel(caughtTravel) : null;
|
|
46827
|
+
const handlers = createTravelHandlers(caughtTravel);
|
|
46828
|
+
const gesture = startDragToTravel(pointerDownEvent, {
|
|
46829
|
+
element: containerRef.current,
|
|
46830
|
+
axes: dragAxes,
|
|
46831
|
+
// Caught in flight: the hand is already in the gesture, so it is answered
|
|
46832
|
+
// from its first pixel rather than after a threshold it has no reason to
|
|
46833
|
+
// cross twice — on the axis what it caught is travelling on.
|
|
46834
|
+
immediate: caughtAxis && dragAxes.includes(caughtAxis) ? caughtAxis : false,
|
|
46835
|
+
...handlers
|
|
46836
|
+
});
|
|
46837
|
+
if (!gesture) {
|
|
45121
46838
|
return;
|
|
45122
46839
|
}
|
|
45123
|
-
|
|
45124
|
-
|
|
46840
|
+
handlers.drag.gesture = gesture;
|
|
46841
|
+
dragRef.current = handlers.drag;
|
|
46842
|
+
};
|
|
46843
|
+
|
|
46844
|
+
// A wheel pushing the box sideways asks for a SLIDE, not for a place between
|
|
46845
|
+
// two: one push, one slide — the same thing an arrow key asks for, and it
|
|
46846
|
+
// plays at its own pace rather than under a hand (see watchWheelTravel).
|
|
46847
|
+
// Watched for the whole life of the box because such a gesture has no press
|
|
46848
|
+
// to start it: it begins with its first event.
|
|
46849
|
+
//
|
|
46850
|
+
// Reached through a ref and never rebuilt for it: a travel CHANGES which
|
|
46851
|
+
// slide is current, so a watcher listening on that would be torn down halfway
|
|
46852
|
+
// through the very gesture moving it.
|
|
46853
|
+
const travelOneStepRef = useRef(null);
|
|
46854
|
+
travelOneStepRef.current = ({
|
|
46855
|
+
axis,
|
|
46856
|
+
sign,
|
|
46857
|
+
event
|
|
46858
|
+
}) => {
|
|
46859
|
+
if (dragRef.current) {
|
|
46860
|
+
// A hand is holding the slides. They are its until it lets go.
|
|
45125
46861
|
return;
|
|
45126
46862
|
}
|
|
45127
|
-
|
|
45128
|
-
|
|
45129
|
-
|
|
45130
|
-
|
|
45131
|
-
|
|
45132
|
-
onDragEnd(pointerEvent);
|
|
45133
|
-
};
|
|
45134
|
-
// The browser's own drag, which a mouse starts on a link or an image after
|
|
45135
|
-
// a few pixels: it would take the pointer away mid-gesture and leave the
|
|
45136
|
-
// slides hanging.
|
|
45137
|
-
const preventNativeDrag = dragStartEvent => {
|
|
45138
|
-
dragStartEvent.preventDefault();
|
|
45139
|
-
};
|
|
45140
|
-
const stop = () => {
|
|
45141
|
-
containerEl.removeEventListener("pointermove", onMove);
|
|
45142
|
-
containerEl.removeEventListener("dragstart", preventNativeDrag);
|
|
45143
|
-
window.removeEventListener("pointerup", onEnd);
|
|
45144
|
-
window.removeEventListener("pointercancel", onEnd);
|
|
45145
|
-
};
|
|
45146
|
-
dragRef.current = {
|
|
45147
|
-
pointerId: pointerDownEvent.pointerId,
|
|
45148
|
-
target,
|
|
45149
|
-
startX: pointerDownEvent.clientX,
|
|
45150
|
-
startY: pointerDownEvent.clientY,
|
|
45151
|
-
// Nothing but a press so far: the axis, the slides either side of the one
|
|
45152
|
-
// being dragged and where they all stand are read the moment the finger
|
|
45153
|
-
// says which way it is going (see onDragMove).
|
|
45154
|
-
axis: null,
|
|
45155
|
-
areaBack: null,
|
|
45156
|
-
areaOn: null,
|
|
45157
|
-
slack: {
|
|
45158
|
-
x: 0,
|
|
45159
|
-
y: 0
|
|
45160
|
-
},
|
|
45161
|
-
pull: {
|
|
45162
|
-
x: 0,
|
|
45163
|
-
y: 0
|
|
45164
|
-
},
|
|
45165
|
-
offset: null,
|
|
45166
|
-
velocity: 0,
|
|
45167
|
-
lastPosition: 0,
|
|
45168
|
-
lastTime: pointerDownEvent.timeStamp,
|
|
45169
|
-
stop
|
|
45170
|
-
};
|
|
45171
|
-
containerEl.addEventListener("pointermove", onMove);
|
|
45172
|
-
containerEl.addEventListener("dragstart", preventNativeDrag);
|
|
45173
|
-
// On the window, not on the box: a pointer released outside it (or taken
|
|
45174
|
-
// away by the browser) must still end the gesture, or the slides would stay
|
|
45175
|
-
// where the finger left them.
|
|
45176
|
-
window.addEventListener("pointerup", onEnd);
|
|
45177
|
-
window.addEventListener("pointercancel", onEnd);
|
|
46863
|
+
if (axis === "x") {
|
|
46864
|
+
move(-sign, 0, event);
|
|
46865
|
+
} else {
|
|
46866
|
+
move(0, -sign, event);
|
|
46867
|
+
}
|
|
45178
46868
|
};
|
|
46869
|
+
useLayoutEffect(() => {
|
|
46870
|
+
if (!scrollAxes) {
|
|
46871
|
+
return undefined;
|
|
46872
|
+
}
|
|
46873
|
+
return watchWheelTravel(containerRef.current, {
|
|
46874
|
+
axes: scrollAxes,
|
|
46875
|
+
onStep: detail => travelOneStepRef.current(detail)
|
|
46876
|
+
});
|
|
46877
|
+
}, [scrollAxes]);
|
|
45179
46878
|
|
|
45180
46879
|
// A gesture is listening on things that outlive this component.
|
|
45181
46880
|
useLayoutEffect(() => {
|
|
45182
46881
|
return () => {
|
|
45183
|
-
dragRef.current?.stop();
|
|
46882
|
+
dragRef.current?.gesture?.stop();
|
|
45184
46883
|
dragRef.current = null;
|
|
46884
|
+
progressAnimationRef.current?.cancel();
|
|
45185
46885
|
};
|
|
45186
46886
|
}, []);
|
|
45187
46887
|
|
|
@@ -45211,27 +46911,27 @@ const SlideContainer = ({
|
|
|
45211
46911
|
// `enabled` leaves the key to whatever else wants it (see
|
|
45212
46912
|
// keyboard_shortcuts.js), rather than swallowing it here.
|
|
45213
46913
|
arrowright: {
|
|
45214
|
-
enabled:
|
|
46914
|
+
enabled: Boolean(keyboardAxes?.includes("x")),
|
|
45215
46915
|
handler: e => travelled(move(1, 0, e))
|
|
45216
46916
|
},
|
|
45217
46917
|
arrowleft: {
|
|
45218
|
-
enabled:
|
|
46918
|
+
enabled: Boolean(keyboardAxes?.includes("x")),
|
|
45219
46919
|
handler: e => travelled(move(-1, 0, e))
|
|
45220
46920
|
},
|
|
45221
46921
|
arrowdown: {
|
|
45222
|
-
enabled:
|
|
46922
|
+
enabled: Boolean(keyboardAxes?.includes("y")),
|
|
45223
46923
|
handler: e => travelled(move(0, 1, e))
|
|
45224
46924
|
},
|
|
45225
46925
|
arrowup: {
|
|
45226
|
-
enabled:
|
|
46926
|
+
enabled: Boolean(keyboardAxes?.includes("y")),
|
|
45227
46927
|
handler: e => travelled(move(0, -1, e))
|
|
45228
46928
|
},
|
|
45229
46929
|
home: {
|
|
45230
|
-
enabled:
|
|
46930
|
+
enabled: Boolean(keyboardAxes),
|
|
45231
46931
|
handler: e => travelled(goToEnd(false, e))
|
|
45232
46932
|
},
|
|
45233
46933
|
end: {
|
|
45234
|
-
enabled:
|
|
46934
|
+
enabled: Boolean(keyboardAxes),
|
|
45235
46935
|
handler: e => travelled(goToEnd(true, e))
|
|
45236
46936
|
}
|
|
45237
46937
|
});
|
|
@@ -45249,7 +46949,13 @@ const SlideContainer = ({
|
|
|
45249
46949
|
// has seen the gesture.
|
|
45250
46950
|
,
|
|
45251
46951
|
|
|
45252
|
-
"data-travel-by-drag": dragAxes ?? undefined
|
|
46952
|
+
"data-travel-by-drag": dragAxes ?? undefined
|
|
46953
|
+
// The same fact, read by the shared gesture stylesheet: what scrolls
|
|
46954
|
+
// inside a box that travels must not spill onto the page behind it (see
|
|
46955
|
+
// drag_to_travel.js).
|
|
46956
|
+
,
|
|
46957
|
+
|
|
46958
|
+
"data-drag-travel": travelAxes ?? undefined,
|
|
45253
46959
|
onPointerDown: e => {
|
|
45254
46960
|
startDrag(e);
|
|
45255
46961
|
rest.onPointerDown?.(e);
|
|
@@ -48327,7 +50033,17 @@ const ListSelectable = props => {
|
|
|
48327
50033
|
name: "select",
|
|
48328
50034
|
prevented: () => e.preventDefault(),
|
|
48329
50035
|
// tell the requester that we don't want to select this item
|
|
48330
|
-
|
|
50036
|
+
// Asked of the item too, not only of the list: an item can be the one
|
|
50037
|
+
// refusing (the list already holds all it accepts, see maxLength), and
|
|
50038
|
+
// it is the one that then says why.
|
|
50039
|
+
allowed: () => {
|
|
50040
|
+
dispatchRequestInteraction(childController.ref.current, {
|
|
50041
|
+
event: e,
|
|
50042
|
+
name: "select",
|
|
50043
|
+
prevented: () => e.preventDefault(),
|
|
50044
|
+
allowed: () => childController.setUIState(childController.value, e)
|
|
50045
|
+
});
|
|
50046
|
+
}
|
|
48331
50047
|
});
|
|
48332
50048
|
},
|
|
48333
50049
|
onnavi_request_unselect: e => {
|
|
@@ -49269,6 +50985,48 @@ const css$v = /* css */`
|
|
|
49269
50985
|
display: none;
|
|
49270
50986
|
}
|
|
49271
50987
|
}
|
|
50988
|
+
|
|
50989
|
+
/* <List itemTransition>: the rows are named — a change the application wraps
|
|
50990
|
+
in a view transition is then seen row by row — and the pictures of the rows
|
|
50991
|
+
are drawn INSIDE the picture of the list, which is what lets the list's edge
|
|
50992
|
+
cut them.
|
|
50993
|
+
|
|
50994
|
+
Whether any of it happens is decided HERE and not in JS: rows that are named
|
|
50995
|
+
without being contained animate across the page (the pictures live in the
|
|
50996
|
+
top layer, where no overflow of the document reaches them), which is worse
|
|
50997
|
+
than not animating at all. So a browser with no nested groups gets no name
|
|
50998
|
+
either, and the change simply happens. */
|
|
50999
|
+
@supports (view-transition-group: contain) {
|
|
51000
|
+
.navi_list_container[data-item-transition] {
|
|
51001
|
+
/* The list needs a name to be a group at all; which name does not matter,
|
|
51002
|
+
only that no other element in the document carries it. */
|
|
51003
|
+
view-transition-name: match-element;
|
|
51004
|
+
view-transition-class: navi_list_transition;
|
|
51005
|
+
view-transition-group: contain;
|
|
51006
|
+
|
|
51007
|
+
/* A row is paired across the change by the id of the item it holds, never
|
|
51008
|
+
by the element that happens to hold it: rows are recycled as the list
|
|
51009
|
+
scrolls, and pairing on the element would pair the wrong two. */
|
|
51010
|
+
[data-view-transition-name] {
|
|
51011
|
+
view-transition-name: attr(
|
|
51012
|
+
data-view-transition-name type(<custom-ident>)
|
|
51013
|
+
);
|
|
51014
|
+
view-transition-class: navi_list_item;
|
|
51015
|
+
}
|
|
51016
|
+
}
|
|
51017
|
+
}
|
|
51018
|
+
|
|
51019
|
+
/* The list's edge, during the transition. */
|
|
51020
|
+
::view-transition-group-children(.navi_list_transition) {
|
|
51021
|
+
overflow: clip;
|
|
51022
|
+
}
|
|
51023
|
+
/* The list box is the same thing before and after — only its rows moved — and
|
|
51024
|
+
a cross-fade of something onto itself is a flicker. */
|
|
51025
|
+
::view-transition-old(.navi_list_transition),
|
|
51026
|
+
::view-transition-new(.navi_list_transition) {
|
|
51027
|
+
mix-blend-mode: normal;
|
|
51028
|
+
animation: none;
|
|
51029
|
+
}
|
|
49272
51030
|
`;
|
|
49273
51031
|
const ListUI = props => {
|
|
49274
51032
|
import.meta.css = [css$v, "@jsenv/navi/src/control/list/list.jsx"];
|
|
@@ -49471,6 +51229,7 @@ const ListUI = props => {
|
|
|
49471
51229
|
...rest,
|
|
49472
51230
|
ref: ref,
|
|
49473
51231
|
baseClassName: "navi_list_container",
|
|
51232
|
+
"data-item-transition": itemTransition ? "" : undefined,
|
|
49474
51233
|
popover: popover,
|
|
49475
51234
|
"data-horizontal": horizontal ? "" : undefined,
|
|
49476
51235
|
"data-scroller": getScrollerAttribute(scroller),
|
|
@@ -49539,6 +51298,9 @@ const ListFirstResolver = props => {
|
|
|
49539
51298
|
*
|
|
49540
51299
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
49541
51300
|
* selectable?: boolean,
|
|
51301
|
+
* multiple?: boolean,
|
|
51302
|
+
* maxLength?: number,
|
|
51303
|
+
* maxLengthGuard?: number,
|
|
49542
51304
|
* action?: (value: any) => void,
|
|
49543
51305
|
* uiAction?: (value: any) => void,
|
|
49544
51306
|
* popover?: boolean,
|
|
@@ -49572,6 +51334,22 @@ const ListFirstResolver = props => {
|
|
|
49572
51334
|
* children?: import("ignore:preact").ComponentChildren,
|
|
49573
51335
|
* [key: string]: any,
|
|
49574
51336
|
* }>}
|
|
51337
|
+
* @param {boolean} [props.itemTransition]
|
|
51338
|
+
* Names each row, so a change the application wraps in
|
|
51339
|
+
* `document.startViewTransition` is seen row by row — rows moving to their new
|
|
51340
|
+
* place, an arriving row appearing where it lands — instead of the list
|
|
51341
|
+
* cross-fading as a block. The rows are drawn inside the list's own picture,
|
|
51342
|
+
* so one coming from outside the visible part of the list is cut at the list's
|
|
51343
|
+
* edge like any other overflow. Requires nested view transition groups
|
|
51344
|
+
* (Chrome/Edge 140+): elsewhere the rows are left unnamed and the change
|
|
51345
|
+
* simply happens — but the browser still names the document root, so the page
|
|
51346
|
+
* cross-fades as a whole unless the application says otherwise, which is its
|
|
51347
|
+
* call and not the list's:
|
|
51348
|
+
* ```css
|
|
51349
|
+
* @supports not (view-transition-group: contain) {
|
|
51350
|
+
* :root { view-transition-name: none; }
|
|
51351
|
+
* }
|
|
51352
|
+
* ```
|
|
49575
51353
|
* @param {false|((index: number) => any)} [props.renderSkeleton]
|
|
49576
51354
|
* What a row on its way looks like — a row of the shape the real ones will
|
|
49577
51355
|
* have, so nothing moves when they arrive. Used for the rows a `<List.Items>`
|
|
@@ -49625,6 +51403,19 @@ const ListFirstResolver = props => {
|
|
|
49625
51403
|
* scroll once it fills up is picked up then. When that is still not the box
|
|
49626
51404
|
* you mean, say so: `"document"`, or the element itself (a ref works) —
|
|
49627
51405
|
* nothing is guessed then.
|
|
51406
|
+
* @param {number} [props.maxLength]
|
|
51407
|
+
* How many items a `selectable multiple` list accepts — the same word, and
|
|
51408
|
+
* the same behaviour, as `maxLength` on a text field: a rule the list is
|
|
51409
|
+
* judged against, not a wall. A longer selection is allowed to exist and is
|
|
51410
|
+
* reported as invalid, which is what lets a value coming from elsewhere (an
|
|
51411
|
+
* API, a URL) be shown and then corrected.
|
|
51412
|
+
* @param {number} [props.maxLengthGuard]
|
|
51413
|
+
* The same limit, enforced as the selection is made: while the list holds as
|
|
51414
|
+
* many items as it accepts, the ones not selected go read-only — still
|
|
51415
|
+
* pointable, focusable and pressable, answering `"[max] max."` instead of
|
|
51416
|
+
* taking — and `uiAction` is not called. The selected ones stay takeable
|
|
51417
|
+
* back, so a selection that arrived too long can always be brought back
|
|
51418
|
+
* under the limit. Implies `maxLength` for validity.
|
|
49628
51419
|
*/
|
|
49629
51420
|
const List = createComponentResolver([ListFirstResolver, ListSelectableResolver, ListUI]);
|
|
49630
51421
|
const ListContent = ({
|
|
@@ -51328,11 +53119,9 @@ const ListItemReal = props => {
|
|
|
51328
53119
|
// if any (there is no standalone highlight prop — see ListItem's own doc).
|
|
51329
53120
|
useSearchHighlight(ref, matchInfo?.matchRanges, [children, hidden]);
|
|
51330
53121
|
const columnsOverrideProps = useListItemColumnsOverrideProps(rest.style);
|
|
51331
|
-
// <List itemTransition>: the row
|
|
51332
|
-
//
|
|
51333
|
-
//
|
|
51334
|
-
// row is only partly visible (see usePartiallyHidden) — a row half-scrolled
|
|
51335
|
-
// out of its container would otherwise animate from a clipped snapshot.
|
|
53122
|
+
// <List itemTransition>: the row carries the name it is to be paired by, and
|
|
53123
|
+
// the stylesheet turns it into a view-transition-name where a browser can
|
|
53124
|
+
// draw it inside the list (see the @supports block in the css above).
|
|
51336
53125
|
const itemTransition = useContext(ItemTransitionContext);
|
|
51337
53126
|
|
|
51338
53127
|
// Pressing a row that is busy or read-only must say why nothing happens,
|
|
@@ -51420,8 +53209,7 @@ const ListItemReal = props => {
|
|
|
51420
53209
|
"aria-busy": loading ? "true" : undefined,
|
|
51421
53210
|
"aria-readonly": readOnly ? "true" : undefined,
|
|
51422
53211
|
"navi-error": error ? "" : undefined,
|
|
51423
|
-
|
|
51424
|
-
viewTransitionClass: itemTransition ? "navi_list_item" : rest.viewTransitionClass,
|
|
53212
|
+
"data-view-transition-name": itemTransition ? `navi_list_item_${id}` : undefined,
|
|
51425
53213
|
onPointerDownCapture: blocked ? explainBlockedInteraction : undefined,
|
|
51426
53214
|
onClickCapture: blocked ? event => {
|
|
51427
53215
|
if (!pressStartedHereRef.current) {
|
|
@@ -54895,6 +56683,27 @@ const css$p = /* css */`
|
|
|
54895
56683
|
}
|
|
54896
56684
|
}
|
|
54897
56685
|
`;
|
|
56686
|
+
|
|
56687
|
+
/**
|
|
56688
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
56689
|
+
* maxLength?: number,
|
|
56690
|
+
* maxLengthGuard?: number,
|
|
56691
|
+
* [key: string]: any,
|
|
56692
|
+
* }>}
|
|
56693
|
+
* @param {number} [props.maxLength]
|
|
56694
|
+
* How many boxes the group accepts — the same word, and the same behaviour,
|
|
56695
|
+
* as `maxLength` on a text field: a rule the group is judged against, not a
|
|
56696
|
+
* wall. More checked boxes than that is allowed to exist and reported as
|
|
56697
|
+
* invalid, which is what lets a value coming from elsewhere be shown and then
|
|
56698
|
+
* corrected.
|
|
56699
|
+
* @param {number} [props.maxLengthGuard]
|
|
56700
|
+
* The same limit, enforced as the boxes are checked: while the group holds as
|
|
56701
|
+
* many as it accepts, the unchecked ones go read-only — still focusable and
|
|
56702
|
+
* pressable, answering `"[max] max."` instead of checking — and `uiAction` is
|
|
56703
|
+
* not called. The checked ones can always be unchecked, so a value that
|
|
56704
|
+
* arrived too long can always be brought back under the limit. Implies
|
|
56705
|
+
* `maxLength` for validity.
|
|
56706
|
+
*/
|
|
54898
56707
|
const CheckboxGroup = props => {
|
|
54899
56708
|
const refDefault = useRef(null);
|
|
54900
56709
|
props.ref = props.ref || refDefault;
|
|
@@ -58550,6 +60359,7 @@ function WheelUI(props) {
|
|
|
58550
60359
|
})
|
|
58551
60360
|
}), jsxs("div", {
|
|
58552
60361
|
className: "navi_wheel_viewport",
|
|
60362
|
+
"data-no-drag-travel": "",
|
|
58553
60363
|
children: [jsx("div", {
|
|
58554
60364
|
className: "navi_wheel_pane",
|
|
58555
60365
|
"data-side": "start"
|
|
@@ -59346,7 +61156,7 @@ const initDragTableColumnViaPointer = (pointerdownEvent, {
|
|
|
59346
61156
|
onDrag,
|
|
59347
61157
|
onRelease
|
|
59348
61158
|
}) => {
|
|
59349
|
-
|
|
61159
|
+
dragAfterIntent(pointerdownEvent, () => {
|
|
59350
61160
|
const [teardown, addTeardown] = createPubSub();
|
|
59351
61161
|
const tableCell = pointerdownEvent.target.closest(".navi_table_cell");
|
|
59352
61162
|
const table = tableCell.closest(".navi_table");
|
|
@@ -59811,6 +61621,7 @@ const TableColumnLeftResizeHandle = ({
|
|
|
59811
61621
|
} = useContext(TableSizeContext);
|
|
59812
61622
|
return jsx("div", {
|
|
59813
61623
|
className: "navi_table_cell_resize_handle",
|
|
61624
|
+
"data-no-drag-travel": "",
|
|
59814
61625
|
"data-left": "",
|
|
59815
61626
|
onPointerDown: e => {
|
|
59816
61627
|
if (e.button !== 0) {
|
|
@@ -59848,6 +61659,7 @@ const TableColumnRightResizeHandle = ({
|
|
|
59848
61659
|
} = useContext(TableSizeContext);
|
|
59849
61660
|
return jsx("div", {
|
|
59850
61661
|
className: "navi_table_cell_resize_handle",
|
|
61662
|
+
"data-no-drag-travel": "",
|
|
59851
61663
|
"data-right": "",
|
|
59852
61664
|
onPointerDown: e => {
|
|
59853
61665
|
if (e.button !== 0) {
|
|
@@ -60133,6 +61945,7 @@ const TableRowTopResizeHandle = ({
|
|
|
60133
61945
|
} = useContext(TableSizeContext);
|
|
60134
61946
|
return jsx("div", {
|
|
60135
61947
|
className: "navi_table_cell_resize_handle",
|
|
61948
|
+
"data-no-drag-travel": "",
|
|
60136
61949
|
"data-top": "",
|
|
60137
61950
|
onPointerDown: e => {
|
|
60138
61951
|
if (e.button !== 0) {
|
|
@@ -60170,6 +61983,7 @@ const TableRowBottomResizeHandle = ({
|
|
|
60170
61983
|
} = useContext(TableSizeContext);
|
|
60171
61984
|
return jsx("div", {
|
|
60172
61985
|
className: "navi_table_cell_resize_handle",
|
|
61986
|
+
"data-no-drag-travel": "",
|
|
60173
61987
|
"data-bottom": "",
|
|
60174
61988
|
onPointerDown: e => {
|
|
60175
61989
|
if (e.button !== 0) {
|
|
@@ -62094,7 +63908,14 @@ const TableCell = props => {
|
|
|
62094
63908
|
"aria-selected": selected,
|
|
62095
63909
|
"data-value": selectionValue,
|
|
62096
63910
|
"data-editing": editing ? "" : undefined,
|
|
62097
|
-
"data-grabbed": columnGrabbed ? "" : undefined
|
|
63911
|
+
"data-grabbed": columnGrabbed ? "" : undefined
|
|
63912
|
+
// A column that can be dragged reads the pointer itself, so the gesture
|
|
63913
|
+
// is its own and a container travelling by drag must not take it (see
|
|
63914
|
+
// drag_to_travel.js). Said here rather than left to whoever puts a table in
|
|
63915
|
+
// a swipeable page: they cannot know, and this can.
|
|
63916
|
+
,
|
|
63917
|
+
|
|
63918
|
+
"data-no-drag-travel": innerCanDragColumn ? "" : undefined,
|
|
62098
63919
|
onClick: onClick,
|
|
62099
63920
|
onPointerDown: e => {
|
|
62100
63921
|
if (!innerCanDragColumn) {
|
|
@@ -64839,5 +66660,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
64839
66660
|
})
|
|
64840
66661
|
});
|
|
64841
66662
|
|
|
64842
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
|
|
66663
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
|
|
64843
66664
|
//# sourceMappingURL=jsenv_navi.js.map
|