@jsenv/navi 0.29.151 → 0.29.153
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 +107 -10
- package/dist/jsenv_navi.js.map +7 -5
- package/docs/AI_INSTRUCTIONS.md +11 -0
- package/docs/error_handling.md +52 -1
- package/docs/popup_open.md +39 -8
- package/package.json +2 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -15,6 +15,45 @@ import { parseDuration, durationContainsNaN, compareTwoDurations, durationToSeco
|
|
|
15
15
|
export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
|
|
16
16
|
import { Suspense, createPortal, forwardRef } from "preact/compat";
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* One page, one navi.
|
|
20
|
+
*
|
|
21
|
+
* navi keeps contexts, registries and singletons at module scope, so a second
|
|
22
|
+
* copy of the library in the same page is a second of each, and nothing
|
|
23
|
+
* crosses between them. Every symptom of that lands far from the cause and
|
|
24
|
+
* reads as an app bug: a <Loading> mounted by one copy is invisible to the
|
|
25
|
+
* hook reading the other's context and reports itself missing, an action
|
|
26
|
+
* registered in one is unknown to the other, focus and z-index each get a
|
|
27
|
+
* second owner. So the copies are detected here, when the second one is
|
|
28
|
+
* imported, and that import fails — the first thing that goes wrong says what
|
|
29
|
+
* is wrong.
|
|
30
|
+
*
|
|
31
|
+
* A copy is a module evaluation, which is what the object added to the set
|
|
32
|
+
* stands for; the urls are only there to tell the reader which two files to
|
|
33
|
+
* compare. Two urls for one file on disk is the usual shape (a dev server
|
|
34
|
+
* stamping a package version into specifiers, a bundle embedding navi next to
|
|
35
|
+
* a copy resolved separately); two versions installed in node_modules is the
|
|
36
|
+
* other.
|
|
37
|
+
*
|
|
38
|
+
* This module must stay in the entry chunk: the whole point is that it is
|
|
39
|
+
* evaluated once per copy of navi, and a shared chunk would be evaluated once
|
|
40
|
+
* for both.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
// Symbol.for so the set is the same one across copies — a copy cannot see the
|
|
44
|
+
// other's module scope, which is the very thing being detected.
|
|
45
|
+
const NAVI_INSTANCE_SET = Symbol.for("navi_instance_set");
|
|
46
|
+
|
|
47
|
+
const instanceSet = globalThis[NAVI_INSTANCE_SET] || new Set();
|
|
48
|
+
globalThis[NAVI_INSTANCE_SET] = instanceSet;
|
|
49
|
+
instanceSet.add({ url: import.meta.url });
|
|
50
|
+
if (instanceSet.size > 1) {
|
|
51
|
+
const urlList = Array.from(instanceSet, ({ url }) => `- ${url}`).join("\n");
|
|
52
|
+
throw new Error(`@jsenv/navi is loaded ${instanceSet.size} times in this page:
|
|
53
|
+
${urlList}
|
|
54
|
+
Each one builds its own contexts, stores and registries, so what one holds is invisible to the components using another.`);
|
|
55
|
+
}
|
|
56
|
+
|
|
18
57
|
installImportMetaCssBuild(import.meta);/**
|
|
19
58
|
* Every z-index navi can be seen competing on, in one place, ordered.
|
|
20
59
|
*
|
|
@@ -5315,6 +5354,26 @@ const extractMessageAndRemainingProps = (props) => {
|
|
|
5315
5354
|
return [ownMessages, remaining];
|
|
5316
5355
|
};
|
|
5317
5356
|
|
|
5357
|
+
/**
|
|
5358
|
+
* The one-line form of a constraint message, or `undefined` when it has none.
|
|
5359
|
+
*
|
|
5360
|
+
* A message is not always a sentence: it can be an `Error`, or a whole element
|
|
5361
|
+
* (an `errorMapping` returning JSX to put a link inside the callout). Those
|
|
5362
|
+
* still render fine in the callout, but they have no one-line form, and
|
|
5363
|
+
* anything that needs one — a `title` attribute, a caller drawing its own
|
|
5364
|
+
* summary — must be given nothing rather than `String(message)`, which writes
|
|
5365
|
+
* "[object Object]" on the screen.
|
|
5366
|
+
*/
|
|
5367
|
+
const getMessageString = (message) => {
|
|
5368
|
+
if (typeof message === "string") {
|
|
5369
|
+
return message;
|
|
5370
|
+
}
|
|
5371
|
+
if (Error.isError(message)) {
|
|
5372
|
+
return message.message;
|
|
5373
|
+
}
|
|
5374
|
+
return undefined;
|
|
5375
|
+
};
|
|
5376
|
+
|
|
5318
5377
|
const getConstraintMessage = (
|
|
5319
5378
|
controller,
|
|
5320
5379
|
constraint,
|
|
@@ -12108,7 +12167,9 @@ const createControlValidation = (
|
|
|
12108
12167
|
typeof checkResult === "string"
|
|
12109
12168
|
? { message: checkResult }
|
|
12110
12169
|
: checkResult;
|
|
12111
|
-
constraintValidityInfo.messageString =
|
|
12170
|
+
constraintValidityInfo.messageString = getMessageString(
|
|
12171
|
+
constraintValidityInfo.message,
|
|
12172
|
+
);
|
|
12112
12173
|
debugUIState(
|
|
12113
12174
|
`${elementSig} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
|
|
12114
12175
|
);
|
|
@@ -12150,10 +12211,15 @@ const createControlValidation = (
|
|
|
12150
12211
|
if (titleLess) {
|
|
12151
12212
|
const element = controller.ref.current;
|
|
12152
12213
|
if (element) {
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12214
|
+
const { messageString } = activeFailedConstraintInfo;
|
|
12215
|
+
if (messageString) {
|
|
12216
|
+
element.setAttribute("title", messageString);
|
|
12217
|
+
} else {
|
|
12218
|
+
// The message has no one-line form (an element, typically): the
|
|
12219
|
+
// callout shows it whole, and the tooltip stays empty rather than
|
|
12220
|
+
// saying "[object Object]".
|
|
12221
|
+
element.removeAttribute("title");
|
|
12222
|
+
}
|
|
12157
12223
|
}
|
|
12158
12224
|
}
|
|
12159
12225
|
} else {
|
|
@@ -12550,6 +12616,13 @@ const useExecuteAction = (
|
|
|
12550
12616
|
let message;
|
|
12551
12617
|
if (errorMapping) {
|
|
12552
12618
|
const errorMappingResult = errorMapping(error);
|
|
12619
|
+
// Anything the chain below does not recognize — nothing returned, above
|
|
12620
|
+
// all — leaves `message` undefined and the control displays nothing. That
|
|
12621
|
+
// is the contract, and both halves of it are useful:
|
|
12622
|
+
// - returning nothing says "don't show this one", per error, where
|
|
12623
|
+
// actionErrorEffect="none" says it once for the whole control;
|
|
12624
|
+
// - not handling an error means returning it untouched (`return error`),
|
|
12625
|
+
// and it is then shown exactly as if there were no mapping at all.
|
|
12553
12626
|
if (typeof errorMappingResult === "string") {
|
|
12554
12627
|
message = errorMappingResult;
|
|
12555
12628
|
} else if (Error.isError(errorMappingResult)) {
|
|
@@ -30169,16 +30242,40 @@ const anyMatchingRouteSignal = (routes) => {
|
|
|
30169
30242
|
};
|
|
30170
30243
|
|
|
30171
30244
|
/**
|
|
30245
|
+
* Runs a navi command from JS. It is the LAST RESORT: the attributes say the
|
|
30246
|
+
* same thing declaratively and go through this very function, so a press that
|
|
30247
|
+
* opens, closes, sends, updates or travels needs none of this.
|
|
30248
|
+
*
|
|
30249
|
+
* ```html
|
|
30250
|
+
* <button command="--navi-open" commandfor="note-dialog" value="42">
|
|
30251
|
+
* ```
|
|
30252
|
+
*
|
|
30253
|
+
* `command` names it (`--navi-x:argument` when it needs an argument),
|
|
30254
|
+
* `commandfor` / `navi-command-target` says to whom, the source's own `value`
|
|
30255
|
+
* says what it is about (`command-value` when the source has a value meaning
|
|
30256
|
+
* something else). Reach for `triggerNaviCommand` only when what decides is not
|
|
30257
|
+
* a press on the element carrying those attributes: a long press, the end of a
|
|
30258
|
+
* drag, a double-click, a keyboard shortcut, a server answer, an observer.
|
|
30259
|
+
*
|
|
30172
30260
|
* @param {Element} element The element asking — the command's source, and the
|
|
30173
30261
|
* anchor a popup opens on unless `anchor` says otherwise.
|
|
30174
30262
|
* @param {string} command
|
|
30175
30263
|
* @param {Event} event What caused this. Mandatory: it is what makes a command
|
|
30176
30264
|
* traceable back to its origin — the debug panel groups everything a gesture
|
|
30177
30265
|
* set off under it, and the gates below read it to know whether the default
|
|
30178
|
-
* was already prevented and which mouse button was pressed.
|
|
30179
|
-
*
|
|
30180
|
-
*
|
|
30181
|
-
*
|
|
30266
|
+
* was already prevented and which mouse button was pressed.
|
|
30267
|
+
*
|
|
30268
|
+
* **Forward the event you were handed**, down through every function between
|
|
30269
|
+
* the handler and this call, so what arrives here is the real gesture. Almost
|
|
30270
|
+
* every command has one: a person did something to the page, or the browser
|
|
30271
|
+
* did. Building a `CustomEvent` at the call site instead of taking `event` as
|
|
30272
|
+
* a parameter looks like it satisfies the requirement, and throws away
|
|
30273
|
+
* exactly what makes it useful — the chain a popup reads to give the focus
|
|
30274
|
+
* back, the button that was pressed, a default already prevented.
|
|
30275
|
+
*
|
|
30276
|
+
* The `CustomEvent` is for the genuinely originless case — a timer firing, an
|
|
30277
|
+
* action settling, a signal changing — and even there it names what happened
|
|
30278
|
+
* and chains to whatever preceded it:
|
|
30182
30279
|
* const expiredEvent = new CustomEvent("session_expired");
|
|
30183
30280
|
* chainEvent(expiredEvent, causeEvent); // when something did precede it
|
|
30184
30281
|
* triggerNaviCommand(dialogEl, "--navi-open", expiredEvent);
|
|
@@ -30200,7 +30297,7 @@ const triggerNaviCommand = (
|
|
|
30200
30297
|
) => {
|
|
30201
30298
|
if (!event) {
|
|
30202
30299
|
throw new Error(
|
|
30203
|
-
`"${command}" triggered without an event: it is mandatory, a command must say what caused it.
|
|
30300
|
+
`"${command}" triggered without an event: it is mandatory, a command must say what caused it. Forward the gesture that led here, or a CustomEvent naming the cause when no gesture did — see triggerNaviCommand's jsdoc.`,
|
|
30204
30301
|
);
|
|
30205
30302
|
}
|
|
30206
30303
|
const naviCommand =
|