@jsenv/navi 0.29.20 → 0.29.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/jsenv_navi.js +669 -32
- package/dist/jsenv_navi.js.map +89 -32
- package/docs/actions.md +117 -250
- package/docs/resource.md +88 -0
- package/docs/scroll.md +1 -1
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -30198,6 +30198,109 @@ const getParamScope = (params) => {
|
|
|
30198
30198
|
return newParamScope;
|
|
30199
30199
|
};
|
|
30200
30200
|
|
|
30201
|
+
/*
|
|
30202
|
+
* GET_PAGE: reading a resource one slice at a time, for a list that draws its
|
|
30203
|
+
* rows as it goes (`<List.Items itemsAction>`).
|
|
30204
|
+
*
|
|
30205
|
+
* It is on purpose not an action. An action keeps the one response it got and
|
|
30206
|
+
* replays it, and takes a place in the rerun graph — two things a slice must
|
|
30207
|
+
* not do: the list already holds the slices it received and glues them back
|
|
30208
|
+
* together, and a mutation would otherwise send every slice ever loaded back to
|
|
30209
|
+
* the network at once. So the reader keeps nothing. It runs the callback with
|
|
30210
|
+
* the range the list asks for, writes what comes back into the store, and hands
|
|
30211
|
+
* the rows back as store items — never a copy of the JSON, so the relations a
|
|
30212
|
+
* row reads are the shared ones and a request sent from a row is read back on
|
|
30213
|
+
* it. A row following its own fields through a write reads them from the store
|
|
30214
|
+
* (`RESOURCE.useById(id)`): an update replaces the item object, and the one the
|
|
30215
|
+
* list is holding is the one it was given.
|
|
30216
|
+
*
|
|
30217
|
+
* The reader is a function, so a list feeds on it the way it feeds on any other
|
|
30218
|
+
* source: `itemsAction={GAME.GET_PAGE.bindParams({ radar })}`.
|
|
30219
|
+
*/
|
|
30220
|
+
|
|
30221
|
+
|
|
30222
|
+
const createPageReader = (
|
|
30223
|
+
actionName,
|
|
30224
|
+
callback,
|
|
30225
|
+
{ store, params: boundParams },
|
|
30226
|
+
) => {
|
|
30227
|
+
const readPage = async (range = {}) => {
|
|
30228
|
+
const { signal, ...rangeParams } = range;
|
|
30229
|
+
const paramsResolved = { ...resolveParams(boundParams), ...rangeParams };
|
|
30230
|
+
const result = await callback(paramsResolved, { signal });
|
|
30231
|
+
if (!result || !Array.isArray(result.items)) {
|
|
30232
|
+
throw new TypeError(
|
|
30233
|
+
`${actionName} must return { items, start, count }, received ${describeResult(result)}.`,
|
|
30234
|
+
);
|
|
30235
|
+
}
|
|
30236
|
+
const items = store.upsert(result.items);
|
|
30237
|
+
let { start, count } = result;
|
|
30238
|
+
if (start === undefined) {
|
|
30239
|
+
const startAsked = rangeParams.start;
|
|
30240
|
+
if (startAsked === undefined || startAsked < 0) {
|
|
30241
|
+
throw new TypeError(
|
|
30242
|
+
`${actionName} must say where the page lands (start), it was asked for ${describeRangeAsked(rangeParams)}.`,
|
|
30243
|
+
);
|
|
30244
|
+
}
|
|
30245
|
+
start = startAsked;
|
|
30246
|
+
}
|
|
30247
|
+
if (count === undefined) {
|
|
30248
|
+
count = start + items.length;
|
|
30249
|
+
}
|
|
30250
|
+
return { items, start, count };
|
|
30251
|
+
};
|
|
30252
|
+
Object.defineProperty(readPage, "name", { value: actionName });
|
|
30253
|
+
readPage.isPageReader = true;
|
|
30254
|
+
readPage.bindParams = (paramsToBind) => {
|
|
30255
|
+
return createPageReader(actionName, callback, {
|
|
30256
|
+
store,
|
|
30257
|
+
params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
|
|
30258
|
+
});
|
|
30259
|
+
};
|
|
30260
|
+
return readPage;
|
|
30261
|
+
};
|
|
30262
|
+
|
|
30263
|
+
// Params bound to a reader may be signals (the radar currently on screen); the
|
|
30264
|
+
// value they hold when the page is asked for is the one the page is about.
|
|
30265
|
+
const resolveParams = (params) => {
|
|
30266
|
+
if (!params) {
|
|
30267
|
+
return {};
|
|
30268
|
+
}
|
|
30269
|
+
const paramsResolved = {};
|
|
30270
|
+
for (const key of Object.keys(params)) {
|
|
30271
|
+
const value = params[key];
|
|
30272
|
+
paramsResolved[key] = isSignal(value) ? value.value : value;
|
|
30273
|
+
}
|
|
30274
|
+
return paramsResolved;
|
|
30275
|
+
};
|
|
30276
|
+
|
|
30277
|
+
const describeResult = (result) => {
|
|
30278
|
+
if (Array.isArray(result)) {
|
|
30279
|
+
return `an array of ${result.length} item${result.length === 1 ? "" : "s"}`;
|
|
30280
|
+
}
|
|
30281
|
+
if (result && typeof result === "object") {
|
|
30282
|
+
return `an object holding ${Object.keys(result).join(", ") || "nothing"}`;
|
|
30283
|
+
}
|
|
30284
|
+
return `${result}`;
|
|
30285
|
+
};
|
|
30286
|
+
|
|
30287
|
+
const describeRangeAsked = (rangeParams) => {
|
|
30288
|
+
const { start, limit, around, before, after } = rangeParams;
|
|
30289
|
+
if (around !== undefined) {
|
|
30290
|
+
return `the rows around "${around}"`;
|
|
30291
|
+
}
|
|
30292
|
+
if (before !== undefined) {
|
|
30293
|
+
return `the ${limit} rows before "${before}"`;
|
|
30294
|
+
}
|
|
30295
|
+
if (after !== undefined) {
|
|
30296
|
+
return `the ${limit} rows after "${after}"`;
|
|
30297
|
+
}
|
|
30298
|
+
if (start < 0) {
|
|
30299
|
+
return `the last ${limit} rows`;
|
|
30300
|
+
}
|
|
30301
|
+
return `${limit} rows from ${start}`;
|
|
30302
|
+
};
|
|
30303
|
+
|
|
30201
30304
|
const resourceLifecycleManager = createResourceLifecycleManager();
|
|
30202
30305
|
const debug$2 = (args) => {
|
|
30203
30306
|
{
|
|
@@ -30216,13 +30319,18 @@ const debug$2 = (args) => {
|
|
|
30216
30319
|
* - GET / POST / PUT / PATCH → the full item object, e.g. `{ id, name }`
|
|
30217
30320
|
* - DELETE → the id or `{ id }` of the removed item
|
|
30218
30321
|
* - GET_MANY / POST_MANY / … → an array of item objects
|
|
30322
|
+
* - GET_PAGE → `{ items, start, count }`, one slice of the collection
|
|
30323
|
+
*
|
|
30324
|
+
* `GET_PAGE` is a reader rather than an action: it keeps no value and takes no place in
|
|
30325
|
+
* the rerun graph, so a `<List.Items>` can feed on it slice by slice
|
|
30326
|
+
* (`itemsAction={USER.GET_PAGE.bindParams({ team })}`).
|
|
30219
30327
|
*
|
|
30220
30328
|
* A sub-resource of the backend (`/games/:id/candidates`) must be modelled with a
|
|
30221
30329
|
* relationship method, never as an `op`/`type` discriminator dispatched inside one
|
|
30222
30330
|
* verb's callback.
|
|
30223
30331
|
*
|
|
30224
30332
|
* @param {string} name - resource name, used in action names and error messages
|
|
30225
|
-
* @param {Object} restCallbacks - `{ idKey, uniqueKeys, rerunOn, dependencies, GET, GET_MANY, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
|
|
30333
|
+
* @param {Object} restCallbacks - `{ idKey, uniqueKeys, rerunOn, dependencies, GET, GET_MANY, GET_PAGE, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
|
|
30226
30334
|
* @param {string} [restCallbacks.idKey] - primary key property, defaults to `"id"` (or the first `uniqueKeys` entry)
|
|
30227
30335
|
* @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`); a callback may return a different `id` to rename the item's primary key
|
|
30228
30336
|
* @see docs/resource.md — relationships, callback return contracts, decision table
|
|
@@ -30246,6 +30354,7 @@ const resource = (
|
|
|
30246
30354
|
|
|
30247
30355
|
GET,
|
|
30248
30356
|
GET_MANY,
|
|
30357
|
+
GET_PAGE,
|
|
30249
30358
|
POST,
|
|
30250
30359
|
POST_MANY,
|
|
30251
30360
|
PUT,
|
|
@@ -30311,6 +30420,7 @@ const resource = (
|
|
|
30311
30420
|
restCallbacks: {
|
|
30312
30421
|
GET,
|
|
30313
30422
|
GET_MANY,
|
|
30423
|
+
GET_PAGE,
|
|
30314
30424
|
POST,
|
|
30315
30425
|
POST_MANY,
|
|
30316
30426
|
PUT,
|
|
@@ -31392,6 +31502,16 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
31392
31502
|
if (restCallback === undefined) {
|
|
31393
31503
|
continue;
|
|
31394
31504
|
}
|
|
31505
|
+
if (restCallbackKey === "GET_PAGE") {
|
|
31506
|
+
// A page is read, never kept: no action, no place in the rerun graph
|
|
31507
|
+
// (see resource_page_reader.js).
|
|
31508
|
+
stateFacade.GET_PAGE = createPageReader(
|
|
31509
|
+
`${name}.GET_PAGE`,
|
|
31510
|
+
restCallback,
|
|
31511
|
+
{ store, params },
|
|
31512
|
+
);
|
|
31513
|
+
continue;
|
|
31514
|
+
}
|
|
31395
31515
|
const isMany = restCallbackKey.endsWith("_MANY");
|
|
31396
31516
|
const verb = isMany
|
|
31397
31517
|
? restCallbackKey.replace("_MANY", "")
|
|
@@ -43562,6 +43682,13 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
43562
43682
|
* the slides in between fly past — least of all in a tab bar, where they are
|
|
43563
43683
|
* not a road one travels but places one goes straight to.
|
|
43564
43684
|
*
|
|
43685
|
+
* A finger (or a mouse) drags the slides too: the track follows the pointer, the
|
|
43686
|
+
* neighbours are brought alongside for the occasion, and letting go either
|
|
43687
|
+
* carries on to the one being pulled in or puts the current one back — the
|
|
43688
|
+
* gesture decides, not the distance alone. It walks ONE AXIS, chosen from the
|
|
43689
|
+
* first few pixels: a diagonal would ask for two travels at once and only one
|
|
43690
|
+
* slide can arrive.
|
|
43691
|
+
*
|
|
43565
43692
|
* The slides live INSIDE the box, which is what makes this work for a popup: a
|
|
43566
43693
|
* dialog and a popover are both promoted to the browser's top layer, so no
|
|
43567
43694
|
* container of ours could ever hold two of them side by side and translate the
|
|
@@ -43609,6 +43736,28 @@ const css$A = /* css */`
|
|
|
43609
43736
|
outline: none;
|
|
43610
43737
|
}
|
|
43611
43738
|
|
|
43739
|
+
/* What a touch may do here: the axis the slides travel on is taken (it is
|
|
43740
|
+
what the gesture drags), the other one is left to the page — so a
|
|
43741
|
+
carousel in an article is swiped sideways and the article still scrolls
|
|
43742
|
+
under the same finger. A map travelling both ways takes both.
|
|
43743
|
+
A scroller INSIDE a slide is not concerned: touch-action is read up to
|
|
43744
|
+
the scroll container the gesture would move, so a row that scrolls
|
|
43745
|
+
sideways within a slide still scrolls sideways. */
|
|
43746
|
+
&[data-travel-by-drag="x"] {
|
|
43747
|
+
touch-action: pan-y;
|
|
43748
|
+
}
|
|
43749
|
+
&[data-travel-by-drag="y"] {
|
|
43750
|
+
touch-action: pan-x;
|
|
43751
|
+
}
|
|
43752
|
+
&[data-travel-by-drag="xy"] {
|
|
43753
|
+
touch-action: none;
|
|
43754
|
+
}
|
|
43755
|
+
/* A drag is not a selection: without this a mouse pulling a slide paints
|
|
43756
|
+
the text it passes over blue. */
|
|
43757
|
+
&[data-slide-dragging] {
|
|
43758
|
+
user-select: none;
|
|
43759
|
+
}
|
|
43760
|
+
|
|
43612
43761
|
/* Outside the box, which is where an outline is drawn by default: nothing
|
|
43613
43762
|
inside can paint over it (the slides are all within), and this box's own
|
|
43614
43763
|
overflow does not clip it either — an element's outline is not its own
|
|
@@ -43691,31 +43840,35 @@ const css$A = /* css */`
|
|
|
43691
43840
|
// own box, because that is what its percentages resolve to.
|
|
43692
43841
|
const ratioOfOneTravel = (track, from, to, targetBefore) => {
|
|
43693
43842
|
const box = track.getBoundingClientRect();
|
|
43694
|
-
const readOffset = offset => {
|
|
43695
|
-
if (!offset || offset === "none") {
|
|
43696
|
-
return {
|
|
43697
|
-
x: 0,
|
|
43698
|
-
y: 0
|
|
43699
|
-
};
|
|
43700
|
-
}
|
|
43701
|
-
const [x = "0", y = "0"] = String(offset).trim().split(/\s+/);
|
|
43702
|
-
const toPx = (value, size) => value.endsWith("%") ? parseFloat(value) / 100 * size : parseFloat(value) || 0;
|
|
43703
|
-
return {
|
|
43704
|
-
x: toPx(x, box.width),
|
|
43705
|
-
y: toPx(y, box.height)
|
|
43706
|
-
};
|
|
43707
|
-
};
|
|
43708
43843
|
const distance = (a, b) => Math.hypot(b.x - a.x, b.y - a.y);
|
|
43709
|
-
const target =
|
|
43710
|
-
const asked = distance(
|
|
43844
|
+
const target = offsetToPx(to, box);
|
|
43845
|
+
const asked = distance(offsetToPx(targetBefore, box), target);
|
|
43711
43846
|
if (!asked) {
|
|
43712
43847
|
return 1;
|
|
43713
43848
|
}
|
|
43714
|
-
const left = distance(
|
|
43849
|
+
const left = distance(offsetToPx(from, box), target);
|
|
43715
43850
|
const ratio = left / asked;
|
|
43716
43851
|
return ratio > 1 ? 1 : ratio;
|
|
43717
43852
|
};
|
|
43718
43853
|
|
|
43854
|
+
// A translate ("-100% 0%", "-260px 0px", "none") as two numbers of pixels.
|
|
43855
|
+
// Percentages are the size of the box, which is what a translate resolves them
|
|
43856
|
+
// against — so an offset written either way can be measured against another.
|
|
43857
|
+
const offsetToPx = (offset, box) => {
|
|
43858
|
+
if (!offset || offset === "none") {
|
|
43859
|
+
return {
|
|
43860
|
+
x: 0,
|
|
43861
|
+
y: 0
|
|
43862
|
+
};
|
|
43863
|
+
}
|
|
43864
|
+
const [x = "0", y = "0"] = String(offset).trim().split(/\s+/);
|
|
43865
|
+
const toPx = (value, size) => value.endsWith("%") ? parseFloat(value) / 100 * size : parseFloat(value) || 0;
|
|
43866
|
+
return {
|
|
43867
|
+
x: toPx(x, box.width),
|
|
43868
|
+
y: toPx(y, box.height)
|
|
43869
|
+
};
|
|
43870
|
+
};
|
|
43871
|
+
|
|
43719
43872
|
// A press landing while the track is already travelling: what is playing is
|
|
43720
43873
|
// sent home in a fifth of the time it has left, and the press it could not take
|
|
43721
43874
|
// yet is taken as soon as it lands. A press has to be FELT — nudging the pace
|
|
@@ -43735,6 +43888,87 @@ const hurryTravel = animation => {
|
|
|
43735
43888
|
animation.playbackRate = rate > HURRY_RATE_MAX ? HURRY_RATE_MAX : rate;
|
|
43736
43889
|
};
|
|
43737
43890
|
|
|
43891
|
+
// How far a pointer goes before it is a travel rather than a click: below this
|
|
43892
|
+
// a press that wandered a pixel is still a press, and the slides do not budge.
|
|
43893
|
+
const DRAG_START_THRESHOLD = 10;
|
|
43894
|
+
// How much of a box has to be pulled for letting go to carry on rather than put
|
|
43895
|
+
// the slide back. Under half, because a gesture that has clearly begun is an
|
|
43896
|
+
// intention: asking for the slide to be dragged all the way across turns a
|
|
43897
|
+
// travel into work.
|
|
43898
|
+
const DRAG_COMMIT_RATIO = 0.3;
|
|
43899
|
+
// A flick travels whatever the distance: the hand said "away" quickly, which is
|
|
43900
|
+
// the whole gesture — px/ms of pointer, and a few pixels to tell it from a tap
|
|
43901
|
+
// that shook.
|
|
43902
|
+
const DRAG_FLICK_VELOCITY = 0.4;
|
|
43903
|
+
const DRAG_FLICK_DISTANCE = 8;
|
|
43904
|
+
// Pulling towards nothing: the track follows at a fraction of the finger, so
|
|
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
|
+
}
|
|
43937
|
+
}
|
|
43938
|
+
element = element.parentElement;
|
|
43939
|
+
}
|
|
43940
|
+
return false;
|
|
43941
|
+
};
|
|
43942
|
+
|
|
43943
|
+
// Which axes the map has anything on, read from the layout alone: it is what
|
|
43944
|
+
// says which way a touch may travel, and a touch is answered before any of the
|
|
43945
|
+
// DOM below has been looked at.
|
|
43946
|
+
const dragAxesOf = layout => {
|
|
43947
|
+
if (typeof layout === "string") {
|
|
43948
|
+
return layout === "column" ? "y" : "x";
|
|
43949
|
+
}
|
|
43950
|
+
const {
|
|
43951
|
+
placeOf
|
|
43952
|
+
} = parseAreas(layout);
|
|
43953
|
+
let hasX = false;
|
|
43954
|
+
let hasY = false;
|
|
43955
|
+
for (const {
|
|
43956
|
+
x,
|
|
43957
|
+
y
|
|
43958
|
+
} of placeOf.values()) {
|
|
43959
|
+
if (x > 0) {
|
|
43960
|
+
hasX = true;
|
|
43961
|
+
}
|
|
43962
|
+
if (y > 0) {
|
|
43963
|
+
hasY = true;
|
|
43964
|
+
}
|
|
43965
|
+
}
|
|
43966
|
+
if (hasX && hasY) {
|
|
43967
|
+
return "xy";
|
|
43968
|
+
}
|
|
43969
|
+
return hasY ? "y" : "x";
|
|
43970
|
+
};
|
|
43971
|
+
|
|
43738
43972
|
// The ways out of a slide: whatever carries a travel command, the built-in
|
|
43739
43973
|
// chevrons (SlideNavButton) and anything a caller wired by hand alike. They are
|
|
43740
43974
|
// the container's chrome, not its content — see rememberFocus.
|
|
@@ -43858,12 +44092,17 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
|
|
|
43858
44092
|
* one that just travelled there. Called once the travel is over, and in the
|
|
43859
44093
|
* same render as the return to rest — anything later shows the old content
|
|
43860
44094
|
* for a frame.
|
|
43861
|
-
* @param {boolean} [props.
|
|
44095
|
+
* @param {boolean} [props.travelByKeyboard=true] - whether the arrows (and
|
|
43862
44096
|
* Home/End) walk the map. On by default: a map one can see is a map one
|
|
43863
44097
|
* expects to walk. Off when the arrows mean something else where these slides
|
|
43864
44098
|
* are — a list of choices one moves through, a picker whose screens are
|
|
43865
44099
|
* steps rather than places — so the keys keep the meaning the content gives
|
|
43866
44100
|
* them, and travelling stays something one asks for (a button, a command).
|
|
44101
|
+
* @param {boolean} [props.travelByDrag=true] - whether a pointer dragging the
|
|
44102
|
+
* slides travels. On by default: slides side by side are something one
|
|
44103
|
+
* expects to push around with a thumb. Off where the gesture belongs to the
|
|
44104
|
+
* content (a canvas one draws on, a map one pans), or where the slides are
|
|
44105
|
+
* steps of a form rather than a row one browses.
|
|
43867
44106
|
* @param {string} [props.duration="300ms"] - how long a slide change takes.
|
|
43868
44107
|
*/
|
|
43869
44108
|
const SlideContainer = ({
|
|
@@ -43873,7 +44112,8 @@ const SlideContainer = ({
|
|
|
43873
44112
|
onCurrentChange,
|
|
43874
44113
|
loop,
|
|
43875
44114
|
onLoop,
|
|
43876
|
-
|
|
44115
|
+
travelByKeyboard = true,
|
|
44116
|
+
travelByDrag = true,
|
|
43877
44117
|
duration = "300ms",
|
|
43878
44118
|
children,
|
|
43879
44119
|
...rest
|
|
@@ -43933,8 +44173,16 @@ const SlideContainer = ({
|
|
|
43933
44173
|
// A focus transfer read off the interaction that asked for the travel, kept
|
|
43934
44174
|
// until the slide it is meant for holds its final DOM (see handOverFocus).
|
|
43935
44175
|
const focusHandOverRef = useRef(null);
|
|
44176
|
+
// The gesture in hand, from the pointer that started it to the slides it
|
|
44177
|
+
// brought alongside. Null when no finger is on the box.
|
|
44178
|
+
const dragRef = useRef(null);
|
|
44179
|
+
// Where the travel about to be drawn departs from, when that is not where the
|
|
44180
|
+
// track rests: a slide let go of halfway carries on from under the finger.
|
|
44181
|
+
// Read and dropped by the layout effect, which is the one drawing it.
|
|
44182
|
+
const travelFromRef = useRef(null);
|
|
43936
44183
|
const current = rollingArea ?? currentProp ?? currentAreaState;
|
|
43937
44184
|
const vertical = layout === "column";
|
|
44185
|
+
const dragAxes = useMemo(() => travelByDrag ? dragAxesOf(layout) : null, [travelByDrag, layout]);
|
|
43938
44186
|
// Which required slides have been answered (see Slide's own `required`). Held
|
|
43939
44187
|
// here rather than in each slide because answering one says something about
|
|
43940
44188
|
// the others: the steps after it were answered about a state that has just
|
|
@@ -44191,7 +44439,15 @@ const SlideContainer = ({
|
|
|
44191
44439
|
// fetch, and having it in hand is also what allows the pace below.
|
|
44192
44440
|
const travelInFlight = trackAnimationRef.current?.playState === "running";
|
|
44193
44441
|
const offsetOnScreen = travelInFlight ? getComputedStyle(track).translate : undefined;
|
|
44194
|
-
|
|
44442
|
+
// …and where a slide let go of halfway was left, which is the same fact
|
|
44443
|
+
// said by the gesture that put it there: the track is at rest as far as any
|
|
44444
|
+
// animation is concerned, so nothing else could tell.
|
|
44445
|
+
const offsetDragged = travelFromRef.current;
|
|
44446
|
+
travelFromRef.current = null;
|
|
44447
|
+
const offsetBefore = offsetDragged ?? offsetOnScreen ?? offsetRef.current;
|
|
44448
|
+
// The travel that was ASKED for is still one box, whatever is left of it to
|
|
44449
|
+
// cover: a slide dragged most of the way there finishes in what is left of
|
|
44450
|
+
// the duration rather than taking a full one over a few pixels.
|
|
44195
44451
|
const offsetTargetBefore = offsetRef.current;
|
|
44196
44452
|
offsetRef.current = offset;
|
|
44197
44453
|
// Where the track ends up, always — the animation below only covers the way
|
|
@@ -44206,9 +44462,10 @@ const SlideContainer = ({
|
|
|
44206
44462
|
// this only ever shortens it (a longer travel is not made slower, which
|
|
44207
44463
|
// would make a two-box move drag).
|
|
44208
44464
|
const travelRatio = ratioOfOneTravel(track, offsetBefore, offset, offsetTargetBefore);
|
|
44209
|
-
// Already moving
|
|
44210
|
-
//
|
|
44211
|
-
|
|
44465
|
+
// Already moving — under an animation or under a finger that has just
|
|
44466
|
+
// let go — so there is no ease-in to play: it would stall the track for
|
|
44467
|
+
// an instant right where the eye is following it.
|
|
44468
|
+
const easing = travelInFlight || offsetDragged ? "ease-out" : "ease";
|
|
44212
44469
|
// Cancelled rather than layered: two animations on the same property
|
|
44213
44470
|
// would blend, and what one sees then is neither of the two moves.
|
|
44214
44471
|
trackAnimationRef.current?.cancel();
|
|
@@ -44233,11 +44490,12 @@ const SlideContainer = ({
|
|
|
44233
44490
|
trackAnimationRef.current.finished.then(settleTravel, () => {
|
|
44234
44491
|
// cancelled by the next travel — that one carries the stage on
|
|
44235
44492
|
});
|
|
44236
|
-
} else if (stage && trackAnimationRef.current?.playState !== "running") {
|
|
44493
|
+
} else if (stage && !dragRef.current?.axis && trackAnimationRef.current?.playState !== "running") {
|
|
44237
44494
|
// Staged with nothing left to play: a travel that was drawn and then had
|
|
44238
44495
|
// its animation taken away (a duration set to 0, a re-render landing
|
|
44239
44496
|
// between the two). Struck at once rather than left standing, since the
|
|
44240
|
-
// thing it was standing for is over.
|
|
44497
|
+
// thing it was standing for is over. A gesture in hand is a stage that is
|
|
44498
|
+
// standing for something — the finger still holding it.
|
|
44241
44499
|
settleTravel();
|
|
44242
44500
|
}
|
|
44243
44501
|
// A window waiting for its travel to be over (see goToArea's own loop
|
|
@@ -44282,6 +44540,9 @@ const SlideContainer = ({
|
|
|
44282
44540
|
if (!stageRef.current) {
|
|
44283
44541
|
drawnAreaRef.current = currentArea;
|
|
44284
44542
|
}
|
|
44543
|
+
// The finger has the last word: everything above drew the map at rest, and
|
|
44544
|
+
// where the track actually is right now is where the gesture put it.
|
|
44545
|
+
paintDrag();
|
|
44285
44546
|
});
|
|
44286
44547
|
|
|
44287
44548
|
/**
|
|
@@ -44567,6 +44828,363 @@ const SlideContainer = ({
|
|
|
44567
44828
|
const moveNext = event => vertical ? move(0, 1, event) || move(1, 0, event) : move(1, 0, event) || move(0, 1, event);
|
|
44568
44829
|
const movePrevious = event => vertical ? move(0, -1, event) || move(-1, 0, event) : move(-1, 0, event) || move(0, -1, event);
|
|
44569
44830
|
|
|
44831
|
+
// Where the track is right now, as the gesture left it: the resting place of
|
|
44832
|
+
// the slide being dragged, plus what the pointer has pulled since.
|
|
44833
|
+
const paintDrag = () => {
|
|
44834
|
+
const drag = dragRef.current;
|
|
44835
|
+
const track = trackRef.current;
|
|
44836
|
+
// Nothing to paint for a pointer that is only resting on the box: until it
|
|
44837
|
+
// has an axis a gesture has moved nothing and knows no geometry.
|
|
44838
|
+
if (!drag || !drag.axis || !track) {
|
|
44839
|
+
return;
|
|
44840
|
+
}
|
|
44841
|
+
const x = drag.baseOffset.x + drag.pull.x;
|
|
44842
|
+
const y = drag.baseOffset.y + drag.pull.y;
|
|
44843
|
+
drag.offset = `${x}px ${y}px`;
|
|
44844
|
+
track.style.setProperty("--slide-container-offset", drag.offset);
|
|
44845
|
+
};
|
|
44846
|
+
|
|
44847
|
+
// The two slides the gesture can bring in, placed one box either side of the
|
|
44848
|
+
// one being dragged — the same stage a travel builds, except that both ends
|
|
44849
|
+
// are set up at once because the finger has not said yet which way it goes.
|
|
44850
|
+
const stageDrag = drag => {
|
|
44851
|
+
const {
|
|
44852
|
+
slideElements,
|
|
44853
|
+
placeOf
|
|
44854
|
+
} = readMap();
|
|
44855
|
+
const step = drag.axis === "x" ? {
|
|
44856
|
+
x: 1,
|
|
44857
|
+
y: 0
|
|
44858
|
+
} : {
|
|
44859
|
+
x: 0,
|
|
44860
|
+
y: 1
|
|
44861
|
+
};
|
|
44862
|
+
const placeByArea = new Map();
|
|
44863
|
+
placeByArea.set(drag.area, drag.basePlace);
|
|
44864
|
+
if (drag.areaBack) {
|
|
44865
|
+
placeByArea.set(drag.areaBack, {
|
|
44866
|
+
x: drag.basePlace.x - step.x,
|
|
44867
|
+
y: drag.basePlace.y - step.y
|
|
44868
|
+
});
|
|
44869
|
+
}
|
|
44870
|
+
if (drag.areaOn) {
|
|
44871
|
+
placeByArea.set(drag.areaOn, {
|
|
44872
|
+
x: drag.basePlace.x + step.x,
|
|
44873
|
+
y: drag.basePlace.y + step.y
|
|
44874
|
+
});
|
|
44875
|
+
}
|
|
44876
|
+
stageRef.current = {
|
|
44877
|
+
placeByArea,
|
|
44878
|
+
area: drag.area
|
|
44879
|
+
};
|
|
44880
|
+
for (const slideElement of slideElements) {
|
|
44881
|
+
const area = readArea(slideElement);
|
|
44882
|
+
const {
|
|
44883
|
+
x,
|
|
44884
|
+
y
|
|
44885
|
+
} = placeByArea.get(area) || placeOf.get(area) || {
|
|
44886
|
+
x: 0,
|
|
44887
|
+
y: 0
|
|
44888
|
+
};
|
|
44889
|
+
slideElement.style.setProperty("--slide-offset", `${x * 100}% ${y * 100}%`);
|
|
44890
|
+
slideElement.toggleAttribute("data-slide-offstage", !placeByArea.has(area));
|
|
44891
|
+
}
|
|
44892
|
+
};
|
|
44893
|
+
|
|
44894
|
+
// Let go of without enough of a gesture to travel: the slide comes back to
|
|
44895
|
+
// where it was, over the distance it was pulled — so a slide barely moved
|
|
44896
|
+
// snaps back and one dragged most of the way there takes its time.
|
|
44897
|
+
const returnToRest = drag => {
|
|
44898
|
+
const track = trackRef.current;
|
|
44899
|
+
if (!track) {
|
|
44900
|
+
return;
|
|
44901
|
+
}
|
|
44902
|
+
const restOffset = `${drag.baseOffset.x}px ${drag.baseOffset.y}px`;
|
|
44903
|
+
const durationMs = durationToMs(duration);
|
|
44904
|
+
const pulled = Math.abs(drag.pull[drag.axis]);
|
|
44905
|
+
const size = drag.axis === "x" ? drag.box.width : drag.box.height;
|
|
44906
|
+
trackAnimationRef.current?.cancel();
|
|
44907
|
+
trackAnimationRef.current = null;
|
|
44908
|
+
track.style.setProperty("--slide-container-offset", restOffset);
|
|
44909
|
+
if (!durationMs || !pulled) {
|
|
44910
|
+
settleTravel();
|
|
44911
|
+
return;
|
|
44912
|
+
}
|
|
44913
|
+
const animation = track.animate([{
|
|
44914
|
+
translate: drag.offset
|
|
44915
|
+
}, {
|
|
44916
|
+
translate: restOffset
|
|
44917
|
+
}], {
|
|
44918
|
+
duration: durationMs * (pulled / size),
|
|
44919
|
+
easing: "ease-out"
|
|
44920
|
+
});
|
|
44921
|
+
trackAnimationRef.current = animation;
|
|
44922
|
+
animation.finished.then(settleTravel, () => {
|
|
44923
|
+
// cancelled by a travel asked for since — that one carries the stage on
|
|
44924
|
+
});
|
|
44925
|
+
};
|
|
44926
|
+
const onDragMove = pointerMoveEvent => {
|
|
44927
|
+
const drag = dragRef.current;
|
|
44928
|
+
if (!drag || pointerMoveEvent.pointerId !== drag.pointerId) {
|
|
44929
|
+
return;
|
|
44930
|
+
}
|
|
44931
|
+
if (!drag.axis) {
|
|
44932
|
+
const reachX = Math.abs(pointerMoveEvent.clientX - drag.startX);
|
|
44933
|
+
const reachY = Math.abs(pointerMoveEvent.clientY - drag.startY);
|
|
44934
|
+
if (reachX < DRAG_START_THRESHOLD && reachY < DRAG_START_THRESHOLD) {
|
|
44935
|
+
return;
|
|
44936
|
+
}
|
|
44937
|
+
// ONE axis, decided by the first few pixels and never revisited: a
|
|
44938
|
+
// diagonal gesture would ask for two travels at once and only one slide
|
|
44939
|
+
// can arrive — so the finger picks the axis it leans on, and the slides
|
|
44940
|
+
// walk that one alone.
|
|
44941
|
+
const axis = reachX >= reachY ? "x" : "y";
|
|
44942
|
+
const sign = axis === "x" ? Math.sign(pointerMoveEvent.clientX - drag.startX) : Math.sign(pointerMoveEvent.clientY - drag.startY);
|
|
44943
|
+
const areaBack = axis === "x" ? areaTowards(-1, 0) : areaTowards(0, -1);
|
|
44944
|
+
const areaOn = axis === "x" ? areaTowards(1, 0) : areaTowards(0, 1);
|
|
44945
|
+
// Everything positional is read HERE rather than when the pointer landed:
|
|
44946
|
+
// the travel that was playing then may have arrived since, and it is what
|
|
44947
|
+
// the slides are doing at the moment the gesture takes them over that the
|
|
44948
|
+
// gesture must carry on from.
|
|
44949
|
+
const track = trackRef.current;
|
|
44950
|
+
const {
|
|
44951
|
+
slideElements,
|
|
44952
|
+
placeOf
|
|
44953
|
+
} = readMap();
|
|
44954
|
+
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
|
|
44955
|
+
const box = track.getBoundingClientRect();
|
|
44956
|
+
if (!areaBack && !areaOn || !currentElement || !box.width || !box.height || scrollRoomTowards(drag.target, currentElement, axis, sign)) {
|
|
44957
|
+
// Nothing that way, or something else with a better claim on the
|
|
44958
|
+
// gesture: given up rather than half-taken, so whatever else wants it
|
|
44959
|
+
// (a scroller, the page) gets it whole.
|
|
44960
|
+
drag.stop();
|
|
44961
|
+
dragRef.current = null;
|
|
44962
|
+
return;
|
|
44963
|
+
}
|
|
44964
|
+
const area = readArea(currentElement);
|
|
44965
|
+
// Where the slide being dragged stands: where the stage put it while a
|
|
44966
|
+
// travel is playing, its place on the map otherwise.
|
|
44967
|
+
const stage = stageRef.current;
|
|
44968
|
+
const basePlace = stage?.placeByArea.get(area) || placeOf.get(area) || {
|
|
44969
|
+
x: 0,
|
|
44970
|
+
y: 0
|
|
44971
|
+
};
|
|
44972
|
+
const baseOffset = {
|
|
44973
|
+
x: -basePlace.x * box.width,
|
|
44974
|
+
y: -basePlace.y * box.height
|
|
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();
|
|
45066
|
+
};
|
|
45067
|
+
document.addEventListener("click", swallowClick, {
|
|
45068
|
+
capture: true
|
|
45069
|
+
});
|
|
45070
|
+
setTimeout(() => {
|
|
45071
|
+
document.removeEventListener("click", swallowClick, {
|
|
45072
|
+
capture: true
|
|
45073
|
+
});
|
|
45074
|
+
});
|
|
45075
|
+
const {
|
|
45076
|
+
axis
|
|
45077
|
+
} = drag;
|
|
45078
|
+
const size = axis === "x" ? drag.box.width : drag.box.height;
|
|
45079
|
+
const pulled = drag.pull[axis];
|
|
45080
|
+
const sign = pulled > 0 ? 1 : -1;
|
|
45081
|
+
const areaPulled = pulled > 0 ? drag.areaBack : drag.areaOn;
|
|
45082
|
+
// A hand that stopped before letting go has said "here", whatever it was
|
|
45083
|
+
// doing a moment earlier — so the speed only counts while it is still going.
|
|
45084
|
+
const velocity = pointerEvent.timeStamp - drag.lastTime > 100 ? 0 : drag.velocity;
|
|
45085
|
+
const flicked = Math.abs(velocity) > DRAG_FLICK_VELOCITY && Math.sign(velocity) === sign && Math.abs(pulled) > DRAG_FLICK_DISTANCE;
|
|
45086
|
+
const travels =
|
|
45087
|
+
// A gesture taken away rather than let go of (the browser scrolling
|
|
45088
|
+
// something else, a call coming in) said nothing: the slide goes back.
|
|
45089
|
+
pointerEvent.type !== "pointercancel" && areaPulled && (flicked || Math.abs(pulled) > size * DRAG_COMMIT_RATIO);
|
|
45090
|
+
if (!travels) {
|
|
45091
|
+
returnToRest(drag);
|
|
45092
|
+
return;
|
|
45093
|
+
}
|
|
45094
|
+
// Where the slide is being left, for the travel to depart from instead of
|
|
45095
|
+
// from the map.
|
|
45096
|
+
travelFromRef.current = drag.offset;
|
|
45097
|
+
const moved = axis === "x" ? move(-sign, 0, pointerEvent) : move(0, -sign, pointerEvent);
|
|
45098
|
+
if (!moved) {
|
|
45099
|
+
// Nowhere to go after all — a slide holding on to the user (preventNav).
|
|
45100
|
+
travelFromRef.current = null;
|
|
45101
|
+
returnToRest(drag);
|
|
45102
|
+
return;
|
|
45103
|
+
}
|
|
45104
|
+
// A container whose `current` is held outside and was not moved: nothing
|
|
45105
|
+
// rendered, so nothing drew the travel and the track is still under where
|
|
45106
|
+
// the finger left it. One frame is all it takes to know.
|
|
45107
|
+
requestAnimationFrame(() => {
|
|
45108
|
+
if (travelFromRef.current) {
|
|
45109
|
+
travelFromRef.current = null;
|
|
45110
|
+
returnToRest(drag);
|
|
45111
|
+
}
|
|
45112
|
+
});
|
|
45113
|
+
};
|
|
45114
|
+
const startDrag = pointerDownEvent => {
|
|
45115
|
+
if (!travelByDrag || dragRef.current || pointerDownEvent.button !== 0) {
|
|
45116
|
+
return;
|
|
45117
|
+
}
|
|
45118
|
+
// A window mid-roll has nothing to drag yet: it is on its way somewhere and
|
|
45119
|
+
// the content that goes with it has not moved (see goToArea).
|
|
45120
|
+
if (rollingRef.current) {
|
|
45121
|
+
return;
|
|
45122
|
+
}
|
|
45123
|
+
const target = pointerDownEvent.target;
|
|
45124
|
+
if (!target.closest || target.closest(DRAG_EXCLUDED_SELECTOR)) {
|
|
45125
|
+
return;
|
|
45126
|
+
}
|
|
45127
|
+
const containerEl = containerRef.current;
|
|
45128
|
+
const onMove = pointerMoveEvent => {
|
|
45129
|
+
onDragMove(pointerMoveEvent);
|
|
45130
|
+
};
|
|
45131
|
+
const onEnd = pointerEvent => {
|
|
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);
|
|
45178
|
+
};
|
|
45179
|
+
|
|
45180
|
+
// A gesture is listening on things that outlive this component.
|
|
45181
|
+
useLayoutEffect(() => {
|
|
45182
|
+
return () => {
|
|
45183
|
+
dragRef.current?.stop();
|
|
45184
|
+
dragRef.current = null;
|
|
45185
|
+
};
|
|
45186
|
+
}, []);
|
|
45187
|
+
|
|
44570
45188
|
// Arrows walk the map, Home/End jump to its ends — but only where those keys
|
|
44571
45189
|
// mean nothing else: applyKeyboardShortcuts refuses to intercept a key the
|
|
44572
45190
|
// focused element has a native use for, so an arrow inside a text field still
|
|
@@ -44593,27 +45211,27 @@ const SlideContainer = ({
|
|
|
44593
45211
|
// `enabled` leaves the key to whatever else wants it (see
|
|
44594
45212
|
// keyboard_shortcuts.js), rather than swallowing it here.
|
|
44595
45213
|
arrowright: {
|
|
44596
|
-
enabled:
|
|
45214
|
+
enabled: travelByKeyboard,
|
|
44597
45215
|
handler: e => travelled(move(1, 0, e))
|
|
44598
45216
|
},
|
|
44599
45217
|
arrowleft: {
|
|
44600
|
-
enabled:
|
|
45218
|
+
enabled: travelByKeyboard,
|
|
44601
45219
|
handler: e => travelled(move(-1, 0, e))
|
|
44602
45220
|
},
|
|
44603
45221
|
arrowdown: {
|
|
44604
|
-
enabled:
|
|
45222
|
+
enabled: travelByKeyboard,
|
|
44605
45223
|
handler: e => travelled(move(0, 1, e))
|
|
44606
45224
|
},
|
|
44607
45225
|
arrowup: {
|
|
44608
|
-
enabled:
|
|
45226
|
+
enabled: travelByKeyboard,
|
|
44609
45227
|
handler: e => travelled(move(0, -1, e))
|
|
44610
45228
|
},
|
|
44611
45229
|
home: {
|
|
44612
|
-
enabled:
|
|
45230
|
+
enabled: travelByKeyboard,
|
|
44613
45231
|
handler: e => travelled(goToEnd(false, e))
|
|
44614
45232
|
},
|
|
44615
45233
|
end: {
|
|
44616
|
-
enabled:
|
|
45234
|
+
enabled: travelByKeyboard,
|
|
44617
45235
|
handler: e => travelled(goToEnd(true, e))
|
|
44618
45236
|
}
|
|
44619
45237
|
});
|
|
@@ -44626,6 +45244,16 @@ const SlideContainer = ({
|
|
|
44626
45244
|
ref: containerRef,
|
|
44627
45245
|
baseClassName: "navi_slide_container",
|
|
44628
45246
|
"data-slide-container": ""
|
|
45247
|
+
// Which axes a touch may travel on, said in the DOM: what the browser
|
|
45248
|
+
// does with a finger is decided by CSS (touch-action) before any of this
|
|
45249
|
+
// has seen the gesture.
|
|
45250
|
+
,
|
|
45251
|
+
|
|
45252
|
+
"data-travel-by-drag": dragAxes ?? undefined,
|
|
45253
|
+
onPointerDown: e => {
|
|
45254
|
+
startDrag(e);
|
|
45255
|
+
rest.onPointerDown?.(e);
|
|
45256
|
+
}
|
|
44629
45257
|
// The focusable one, and a Tab stop: a slide is not (see Slide), so the
|
|
44630
45258
|
// keyboard lands on what the current slide holds, and on this box when it
|
|
44631
45259
|
// holds nothing. It is also what makes the arrows and Home/End reachable
|
|
@@ -51063,6 +51691,12 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
51063
51691
|
* carries a `signal`, aborted when the list stops wanting those rows (the
|
|
51064
51692
|
* window has moved on) — pass it to fetch to call the request off.
|
|
51065
51693
|
*
|
|
51694
|
+
* A resource answers through its page reader:
|
|
51695
|
+
* `itemsAction={GAME.GET_PAGE.bindParams({ radar })}` — the rows are upserted
|
|
51696
|
+
* into the store on their way in, so the list draws store items rather than
|
|
51697
|
+
* copies of the JSON. The list holds the pages, the store holds the objects
|
|
51698
|
+
* (see docs/resource.md).
|
|
51699
|
+
*
|
|
51066
51700
|
* A collection held in memory answers synchronously: `itemsAction={() => rows}`.
|
|
51067
51701
|
* What it gives back is kept, so a collection that changes as a whole (a search
|
|
51068
51702
|
* reordering it) is a different collection: give the run a `key` that changes
|
|
@@ -51594,6 +52228,9 @@ const useItemStore = ({
|
|
|
51594
52228
|
};
|
|
51595
52229
|
let result;
|
|
51596
52230
|
try {
|
|
52231
|
+
if (typeof itemsAction !== "function") {
|
|
52232
|
+
throw new TypeError(`itemsAction must be a function, received ${itemsAction}. A resource feeds a list through its page reader: itemsAction={RESOURCE.GET_PAGE.bindParams(...)} — its other actions keep one response and cannot answer a range.`);
|
|
52233
|
+
}
|
|
51597
52234
|
result = itemsAction(range);
|
|
51598
52235
|
} catch (e) {
|
|
51599
52236
|
failed(e);
|