@jsenv/navi 0.29.508 → 0.29.510
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev/jsenv_navi.js +128 -51
- package/dist/dev/jsenv_navi.js.map +5 -5
- package/dist/jsenv_navi.js +128 -51
- package/dist/jsenv_navi.js.map +5 -5
- package/docs/navigation.md +9 -6
- package/package.json +1 -1
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -8918,7 +8918,7 @@ const route = (pattern, options = {}) => {
|
|
|
8918
8918
|
};
|
|
8919
8919
|
route.redirectTo = (
|
|
8920
8920
|
params,
|
|
8921
|
-
{ callReason, history = "replace", debounceUrl } = {},
|
|
8921
|
+
{ callReason, history = "replace", debounceUrl, isSignalChange } = {},
|
|
8922
8922
|
) => {
|
|
8923
8923
|
if (!integration) {
|
|
8924
8924
|
{
|
|
@@ -8933,6 +8933,7 @@ const route = (pattern, options = {}) => {
|
|
|
8933
8933
|
replace: history !== "push",
|
|
8934
8934
|
debounce: debounceUrl,
|
|
8935
8935
|
callReason,
|
|
8936
|
+
fromSignal: isSignalChange,
|
|
8936
8937
|
});
|
|
8937
8938
|
};
|
|
8938
8939
|
route.replaceParams = (
|
|
@@ -8947,52 +8948,18 @@ const route = (pattern, options = {}) => {
|
|
|
8947
8948
|
return null;
|
|
8948
8949
|
}
|
|
8949
8950
|
|
|
8950
|
-
|
|
8951
|
-
let mostSpecificRoute = route;
|
|
8952
|
-
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
8953
|
-
let maxDepth = routePrivateProperties.routePattern.depth;
|
|
8954
|
-
for (const routeCandidate of routeSet) {
|
|
8955
|
-
if (routeCandidate === route) {
|
|
8956
|
-
continue;
|
|
8957
|
-
}
|
|
8958
|
-
if (!routeCandidate.matching) {
|
|
8959
|
-
continue;
|
|
8960
|
-
}
|
|
8961
|
-
const matchingRoute = routeCandidate;
|
|
8962
|
-
const matchingRoutePrivateProperties =
|
|
8963
|
-
getRoutePrivateProperties(matchingRoute);
|
|
8964
|
-
const depth = matchingRoutePrivateProperties.routePattern.depth;
|
|
8965
|
-
if (depth > maxDepth) {
|
|
8966
|
-
maxDepth = depth;
|
|
8967
|
-
mostSpecificRoute = matchingRoute;
|
|
8968
|
-
}
|
|
8969
|
-
}
|
|
8951
|
+
const mostSpecificRoute = findMostSpecificMatchingRoute(route, routeSet);
|
|
8970
8952
|
const isMostSpecificRoute = mostSpecificRoute === route;
|
|
8971
8953
|
|
|
8972
|
-
// If we found a more specific route, delegate to it; otherwise handle it ourselves
|
|
8954
|
+
// If we found a more specific route, delegate to it; otherwise handle it ourselves.
|
|
8955
|
+
// A signal change reaches this only when the more specific route does not
|
|
8956
|
+
// carry the param (see writeValueInUrl): left to it, the change would be dropped.
|
|
8973
8957
|
if (!isMostSpecificRoute) {
|
|
8974
|
-
// For signal-originated calls, only skip delegation if the more specific route
|
|
8975
|
-
// has its own signal connection for the same params — meaning its own effect will
|
|
8976
|
-
// fire and handle the redirect. If it doesn't have the connection, the signal
|
|
8977
|
-
// change would be silently dropped, so we must delegate anyway.
|
|
8978
|
-
if (isSignalChange) {
|
|
8979
|
-
const mostSpecificRoutePrivateProperties =
|
|
8980
|
-
getRoutePrivateProperties(mostSpecificRoute);
|
|
8981
|
-
const { pathConnectionMap, queryConnectionMap } =
|
|
8982
|
-
mostSpecificRoutePrivateProperties.routePattern;
|
|
8983
|
-
const willHandleItself = Object.keys(newParams).every(
|
|
8984
|
-
(paramName) =>
|
|
8985
|
-
pathConnectionMap.has(paramName) ||
|
|
8986
|
-
queryConnectionMap.has(paramName),
|
|
8987
|
-
);
|
|
8988
|
-
if (willHandleItself) {
|
|
8989
|
-
return null;
|
|
8990
|
-
}
|
|
8991
|
-
}
|
|
8992
8958
|
return mostSpecificRoute.redirectTo(newParams, {
|
|
8993
8959
|
callReason: `replaceParams delegation from ${route} to ${mostSpecificRoute} (original reason: ${callReason})`,
|
|
8994
8960
|
history,
|
|
8995
8961
|
debounceUrl,
|
|
8962
|
+
isSignalChange,
|
|
8996
8963
|
});
|
|
8997
8964
|
}
|
|
8998
8965
|
|
|
@@ -9016,6 +8983,7 @@ const route = (pattern, options = {}) => {
|
|
|
9016
8983
|
replace: history !== "push",
|
|
9017
8984
|
debounce: debounceUrl,
|
|
9018
8985
|
callReason,
|
|
8986
|
+
fromSignal: isSignalChange,
|
|
9019
8987
|
});
|
|
9020
8988
|
};
|
|
9021
8989
|
route.matchesParams = (providedParams) => {
|
|
@@ -9078,7 +9046,7 @@ const route = (pattern, options = {}) => {
|
|
|
9078
9046
|
});
|
|
9079
9047
|
// Signal -> URL sync: When signal changes, update URL to reflect meaningful state
|
|
9080
9048
|
// Only sync non-default values to keep URLs clean (static fallbacks stay invisible)
|
|
9081
|
-
registerSetup(() => {
|
|
9049
|
+
registerSetup(({ routeSet }) => {
|
|
9082
9050
|
const cleanupSignalUrlEffectSet = new Set();
|
|
9083
9051
|
// important: read connections at setup time so it includes query connections
|
|
9084
9052
|
// inherited from ancestor patterns
|
|
@@ -9117,6 +9085,24 @@ const route = (pattern, options = {}) => {
|
|
|
9117
9085
|
// Route not matching, no URL sync needed
|
|
9118
9086
|
return;
|
|
9119
9087
|
}
|
|
9088
|
+
// A param declared on an ancestor is carried by every route below it,
|
|
9089
|
+
// each with its own connection: all of them hear the write, the most
|
|
9090
|
+
// specific one makes it. The others step aside before the history is
|
|
9091
|
+
// read — a stateSignalGroup's push goes to the first write reading it.
|
|
9092
|
+
const mostSpecificRoute = findMostSpecificMatchingRoute(
|
|
9093
|
+
route,
|
|
9094
|
+
routeSet,
|
|
9095
|
+
);
|
|
9096
|
+
if (mostSpecificRoute !== route) {
|
|
9097
|
+
const { pathConnectionMap, queryConnectionMap } =
|
|
9098
|
+
getRoutePrivateProperties(mostSpecificRoute).routePattern;
|
|
9099
|
+
if (
|
|
9100
|
+
pathConnectionMap.has(paramName) ||
|
|
9101
|
+
queryConnectionMap.has(paramName)
|
|
9102
|
+
) {
|
|
9103
|
+
return;
|
|
9104
|
+
}
|
|
9105
|
+
}
|
|
9120
9106
|
if (urlParamValue === undefined) {
|
|
9121
9107
|
// No URL parameter exists - check if signal has meaningful value to add
|
|
9122
9108
|
if (connection.isDefaultValue(value)) {
|
|
@@ -9909,6 +9895,27 @@ const urlWithoutSearchAndHash = (url) => {
|
|
|
9909
9895
|
return `${urlObject.origin}${urlObject.pathname}`;
|
|
9910
9896
|
};
|
|
9911
9897
|
|
|
9898
|
+
// Deeper is more specific: among the routes matching the address, the one a
|
|
9899
|
+
// params write lands on.
|
|
9900
|
+
const findMostSpecificMatchingRoute = (route, routeSet) => {
|
|
9901
|
+
let mostSpecificRoute = route;
|
|
9902
|
+
let maxDepth = getRoutePrivateProperties(route).routePattern.depth;
|
|
9903
|
+
for (const routeCandidate of routeSet) {
|
|
9904
|
+
if (routeCandidate === route) {
|
|
9905
|
+
continue;
|
|
9906
|
+
}
|
|
9907
|
+
if (!routeCandidate.matching) {
|
|
9908
|
+
continue;
|
|
9909
|
+
}
|
|
9910
|
+
const depth = getRoutePrivateProperties(routeCandidate).routePattern.depth;
|
|
9911
|
+
if (depth > maxDepth) {
|
|
9912
|
+
maxDepth = depth;
|
|
9913
|
+
mostSpecificRoute = routeCandidate;
|
|
9914
|
+
}
|
|
9915
|
+
}
|
|
9916
|
+
return mostSpecificRoute;
|
|
9917
|
+
};
|
|
9918
|
+
|
|
9912
9919
|
// Every route action, with whether it asks something of the address at the
|
|
9913
9920
|
// moment: its route matches and its params getter returns something.
|
|
9914
9921
|
const routeActionEntriesSignal = signal([]);
|
|
@@ -14496,11 +14503,15 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
14496
14503
|
// browser answers — a before/after cycle here would be about a navigation
|
|
14497
14504
|
// that never happens. A push with something of its own for its entry — a
|
|
14498
14505
|
// state, what the press handed over (see carryByLinkNavigation) — is a
|
|
14499
|
-
// new entry, never one standing there already.
|
|
14506
|
+
// new entry, never one standing there already. So is a push a signal write
|
|
14507
|
+
// makes: the writes after it in the same move (a layer, then what is open
|
|
14508
|
+
// inside it) build on its address at once, and a traversal would only land
|
|
14509
|
+
// after they went to the entry being left.
|
|
14500
14510
|
if (
|
|
14501
14511
|
options.navigationType === "push" &&
|
|
14502
14512
|
options.state === undefined &&
|
|
14503
14513
|
!options.pressWrites &&
|
|
14514
|
+
!options.fromSignal &&
|
|
14504
14515
|
url !== readAddress()
|
|
14505
14516
|
) {
|
|
14506
14517
|
const delta = adjacentEntryDelta(url);
|
|
@@ -14923,12 +14934,23 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
14923
14934
|
|
|
14924
14935
|
const navTo = async (
|
|
14925
14936
|
url,
|
|
14926
|
-
{
|
|
14937
|
+
{
|
|
14938
|
+
replace,
|
|
14939
|
+
state,
|
|
14940
|
+
routeTransition,
|
|
14941
|
+
debounce,
|
|
14942
|
+
asReturn,
|
|
14943
|
+
open,
|
|
14944
|
+
fromSignal,
|
|
14945
|
+
} = {},
|
|
14927
14946
|
) => {
|
|
14928
14947
|
const routingOptions = {
|
|
14929
14948
|
reason: `navTo called with "${url}"`,
|
|
14930
14949
|
navigationType: replace ? "replace" : "push",
|
|
14931
14950
|
state,
|
|
14951
|
+
// Made by a signal written into the url (see route.js writeValueInUrl):
|
|
14952
|
+
// such a push is never turned into a traversal (see handleRoutingTask).
|
|
14953
|
+
fromSignal,
|
|
14932
14954
|
// A push that lands where its address was last left (see
|
|
14933
14955
|
// link_return.js).
|
|
14934
14956
|
asReturn,
|
|
@@ -48466,7 +48488,7 @@ const useRenderScope = (init, update) => {
|
|
|
48466
48488
|
* interface UIStateController {
|
|
48467
48489
|
* id: string;
|
|
48468
48490
|
* controlType: string; // Used for debug logging
|
|
48469
|
-
* uiStateSignal: Signal;
|
|
48491
|
+
* uiStateSignal: Signal | null; // What a button child with no value of its own inherits (null: nothing)
|
|
48470
48492
|
* registerChild(child): void; // Called on child mount
|
|
48471
48493
|
* onChildUIAction(child, e, { stateChanged: boolean }): void; // Called when a child fires a UI action
|
|
48472
48494
|
* unregisterChild(child): void; // Called on child unmount
|
|
@@ -50872,7 +50894,12 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
50872
50894
|
props,
|
|
50873
50895
|
pushStateDownToChild,
|
|
50874
50896
|
ref: realUIStateController.ref,
|
|
50875
|
-
|
|
50897
|
+
// Nothing for a button in the popup to inherit. The picker's value is
|
|
50898
|
+
// what its one child holds, and a button is refused as that child
|
|
50899
|
+
// (canRegisterAsFacadeChild): it answers for itself. Handed the value,
|
|
50900
|
+
// its action would run with the picker's selection over the params it
|
|
50901
|
+
// was bound with — a "refresh" in a sort picker sending the sort order.
|
|
50902
|
+
uiStateSignal: null,
|
|
50876
50903
|
controlHostProps: realUIStateController.controlHostProps,
|
|
50877
50904
|
readOnlyWithoutRun: realUIStateController.readOnlyWithoutRun,
|
|
50878
50905
|
registerChild: (child) => {
|
|
@@ -51003,7 +51030,6 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
51003
51030
|
(s) => {
|
|
51004
51031
|
s.controller.props = props;
|
|
51005
51032
|
s.controller.ref = realUIStateController.ref;
|
|
51006
|
-
s.controller.uiStateSignal = realUIStateController.uiStateSignal;
|
|
51007
51033
|
s.controller.controlHostProps = realUIStateController.controlHostProps;
|
|
51008
51034
|
s.controller.readOnlyWithoutRun =
|
|
51009
51035
|
realUIStateController.readOnlyWithoutRun;
|
|
@@ -56759,18 +56785,69 @@ const TextShrinkWrap = props => {
|
|
|
56759
56785
|
shrinkWrap: undefined
|
|
56760
56786
|
});
|
|
56761
56787
|
};
|
|
56762
|
-
// Whether the text is cut is read
|
|
56763
|
-
//
|
|
56764
|
-
//
|
|
56788
|
+
// Whether the text is cut is read from its line boxes against the content box,
|
|
56789
|
+
// on the axis navi cuts it on: one line ends in an ellipsis on the inline axis,
|
|
56790
|
+
// n lines are clamped on the block one. Not from scrollWidth/scrollHeight: the
|
|
56791
|
+
// scroll extent holds the padding, so a text cut inside its end padding would
|
|
56792
|
+
// read as fitting.
|
|
56765
56793
|
const isTextTruncated = (element, maxLines) => {
|
|
56766
56794
|
if (maxLines === 1 || maxLines === "1") {
|
|
56767
|
-
|
|
56795
|
+
const content = getContentEdges(element);
|
|
56796
|
+
// The ellipsis is painted over a line laid out at its full width. The
|
|
56797
|
+
// 0.5px absorbs the rounding of a box that exactly fits.
|
|
56798
|
+
return getLineRects(element).some(rect => rect.right - content.right > 0.5 || content.left - rect.left > 0.5);
|
|
56768
56799
|
}
|
|
56769
56800
|
if (maxLines > 1) {
|
|
56770
|
-
|
|
56801
|
+
const content = getContentEdges(element);
|
|
56802
|
+
// A clamped line is still laid out, under the content box. Its middle is
|
|
56803
|
+
// read, not its top: under a line-height of 1 or less the glyphs of the
|
|
56804
|
+
// next line start above the bottom of the one before.
|
|
56805
|
+
return getLineRects(element).some(rect => rect.top + rect.height / 2 > content.bottom);
|
|
56771
56806
|
}
|
|
56772
56807
|
return false;
|
|
56773
56808
|
};
|
|
56809
|
+
// Read from the in-flow children: an overlay positioned over the box (a loading
|
|
56810
|
+
// skeleton) fills the padding and is not part of a line.
|
|
56811
|
+
const getLineRects = element => {
|
|
56812
|
+
const range = document.createRange();
|
|
56813
|
+
const rects = [];
|
|
56814
|
+
for (const child of element.childNodes) {
|
|
56815
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
56816
|
+
const {
|
|
56817
|
+
position
|
|
56818
|
+
} = window.getComputedStyle(child);
|
|
56819
|
+
if (position === "absolute" || position === "fixed") {
|
|
56820
|
+
continue;
|
|
56821
|
+
}
|
|
56822
|
+
}
|
|
56823
|
+
range.selectNode(child);
|
|
56824
|
+
rects.push(...range.getClientRects());
|
|
56825
|
+
}
|
|
56826
|
+
return rects;
|
|
56827
|
+
};
|
|
56828
|
+
// In the coordinates of the line rects: a transformed ancestor (a pressed
|
|
56829
|
+
// button's scale) shrinks the rects and the box, not the computed paddings.
|
|
56830
|
+
const getContentEdges = element => {
|
|
56831
|
+
const style = window.getComputedStyle(element);
|
|
56832
|
+
const box = element.getBoundingClientRect();
|
|
56833
|
+
const scaleX = scaleOf(box.width, element.offsetWidth);
|
|
56834
|
+
const scaleY = scaleOf(box.height, element.offsetHeight);
|
|
56835
|
+
const startX = parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft);
|
|
56836
|
+
const endX = parseFloat(style.borderRightWidth) + parseFloat(style.paddingRight);
|
|
56837
|
+
const endY = parseFloat(style.borderBottomWidth) + parseFloat(style.paddingBottom);
|
|
56838
|
+
return {
|
|
56839
|
+
left: box.left + startX * scaleX,
|
|
56840
|
+
right: box.right - endX * scaleX,
|
|
56841
|
+
bottom: box.bottom - endY * scaleY
|
|
56842
|
+
};
|
|
56843
|
+
};
|
|
56844
|
+
const scaleOf = (screenSize, layoutSize) => {
|
|
56845
|
+
// A box with no size has no padding or border to scale either.
|
|
56846
|
+
if (layoutSize === 0) {
|
|
56847
|
+
return 1;
|
|
56848
|
+
}
|
|
56849
|
+
return screenSize / layoutSize;
|
|
56850
|
+
};
|
|
56774
56851
|
const TextReportingTruncation = ({
|
|
56775
56852
|
onTruncatedChange,
|
|
56776
56853
|
...props
|