@nxgt/material-core 0.9.1 → 0.11.0
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/CHANGELOG.md +98 -0
- package/README.md +180 -6
- package/dist/chunks/index-15vj9xbx.js +26 -0
- package/dist/chunks/index-15vj9xbx.js.map +10 -0
- package/dist/chunks/index-407b6zs0.js +41 -0
- package/dist/chunks/index-407b6zs0.js.map +10 -0
- package/dist/chunks/{index-h3vjydz7.js → index-r93kpbtg.js} +8 -23
- package/dist/chunks/index-r93kpbtg.js.map +10 -0
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/code-errors.d.ts +75 -0
- package/dist/lib/code-errors.d.ts.map +1 -0
- package/dist/lib/code-errors.js +37 -0
- package/dist/lib/code-errors.js.map +10 -0
- package/dist/lib/focus-control.js +7 -39
- package/dist/lib/focus-control.js.map +3 -4
- package/dist/lib/pdf-download.d.ts +1 -6
- package/dist/lib/pdf-download.d.ts.map +1 -1
- package/dist/lib/pdf-download.js +5 -3
- package/dist/lib/pdf-download.js.map +1 -1
- package/dist/lib/print-only.d.ts +29 -0
- package/dist/lib/print-only.d.ts.map +1 -0
- package/dist/lib/print-only.js +39 -0
- package/dist/lib/print-only.js.map +10 -0
- package/dist/lib/repeatable-group.d.ts +70 -0
- package/dist/lib/repeatable-group.d.ts.map +1 -0
- package/dist/lib/repeatable-group.js +59 -0
- package/dist/lib/repeatable-group.js.map +10 -0
- package/dist/lib/save-blob.d.ts +16 -0
- package/dist/lib/save-blob.d.ts.map +1 -0
- package/dist/lib/submit-guard.d.ts +72 -0
- package/dist/lib/submit-guard.d.ts.map +1 -0
- package/dist/lib/submit-guard.js +69 -0
- package/dist/lib/submit-guard.js.map +10 -0
- package/dist/lib/two-factor.d.ts +37 -0
- package/dist/lib/two-factor.d.ts.map +1 -0
- package/dist/lib/two-factor.js +38 -0
- package/dist/lib/two-factor.js.map +10 -0
- package/docs/roadmap.md +13 -0
- package/package.json +32 -2
- package/dist/chunks/index-h3vjydz7.js.map +0 -10
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/lib/code-errors.ts
|
|
2
|
+
function defaultCodeOf(issue) {
|
|
3
|
+
if (issue.code !== "custom")
|
|
4
|
+
return;
|
|
5
|
+
const code = issue.params?.code;
|
|
6
|
+
return typeof code === "string" ? code : undefined;
|
|
7
|
+
}
|
|
8
|
+
function isEmptyRequired(issue) {
|
|
9
|
+
if (issue.code === "too_small")
|
|
10
|
+
return issue.origin === "string" && Number(issue.minimum) === 1;
|
|
11
|
+
if (issue.code === "invalid_type")
|
|
12
|
+
return issue.input === undefined || issue.input === null;
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function createCodeErrorMap({
|
|
16
|
+
translate,
|
|
17
|
+
codeOf = defaultCodeOf,
|
|
18
|
+
fallback
|
|
19
|
+
}) {
|
|
20
|
+
return (issue) => {
|
|
21
|
+
const code = codeOf(issue);
|
|
22
|
+
if (code) {
|
|
23
|
+
const words = translate(code, issue.params);
|
|
24
|
+
if (words !== undefined && words !== code)
|
|
25
|
+
return words;
|
|
26
|
+
}
|
|
27
|
+
return fallback(issue);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
createCodeErrorMap,
|
|
32
|
+
defaultCodeOf,
|
|
33
|
+
isEmptyRequired
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
//# debugId=7DC5306E81E48FAB64756E2164756E21
|
|
37
|
+
//# sourceMappingURL=code-errors.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/code-errors.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * The part of a zod issue a code error map reads, by shape: core has no zod\n * dependency, and every issue zod 4 hands an error map is assignable to it.\n */\nexport type CodeIssue = {\n\treadonly code: string;\n\t/** A custom issue's params: `.refine(…, { params: { code } })`. */\n\treadonly params?: Record<string, unknown> | undefined;\n\t/** The value that failed. */\n\treadonly input?: unknown;\n\t/** What a size issue measured: `'string'`, `'number'`, `'array'`… */\n\treadonly origin?: string | undefined;\n\treadonly minimum?: unknown;\n\treadonly maximum?: unknown;\n\treadonly expected?: unknown;\n\treadonly message?: string | undefined;\n};\n\n/** The words for an issue, or `undefined` to let zod word it. */\nexport type CodeErrorMap = (issue: CodeIssue) => string | undefined;\n\nexport type CodeErrorMapOptions = {\n\t/**\n\t * The app's words for `code`, interpolating `params` (the custom issue's\n\t * params, so `{ code, min: 3 }` can read \"At least 3\"). `undefined`, or\n\t * the code returned verbatim (what a translator does with a key it lacks),\n\t * means there are no words for it: `fallback` words the issue instead.\n\t */\n\ttranslate: (\n\t\tcode: string,\n\t\tparams: Record<string, unknown> | undefined,\n\t) => string | undefined;\n\t/**\n\t * Which code an issue carries. Defaults to {@link defaultCodeOf}: a custom\n\t * issue's `params.code`, when it is a string.\n\t */\n\tcodeOf?: (issue: CodeIssue) => string | undefined;\n\t/**\n\t * Words an issue with no code, or one whose code has no words: the\n\t * libraries pass `zodLocaleError({ language, translate })`.\n\t */\n\tfallback: CodeErrorMap;\n};\n\n/** A custom issue's `params.code`, when it is a string; otherwise none. */\nexport function defaultCodeOf(issue: CodeIssue): string | undefined {\n\tif (issue.code !== 'custom') return undefined;\n\tconst code = issue.params?.code;\n\treturn typeof code === 'string' ? code : undefined;\n}\n\n/**\n * Whether an issue only says the field was left empty: a string shorter than\n * one character (`z.string().min(1)`), or a value of the wrong type that is\n * not there at all (`undefined` or `null`).\n *\n * @example\n * ```ts\n * createCodeErrorMap({\n * translate,\n * fallback,\n * codeOf: (issue) =>\n * isEmptyRequired(issue) ? 'errors.required' : defaultCodeOf(issue),\n * });\n * ```\n */\nexport function isEmptyRequired(issue: CodeIssue): boolean {\n\tif (issue.code === 'too_small')\n\t\treturn issue.origin === 'string' && Number(issue.minimum) === 1;\n\tif (issue.code === 'invalid_type')\n\t\treturn issue.input === undefined || issue.input === null;\n\treturn false;\n}\n\n/**\n * A zod 4 error map that words an issue by the code it carries, in the\n * app's words: a refinement's `params.code` (an API error code, say) reads\n * as `translate(code, params)`. An issue with no code, or whose code\n * `translate` has no words for, is worded by `fallback`.\n *\n * @example\n * ```ts\n * import { createCodeErrorMap } from '@nxgt/material-core/code-errors';\n *\n * const error = createCodeErrorMap({\n * translate: (code, params) => translate(code, params, language),\n * fallback: zodLocaleError({ language, translate }),\n * });\n * schema.safeParse(values, { error });\n * ```\n */\nexport function createCodeErrorMap({\n\ttranslate,\n\tcodeOf = defaultCodeOf,\n\tfallback,\n}: CodeErrorMapOptions): CodeErrorMap {\n\treturn (issue) => {\n\t\tconst code = codeOf(issue);\n\t\tif (code) {\n\t\t\tconst words = translate(code, issue.params);\n\t\t\tif (words !== undefined && words !== code) return words;\n\t\t}\n\t\treturn fallback(issue);\n\t};\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AA6CO,SAAS,aAAa,CAAC,OAAsC;AAAA,EACnE,IAAI,MAAM,SAAS;AAAA,IAAU;AAAA,EAC7B,MAAM,OAAO,MAAM,QAAQ;AAAA,EAC3B,OAAO,OAAO,SAAS,WAAW,OAAO;AAAA;AAkBnC,SAAS,eAAe,CAAC,OAA2B;AAAA,EAC1D,IAAI,MAAM,SAAS;AAAA,IAClB,OAAO,MAAM,WAAW,YAAY,OAAO,MAAM,OAAO,MAAM;AAAA,EAC/D,IAAI,MAAM,SAAS;AAAA,IAClB,OAAO,MAAM,UAAU,aAAa,MAAM,UAAU;AAAA,EACrD,OAAO;AAAA;AAoBD,SAAS,kBAAkB;AAAA,EACjC;AAAA,EACA,SAAS;AAAA,EACT;AAAA,GACqC;AAAA,EACrC,OAAO,CAAC,UAAU;AAAA,IACjB,MAAM,OAAO,OAAO,KAAK;AAAA,IACzB,IAAI,MAAM;AAAA,MACT,MAAM,QAAQ,UAAU,MAAM,MAAM,MAAM;AAAA,MAC1C,IAAI,UAAU,aAAa,UAAU;AAAA,QAAM,OAAO;AAAA,IACnD;AAAA,IACA,OAAO,SAAS,KAAK;AAAA;AAAA;",
|
|
8
|
+
"debugId": "7DC5306E81E48FAB64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -1,43 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const controls = root.matches(TABBABLE) ? [root] : root.querySelectorAll(TABBABLE);
|
|
6
|
-
for (const control of controls) {
|
|
7
|
-
if (control.getClientRects().length)
|
|
8
|
-
return control;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
function focusHandle(roots) {
|
|
14
|
-
return {
|
|
15
|
-
focus: () => {
|
|
16
|
-
firstControl(roots)?.focus();
|
|
17
|
-
setTimeout(() => {
|
|
18
|
-
const list = [...roots];
|
|
19
|
-
const active = list[0]?.ownerDocument.activeElement ?? null;
|
|
20
|
-
if (!list.some((root) => root.contains(active))) {
|
|
21
|
-
firstControl(list)?.focus();
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
},
|
|
25
|
-
select: () => {
|
|
26
|
-
const control = firstControl(roots);
|
|
27
|
-
if (control && "select" in control) {
|
|
28
|
-
control.select();
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
setCustomValidity: () => {
|
|
32
|
-
return;
|
|
33
|
-
},
|
|
34
|
-
reportValidity: () => true
|
|
35
|
-
};
|
|
36
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
firstControl2,
|
|
3
|
+
focusHandle2
|
|
4
|
+
} from "../chunks/index-407b6zs0.js";
|
|
37
5
|
export {
|
|
38
|
-
firstControl,
|
|
39
|
-
focusHandle
|
|
6
|
+
firstControl2 as firstControl,
|
|
7
|
+
focusHandle2 as focusHandle
|
|
40
8
|
};
|
|
41
9
|
|
|
42
|
-
//# debugId=
|
|
10
|
+
//# debugId=3CD7CAE6F251A45964756E2164756E21
|
|
43
11
|
//# sourceMappingURL=focus-control.js.map
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": [
|
|
3
|
+
"sources": [],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/** What a user can Tab to inside a field. */\nconst TABBABLE =\n\t'input:not([type=\"hidden\"]):not(:disabled):not([tabindex=\"-1\"]), textarea:not(:disabled):not([tabindex=\"-1\"]), select:not(:disabled):not([tabindex=\"-1\"]), button:not(:disabled):not([tabindex=\"-1\"]), [contenteditable=\"true\"], [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * The first control of `roots` a user can reach, in document order: one a\n * user can Tab to that is rendered — not a `display: none` input a library\n * keeps for forms. A root that is itself such a control is its own first\n * control. `null` when none is. Reads the DOM when called.\n */\nexport function firstControl(roots: Iterable<Element>): HTMLElement | null {\n\tfor (const root of roots) {\n\t\tconst controls = root.matches(TABBABLE)\n\t\t\t? [root as HTMLElement]\n\t\t\t: root.querySelectorAll<HTMLElement>(TABBABLE);\n\t\tfor (const control of controls) {\n\t\t\tif (control.getClientRects().length) return control;\n\t\t}\n\t}\n\treturn null;\n}\n\n/** What a form library calls on the element a field registered. */\nexport type FocusHandle = {\n\tfocus: () => void;\n\tselect: () => void;\n\tsetCustomValidity: (message: string) => void;\n\treportValidity: () => boolean;\n};\n\n/**\n * A stand-in element for a field made of several controls, for a form\n * library that focuses an invalid field by calling `focus()` on what the\n * field registered (React Hook Form's `field.ref`). `focus()` focuses the\n * first control of `roots` a user can reach, looked up when it is called —\n * `roots` is read live, so pass the field's own set — and again a turn\n * later when focus is not in the field by then: the error that triggered it\n * may re-render the field and replace its control. `select()` selects that\n * control's text when it has any. Native validation is not used:\n * `setCustomValidity` does nothing and `reportValidity` returns `true`.\n */\nexport function focusHandle(roots: Iterable<Element>): FocusHandle {\n\treturn {\n\t\tfocus: () => {\n\t\t\tfirstControl(roots)?.focus();\n\t\t\tsetTimeout(() => {\n\t\t\t\tconst list = [...roots];\n\t\t\t\tconst active = list[0]?.ownerDocument.activeElement ?? null;\n\t\t\t\tif (!list.some((root) => root.contains(active))) {\n\t\t\t\t\tfirstControl(list)?.focus();\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t\tselect: () => {\n\t\t\tconst control = firstControl(roots);\n\t\t\tif (control && 'select' in control) {\n\t\t\t\t(control as HTMLInputElement).select();\n\t\t\t}\n\t\t},\n\t\tsetCustomValidity: () => undefined,\n\t\treportValidity: () => true,\n\t};\n}\n"
|
|
6
5
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
6
|
+
"mappings": "",
|
|
7
|
+
"debugId": "3CD7CAE6F251A45964756E2164756E21",
|
|
9
8
|
"names": []
|
|
10
9
|
}
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { type PdfSource } from './pdf-source';
|
|
2
|
-
|
|
3
|
-
* How long an object URL outlives the click that started its download or
|
|
4
|
-
* opened its tab. Revoking it synchronously cancels the download in Firefox
|
|
5
|
-
* and Safari, which read the URL after `click()` returns.
|
|
6
|
-
*/
|
|
7
|
-
export declare const OBJECT_URL_TTL_MS = 60000;
|
|
2
|
+
export { OBJECT_URL_TTL_MS } from './save-blob';
|
|
8
3
|
export type PdfFileOptions = {
|
|
9
4
|
/** Name of the saved file; `.pdf` is added when missing. */
|
|
10
5
|
fileName?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pdf-download.d.ts","sourceRoot":"","sources":["../../src/lib/pdf-download.ts"],"names":[],"mappings":"AACA,OAAO,EAIN,KAAK,SAAS,EAEd,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"pdf-download.d.ts","sourceRoot":"","sources":["../../src/lib/pdf-download.ts"],"names":[],"mappings":"AACA,OAAO,EAIN,KAAK,SAAS,EAEd,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,MAAM,cAAc,GAAG;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MACP,UAAU,GACV,IAAI,GACJ,SAAS,GACT,OAAO,CAAC,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CAC1C,CAAC;AAsDF;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,MAAM,EAAE,SAAS,EACjB,OAAO,GAAE,cAAmB,iBAW5B;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CACjC,MAAM,EAAE,SAAS,EACjB,OAAO,GAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAM,iBA8B7C"}
|
package/dist/lib/pdf-download.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
OBJECT_URL_TTL_MS2,
|
|
3
2
|
downloadPdfFile2,
|
|
4
3
|
printPdfFile2
|
|
5
|
-
} from "../chunks/index-
|
|
4
|
+
} from "../chunks/index-r93kpbtg.js";
|
|
5
|
+
import {
|
|
6
|
+
OBJECT_URL_TTL_MS2
|
|
7
|
+
} from "../chunks/index-15vj9xbx.js";
|
|
6
8
|
import"../chunks/index-paaaedrm.js";
|
|
7
9
|
import"../chunks/index-5r6rzp43.js";
|
|
8
10
|
export {
|
|
@@ -11,5 +13,5 @@ export {
|
|
|
11
13
|
printPdfFile2 as printPdfFile
|
|
12
14
|
};
|
|
13
15
|
|
|
14
|
-
//# debugId=
|
|
16
|
+
//# debugId=ACA699EE49816B1764756E2164756E21
|
|
15
17
|
//# sourceMappingURL=pdf-download.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nxgt/material-core/print-only` — prints one element of the page and
|
|
3
|
+
* nothing else, without an inline style or a new window: the page's
|
|
4
|
+
* stylesheet does the hiding. Both libraries ship the rules (`print-only.css`,
|
|
5
|
+
* through their `styles.css`): under
|
|
6
|
+
* `html[data-nxgt-print]`, print media shows only `[data-nxgt-print-target]`.
|
|
7
|
+
* Reads the DOM only when called.
|
|
8
|
+
*/
|
|
9
|
+
/** Set on `<html>` while printing, to the `name` given to {@link printOnly}. */
|
|
10
|
+
export declare const PRINT_ATTRIBUTE = "data-nxgt-print";
|
|
11
|
+
/** Set on the element being printed. */
|
|
12
|
+
export declare const PRINT_TARGET_ATTRIBUTE = "data-nxgt-print-target";
|
|
13
|
+
/**
|
|
14
|
+
* Marks `target` as the only thing to print, opens the browser's print
|
|
15
|
+
* dialog and unmarks it once printing is over: on `afterprint`, or as soon as
|
|
16
|
+
* `print()` returns where it blocks until the dialog closes (it fired
|
|
17
|
+
* `beforeprint` before returning). The returned function unmarks it at once —
|
|
18
|
+
* call it when the element goes away, for a browser whose `print()` returns
|
|
19
|
+
* early and never says it is done.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const stop = printOnly(card, 'recovery-codes');
|
|
24
|
+
* // later, when the card unmounts:
|
|
25
|
+
* stop();
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function printOnly(target: HTMLElement, name?: string): () => void;
|
|
29
|
+
//# sourceMappingURL=print-only.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"print-only.d.ts","sourceRoot":"","sources":["../../src/lib/print-only.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,gFAAgF;AAChF,eAAO,MAAM,eAAe,oBAAoB,CAAC;AACjD,wCAAwC;AACxC,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAE/D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,SAAY,GAAG,MAAM,IAAI,CA0B3E"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/lib/print-only.ts
|
|
2
|
+
var PRINT_ATTRIBUTE = "data-nxgt-print";
|
|
3
|
+
var PRINT_TARGET_ATTRIBUTE = "data-nxgt-print-target";
|
|
4
|
+
function printOnly(target, name = "element") {
|
|
5
|
+
const doc = target.ownerDocument;
|
|
6
|
+
const view = doc.defaultView ?? window;
|
|
7
|
+
const root = doc.documentElement;
|
|
8
|
+
let began = false;
|
|
9
|
+
const onBegin = () => {
|
|
10
|
+
began = true;
|
|
11
|
+
};
|
|
12
|
+
const clear = () => {
|
|
13
|
+
view.removeEventListener("beforeprint", onBegin);
|
|
14
|
+
view.removeEventListener("afterprint", clear);
|
|
15
|
+
root.removeAttribute(PRINT_ATTRIBUTE);
|
|
16
|
+
target.removeAttribute(PRINT_TARGET_ATTRIBUTE);
|
|
17
|
+
};
|
|
18
|
+
root.setAttribute(PRINT_ATTRIBUTE, name);
|
|
19
|
+
target.setAttribute(PRINT_TARGET_ATTRIBUTE, "");
|
|
20
|
+
view.addEventListener("beforeprint", onBegin);
|
|
21
|
+
view.addEventListener("afterprint", clear);
|
|
22
|
+
try {
|
|
23
|
+
view.print();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
clear();
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
if (began)
|
|
29
|
+
clear();
|
|
30
|
+
return clear;
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
PRINT_ATTRIBUTE,
|
|
34
|
+
PRINT_TARGET_ATTRIBUTE,
|
|
35
|
+
printOnly
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//# debugId=BF8B5FF251A0F79964756E2164756E21
|
|
39
|
+
//# sourceMappingURL=print-only.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/print-only.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * `@nxgt/material-core/print-only` — prints one element of the page and\n * nothing else, without an inline style or a new window: the page's\n * stylesheet does the hiding. Both libraries ship the rules (`print-only.css`,\n * through their `styles.css`): under\n * `html[data-nxgt-print]`, print media shows only `[data-nxgt-print-target]`.\n * Reads the DOM only when called.\n */\n\n/** Set on `<html>` while printing, to the `name` given to {@link printOnly}. */\nexport const PRINT_ATTRIBUTE = 'data-nxgt-print';\n/** Set on the element being printed. */\nexport const PRINT_TARGET_ATTRIBUTE = 'data-nxgt-print-target';\n\n/**\n * Marks `target` as the only thing to print, opens the browser's print\n * dialog and unmarks it once printing is over: on `afterprint`, or as soon as\n * `print()` returns where it blocks until the dialog closes (it fired\n * `beforeprint` before returning). The returned function unmarks it at once —\n * call it when the element goes away, for a browser whose `print()` returns\n * early and never says it is done.\n *\n * @example\n * ```ts\n * const stop = printOnly(card, 'recovery-codes');\n * // later, when the card unmounts:\n * stop();\n * ```\n */\nexport function printOnly(target: HTMLElement, name = 'element'): () => void {\n\tconst doc = target.ownerDocument;\n\tconst view = doc.defaultView ?? window;\n\tconst root = doc.documentElement;\n\tlet began = false;\n\tconst onBegin = () => {\n\t\tbegan = true;\n\t};\n\tconst clear = () => {\n\t\tview.removeEventListener('beforeprint', onBegin);\n\t\tview.removeEventListener('afterprint', clear);\n\t\troot.removeAttribute(PRINT_ATTRIBUTE);\n\t\ttarget.removeAttribute(PRINT_TARGET_ATTRIBUTE);\n\t};\n\troot.setAttribute(PRINT_ATTRIBUTE, name);\n\ttarget.setAttribute(PRINT_TARGET_ATTRIBUTE, '');\n\tview.addEventListener('beforeprint', onBegin);\n\tview.addEventListener('afterprint', clear);\n\ttry {\n\t\tview.print();\n\t} catch (error) {\n\t\tclear();\n\t\tthrow error;\n\t}\n\tif (began) clear();\n\treturn clear;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAUO,IAAM,kBAAkB;AAExB,IAAM,yBAAyB;AAiB/B,SAAS,SAAS,CAAC,QAAqB,OAAO,WAAuB;AAAA,EAC5E,MAAM,MAAM,OAAO;AAAA,EACnB,MAAM,OAAO,IAAI,eAAe;AAAA,EAChC,MAAM,OAAO,IAAI;AAAA,EACjB,IAAI,QAAQ;AAAA,EACZ,MAAM,UAAU,MAAM;AAAA,IACrB,QAAQ;AAAA;AAAA,EAET,MAAM,QAAQ,MAAM;AAAA,IACnB,KAAK,oBAAoB,eAAe,OAAO;AAAA,IAC/C,KAAK,oBAAoB,cAAc,KAAK;AAAA,IAC5C,KAAK,gBAAgB,eAAe;AAAA,IACpC,OAAO,gBAAgB,sBAAsB;AAAA;AAAA,EAE9C,KAAK,aAAa,iBAAiB,IAAI;AAAA,EACvC,OAAO,aAAa,wBAAwB,EAAE;AAAA,EAC9C,KAAK,iBAAiB,eAAe,OAAO;AAAA,EAC5C,KAAK,iBAAiB,cAAc,KAAK;AAAA,EACzC,IAAI;AAAA,IACH,KAAK,MAAM;AAAA,IACV,OAAO,OAAO;AAAA,IACf,MAAM;AAAA,IACN,MAAM;AAAA;AAAA,EAEP,IAAI;AAAA,IAAO,MAAM;AAAA,EACjB,OAAO;AAAA;",
|
|
8
|
+
"debugId": "BF8B5FF251A0F79964756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Which way a row moves: towards the first row, or towards the last. */
|
|
2
|
+
export type RowMoveDirection = 'up' | 'down';
|
|
3
|
+
/**
|
|
4
|
+
* Where the focus goes once a RepeatableGroup's rows have changed: a row's
|
|
5
|
+
* first field, the Add button, or one of a row's move buttons. Indexes are
|
|
6
|
+
* 0-based, in the rows as they are after the change.
|
|
7
|
+
*/
|
|
8
|
+
export type RowFocusTarget = {
|
|
9
|
+
kind: 'field';
|
|
10
|
+
index: number;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'add';
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'move';
|
|
15
|
+
index: number;
|
|
16
|
+
direction: RowMoveDirection;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* What the group's polite live region says about a change. `index`, `from`
|
|
20
|
+
* and `to` are 1-based: they are what a user reads ("period 2").
|
|
21
|
+
*/
|
|
22
|
+
export type RowAnnouncement = {
|
|
23
|
+
kind: 'added';
|
|
24
|
+
index: number;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'removed';
|
|
27
|
+
index: number;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'moved';
|
|
30
|
+
from: number;
|
|
31
|
+
to: number;
|
|
32
|
+
};
|
|
33
|
+
/** Another row can be added: no `max`, or fewer rows than it. */
|
|
34
|
+
export declare const canAddRow: (count: number, max?: number) => boolean;
|
|
35
|
+
/** A row can be removed: no `min`, or more rows than it. */
|
|
36
|
+
export declare const canRemoveRow: (count: number, min?: number) => boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The index the row at `index` moves to, or `null` when it cannot move that
|
|
39
|
+
* way (the first row up, the last one down).
|
|
40
|
+
*/
|
|
41
|
+
export declare function rowMoveTarget(index: number, direction: RowMoveDirection, count: number): number | null;
|
|
42
|
+
/**
|
|
43
|
+
* Where the focus goes once the row at `removed` is gone and `remaining`
|
|
44
|
+
* rows are left: the row that took its place, else the row before it (the
|
|
45
|
+
* last one was removed), else the Add button (no row is left).
|
|
46
|
+
*/
|
|
47
|
+
export declare function focusAfterRemove(removed: number, remaining: number): RowFocusTarget;
|
|
48
|
+
/**
|
|
49
|
+
* Where the focus goes once a row moved `direction` to `to`: the button that
|
|
50
|
+
* moved it, so the user can press it again — unless the row can no longer
|
|
51
|
+
* move that way (it reached the first or the last place, and that button is
|
|
52
|
+
* now disabled), then the other move button of the same row.
|
|
53
|
+
*/
|
|
54
|
+
export declare function focusAfterMove(to: number, direction: RowMoveDirection, count: number): RowFocusTarget;
|
|
55
|
+
/** The `data-slot` of each row and of its fields. */
|
|
56
|
+
export declare const ROW_SLOT = "repeatable-group-row";
|
|
57
|
+
export declare const ROW_FIELDS_SLOT = "repeatable-group-fields";
|
|
58
|
+
/**
|
|
59
|
+
* The attribute that marks the Add button: an attribute of its own, as the
|
|
60
|
+
* button keeps the `data-slot` of the library's Button.
|
|
61
|
+
*/
|
|
62
|
+
export declare const ADD_ATTRIBUTE = "data-repeatable-group-add";
|
|
63
|
+
/**
|
|
64
|
+
* Moves the focus to `target` inside the group's `root`: the first control a
|
|
65
|
+
* user can reach in the row's fields, the Add button (`data-repeatable-group-add`), or the row's move
|
|
66
|
+
* button (`data-move="up"` / `"down"`). Reads the DOM when called; returns
|
|
67
|
+
* whether something took the focus.
|
|
68
|
+
*/
|
|
69
|
+
export declare function focusRowTarget(root: Element, target: RowFocusTarget): boolean;
|
|
70
|
+
//# sourceMappingURL=repeatable-group.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repeatable-group.d.ts","sourceRoot":"","sources":["../../src/lib/repeatable-group.ts"],"names":[],"mappings":"AAEA,yEAAyE;AACzE,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,MAAM,CAAC;AAE7C;;;;GAIG;AACH,MAAM,MAAM,cAAc,GACvB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAEhE;;;GAGG;AACH,MAAM,MAAM,eAAe,GACxB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,iEAAiE;AACjE,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,OACvB,CAAC;AAElC,4DAA4D;AAC5D,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,OAC1B,CAAC;AAElC;;;GAGG;AACH,wBAAgB,aAAa,CAC5B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,gBAAgB,EAC3B,KAAK,EAAE,MAAM,GACX,MAAM,GAAG,IAAI,CAGf;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC/B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GACf,cAAc,CAGhB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC7B,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,gBAAgB,EAC3B,KAAK,EAAE,MAAM,GACX,cAAc,CAIhB;AAED,qDAAqD;AACrD,eAAO,MAAM,QAAQ,yBAAyB,CAAC;AAC/C,eAAO,MAAM,eAAe,4BAA4B,CAAC;AACzD;;;GAGG;AACH,eAAO,MAAM,aAAa,8BAA8B,CAAC;AAEzD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAK7E"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
firstControl2
|
|
3
|
+
} from "../chunks/index-407b6zs0.js";
|
|
4
|
+
|
|
5
|
+
// src/lib/repeatable-group.ts
|
|
6
|
+
var canAddRow = (count, max) => max === undefined || count < max;
|
|
7
|
+
var canRemoveRow = (count, min) => min === undefined || count > min;
|
|
8
|
+
function rowMoveTarget(index, direction, count) {
|
|
9
|
+
const to = direction === "up" ? index - 1 : index + 1;
|
|
10
|
+
return to < 0 || to >= count ? null : to;
|
|
11
|
+
}
|
|
12
|
+
function focusAfterRemove(removed, remaining) {
|
|
13
|
+
if (remaining <= 0)
|
|
14
|
+
return { kind: "add" };
|
|
15
|
+
return { kind: "field", index: Math.min(removed, remaining - 1) };
|
|
16
|
+
}
|
|
17
|
+
function focusAfterMove(to, direction, count) {
|
|
18
|
+
const stays = rowMoveTarget(to, direction, count) !== null;
|
|
19
|
+
const other = direction === "up" ? "down" : "up";
|
|
20
|
+
return { kind: "move", index: to, direction: stays ? direction : other };
|
|
21
|
+
}
|
|
22
|
+
var ROW_SLOT = "repeatable-group-row";
|
|
23
|
+
var ROW_FIELDS_SLOT = "repeatable-group-fields";
|
|
24
|
+
var ADD_ATTRIBUTE = "data-repeatable-group-add";
|
|
25
|
+
function focusRowTarget(root, target) {
|
|
26
|
+
const el = targetElement(root, target);
|
|
27
|
+
if (!el)
|
|
28
|
+
return false;
|
|
29
|
+
el.focus();
|
|
30
|
+
return el.ownerDocument.activeElement === el;
|
|
31
|
+
}
|
|
32
|
+
function targetElement(root, target) {
|
|
33
|
+
if (target.kind === "add") {
|
|
34
|
+
return root.querySelector(`[${ADD_ATTRIBUTE}]`);
|
|
35
|
+
}
|
|
36
|
+
const rows = root.querySelectorAll(`[data-slot="${ROW_SLOT}"]`);
|
|
37
|
+
const row = rows[target.index];
|
|
38
|
+
if (!row)
|
|
39
|
+
return null;
|
|
40
|
+
if (target.kind === "move") {
|
|
41
|
+
return row.querySelector(`[data-move="${target.direction}"]`);
|
|
42
|
+
}
|
|
43
|
+
const fields = row.querySelector(`[data-slot="${ROW_FIELDS_SLOT}"]`);
|
|
44
|
+
return firstControl2([fields ?? row]);
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
ADD_ATTRIBUTE,
|
|
48
|
+
ROW_FIELDS_SLOT,
|
|
49
|
+
ROW_SLOT,
|
|
50
|
+
canAddRow,
|
|
51
|
+
canRemoveRow,
|
|
52
|
+
focusAfterMove,
|
|
53
|
+
focusAfterRemove,
|
|
54
|
+
focusRowTarget,
|
|
55
|
+
rowMoveTarget
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
//# debugId=EF31DAFB676F33B564756E2164756E21
|
|
59
|
+
//# sourceMappingURL=repeatable-group.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/repeatable-group.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { firstControl } from './focus-control';\n\n/** Which way a row moves: towards the first row, or towards the last. */\nexport type RowMoveDirection = 'up' | 'down';\n\n/**\n * Where the focus goes once a RepeatableGroup's rows have changed: a row's\n * first field, the Add button, or one of a row's move buttons. Indexes are\n * 0-based, in the rows as they are after the change.\n */\nexport type RowFocusTarget =\n\t| { kind: 'field'; index: number }\n\t| { kind: 'add' }\n\t| { kind: 'move'; index: number; direction: RowMoveDirection };\n\n/**\n * What the group's polite live region says about a change. `index`, `from`\n * and `to` are 1-based: they are what a user reads (\"period 2\").\n */\nexport type RowAnnouncement =\n\t| { kind: 'added'; index: number }\n\t| { kind: 'removed'; index: number }\n\t| { kind: 'moved'; from: number; to: number };\n\n/** Another row can be added: no `max`, or fewer rows than it. */\nexport const canAddRow = (count: number, max?: number): boolean =>\n\tmax === undefined || count < max;\n\n/** A row can be removed: no `min`, or more rows than it. */\nexport const canRemoveRow = (count: number, min?: number): boolean =>\n\tmin === undefined || count > min;\n\n/**\n * The index the row at `index` moves to, or `null` when it cannot move that\n * way (the first row up, the last one down).\n */\nexport function rowMoveTarget(\n\tindex: number,\n\tdirection: RowMoveDirection,\n\tcount: number,\n): number | null {\n\tconst to = direction === 'up' ? index - 1 : index + 1;\n\treturn to < 0 || to >= count ? null : to;\n}\n\n/**\n * Where the focus goes once the row at `removed` is gone and `remaining`\n * rows are left: the row that took its place, else the row before it (the\n * last one was removed), else the Add button (no row is left).\n */\nexport function focusAfterRemove(\n\tremoved: number,\n\tremaining: number,\n): RowFocusTarget {\n\tif (remaining <= 0) return { kind: 'add' };\n\treturn { kind: 'field', index: Math.min(removed, remaining - 1) };\n}\n\n/**\n * Where the focus goes once a row moved `direction` to `to`: the button that\n * moved it, so the user can press it again — unless the row can no longer\n * move that way (it reached the first or the last place, and that button is\n * now disabled), then the other move button of the same row.\n */\nexport function focusAfterMove(\n\tto: number,\n\tdirection: RowMoveDirection,\n\tcount: number,\n): RowFocusTarget {\n\tconst stays = rowMoveTarget(to, direction, count) !== null;\n\tconst other: RowMoveDirection = direction === 'up' ? 'down' : 'up';\n\treturn { kind: 'move', index: to, direction: stays ? direction : other };\n}\n\n/** The `data-slot` of each row and of its fields. */\nexport const ROW_SLOT = 'repeatable-group-row';\nexport const ROW_FIELDS_SLOT = 'repeatable-group-fields';\n/**\n * The attribute that marks the Add button: an attribute of its own, as the\n * button keeps the `data-slot` of the library's Button.\n */\nexport const ADD_ATTRIBUTE = 'data-repeatable-group-add';\n\n/**\n * Moves the focus to `target` inside the group's `root`: the first control a\n * user can reach in the row's fields, the Add button (`data-repeatable-group-add`), or the row's move\n * button (`data-move=\"up\"` / `\"down\"`). Reads the DOM when called; returns\n * whether something took the focus.\n */\nexport function focusRowTarget(root: Element, target: RowFocusTarget): boolean {\n\tconst el = targetElement(root, target);\n\tif (!el) return false;\n\tel.focus();\n\treturn el.ownerDocument.activeElement === el;\n}\n\nfunction targetElement(\n\troot: Element,\n\ttarget: RowFocusTarget,\n): HTMLElement | null {\n\tif (target.kind === 'add') {\n\t\treturn root.querySelector<HTMLElement>(`[${ADD_ATTRIBUTE}]`);\n\t}\n\tconst rows = root.querySelectorAll(`[data-slot=\"${ROW_SLOT}\"]`);\n\tconst row = rows[target.index];\n\tif (!row) return null;\n\tif (target.kind === 'move') {\n\t\treturn row.querySelector<HTMLElement>(`[data-move=\"${target.direction}\"]`);\n\t}\n\tconst fields = row.querySelector(`[data-slot=\"${ROW_FIELDS_SLOT}\"]`);\n\treturn firstControl([fields ?? row]);\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;AAyBO,IAAM,YAAY,CAAC,OAAe,QACxC,QAAQ,aAAa,QAAQ;AAGvB,IAAM,eAAe,CAAC,OAAe,QAC3C,QAAQ,aAAa,QAAQ;AAMvB,SAAS,aAAa,CAC5B,OACA,WACA,OACgB;AAAA,EAChB,MAAM,KAAK,cAAc,OAAO,QAAQ,IAAI,QAAQ;AAAA,EACpD,OAAO,KAAK,KAAK,MAAM,QAAQ,OAAO;AAAA;AAQhC,SAAS,gBAAgB,CAC/B,SACA,WACiB;AAAA,EACjB,IAAI,aAAa;AAAA,IAAG,OAAO,EAAE,MAAM,MAAM;AAAA,EACzC,OAAO,EAAE,MAAM,SAAS,OAAO,KAAK,IAAI,SAAS,YAAY,CAAC,EAAE;AAAA;AAS1D,SAAS,cAAc,CAC7B,IACA,WACA,OACiB;AAAA,EACjB,MAAM,QAAQ,cAAc,IAAI,WAAW,KAAK,MAAM;AAAA,EACtD,MAAM,QAA0B,cAAc,OAAO,SAAS;AAAA,EAC9D,OAAO,EAAE,MAAM,QAAQ,OAAO,IAAI,WAAW,QAAQ,YAAY,MAAM;AAAA;AAIjE,IAAM,WAAW;AACjB,IAAM,kBAAkB;AAKxB,IAAM,gBAAgB;AAQtB,SAAS,cAAc,CAAC,MAAe,QAAiC;AAAA,EAC9E,MAAM,KAAK,cAAc,MAAM,MAAM;AAAA,EACrC,IAAI,CAAC;AAAA,IAAI,OAAO;AAAA,EAChB,GAAG,MAAM;AAAA,EACT,OAAO,GAAG,cAAc,kBAAkB;AAAA;AAG3C,SAAS,aAAa,CACrB,MACA,QACqB;AAAA,EACrB,IAAI,OAAO,SAAS,OAAO;AAAA,IAC1B,OAAO,KAAK,cAA2B,IAAI,gBAAgB;AAAA,EAC5D;AAAA,EACA,MAAM,OAAO,KAAK,iBAAiB,eAAe,YAAY;AAAA,EAC9D,MAAM,MAAM,KAAK,OAAO;AAAA,EACxB,IAAI,CAAC;AAAA,IAAK,OAAO;AAAA,EACjB,IAAI,OAAO,SAAS,QAAQ;AAAA,IAC3B,OAAO,IAAI,cAA2B,eAAe,OAAO,aAAa;AAAA,EAC1E;AAAA,EACA,MAAM,SAAS,IAAI,cAAc,eAAe,mBAAmB;AAAA,EACnE,OAAO,cAAa,CAAC,UAAU,GAAG,CAAC;AAAA;",
|
|
8
|
+
"debugId": "EF31DAFB676F33B564756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saving a Blob as a file through a temporary `<a download>`: what
|
|
3
|
+
* `/pdf-download` and `/two-factor`'s `downloadText` share. Reads the DOM
|
|
4
|
+
* only when called.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* How long an object URL outlives the click that started its download or
|
|
8
|
+
* opened its tab. Revoking it synchronously cancels the download in Firefox
|
|
9
|
+
* and Safari, which read the URL after `click()` returns.
|
|
10
|
+
*/
|
|
11
|
+
export declare const OBJECT_URL_TTL_MS = 60000;
|
|
12
|
+
export declare function revokeLater(url: string): void;
|
|
13
|
+
export declare function clickLink(href: string, attributes: Record<string, string>): void;
|
|
14
|
+
/** Saves a Blob under `fileName`, and revokes its URL once the save began. */
|
|
15
|
+
export declare function saveBlob(blob: Blob, fileName: string): void;
|
|
16
|
+
//# sourceMappingURL=save-blob.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"save-blob.d.ts","sourceRoot":"","sources":["../../src/lib/save-blob.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,QAAS,CAAC;AAExC,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,QAEtC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAWzE;AAED,8EAA8E;AAC9E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,QAIpD"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** How {@link SubmitGuard.once} treats what `run` returns. */
|
|
2
|
+
export type SubmitOnceOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* A result that is not a promise (a handler that returned early, say)
|
|
5
|
+
* settles the guard at once. Default `false`: the guard then waits for
|
|
6
|
+
* `settle()`, which suits a request followed by a busy flag
|
|
7
|
+
* ({@link busySettler}). Without one, return the request's promise or set
|
|
8
|
+
* this.
|
|
9
|
+
*/
|
|
10
|
+
settleSync?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** One submit in flight: see {@link createSubmitGuard}. */
|
|
13
|
+
export type SubmitGuard = {
|
|
14
|
+
/**
|
|
15
|
+
* `run`, wrapped so it runs only when no submit is on its way. The wrapper
|
|
16
|
+
* returns what `run` returned — a promise as one that settles as `run`'s
|
|
17
|
+
* does, rejection included — or `undefined` when the call was dropped.
|
|
18
|
+
* A dropped call whose first argument is an event (a form's `submit`)
|
|
19
|
+
* has its default prevented, so it never falls back to the browser's own
|
|
20
|
+
* submission, which would reload the page.
|
|
21
|
+
*/
|
|
22
|
+
once<A extends unknown[], R>(run: (...args: A) => R, options?: SubmitOnceOptions): (...args: A) => R | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Opens the guard again: the next call goes through. Given a run (from
|
|
25
|
+
* {@link SubmitGuard.pendingRun}), only while that run is still the one
|
|
26
|
+
* on its way: a late `settle(run)` never reopens it over a newer one.
|
|
27
|
+
*/
|
|
28
|
+
settle(run?: number): void;
|
|
29
|
+
/** The run on its way (an increasing number), or `undefined` when open. */
|
|
30
|
+
pendingRun(): number | undefined;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Lets one submit through until the request it started settles. A second
|
|
34
|
+
* click, or Enter, before the busy state renders (a disabled or loading
|
|
35
|
+
* button only stops clicks once rendered, and never Enter) would post again,
|
|
36
|
+
* and may abort the first request mid-call.
|
|
37
|
+
*
|
|
38
|
+
* `settle()` opens it again. A `run` that returns a promise settles it when
|
|
39
|
+
* that promise ends, fulfilled or rejected; a `run` that throws settles it
|
|
40
|
+
* and rethrows; any other result keeps it closed until `settle()`, unless
|
|
41
|
+
* `once(run, { settleSync: true })`. So: return the request's promise, or
|
|
42
|
+
* follow the request with a busy flag ({@link busySettler}). The guard never
|
|
43
|
+
* swallows a rejection: the promise the wrapper returns rejects as `run`'s
|
|
44
|
+
* did, for the caller to handle (left unhandled, it is reported as `run`'s
|
|
45
|
+
* would have been), and the guard adds no promise of its own that could
|
|
46
|
+
* reject unhandled.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const guard = createSubmitGuard();
|
|
51
|
+
* const submit = guard.once((values: Values) => save(values));
|
|
52
|
+
* form.addEventListener('submit', (event) => {
|
|
53
|
+
* event.preventDefault();
|
|
54
|
+
* submit(read(form)); // a second submit before `save` ends is dropped
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function createSubmitGuard(): SubmitGuard;
|
|
59
|
+
/**
|
|
60
|
+
* Follows a request's busy flag for `guard`: call the function it returns
|
|
61
|
+
* with each value of the flag. Once the flag was true and is false again, it
|
|
62
|
+
* settles the run that was on its way when the flag rose — never a newer one
|
|
63
|
+
* — or, when none was, the run on its way when it falls.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const onBusy = busySettler(guard);
|
|
68
|
+
* router.subscribe((state) => onBusy(state.navigation.state !== 'idle'));
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export declare function busySettler(guard: SubmitGuard): (busy: boolean) => void;
|
|
72
|
+
//# sourceMappingURL=submit-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submit-guard.d.ts","sourceRoot":"","sources":["../../src/lib/submit-guard.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,MAAM,MAAM,iBAAiB,GAAG;IAC/B;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,WAAW,GAAG;IACzB;;;;;;;OAOG;IACH,IAAI,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,CAAC,EAC1B,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,EACtB,OAAO,CAAC,EAAE,iBAAiB,GACzB,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,2EAA2E;IAC3E,UAAU,IAAI,MAAM,GAAG,SAAS,CAAC;CACjC,CAAC;AAcF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,IAAI,WAAW,CAuC/C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAcvE"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/lib/submit-guard.ts
|
|
2
|
+
var preventDefault = (event) => {
|
|
3
|
+
const target = event;
|
|
4
|
+
if (typeof target?.preventDefault === "function")
|
|
5
|
+
event.preventDefault();
|
|
6
|
+
};
|
|
7
|
+
var isThenable = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
|
|
8
|
+
function createSubmitGuard() {
|
|
9
|
+
let pending = false;
|
|
10
|
+
let run = 0;
|
|
11
|
+
const settle = (id) => {
|
|
12
|
+
if (id === undefined || id === run)
|
|
13
|
+
pending = false;
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
once(submit, options = {}) {
|
|
17
|
+
return (...args) => {
|
|
18
|
+
if (pending) {
|
|
19
|
+
preventDefault(args[0]);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
pending = true;
|
|
23
|
+
run += 1;
|
|
24
|
+
const id = run;
|
|
25
|
+
let ran;
|
|
26
|
+
try {
|
|
27
|
+
ran = submit(...args);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
settle(id);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
if (!isThenable(ran)) {
|
|
33
|
+
if (options.settleSync)
|
|
34
|
+
settle(id);
|
|
35
|
+
return ran;
|
|
36
|
+
}
|
|
37
|
+
const settled = Promise.resolve(ran).finally(() => settle(id));
|
|
38
|
+
return settled;
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
settle,
|
|
42
|
+
pendingRun: () => pending ? run : undefined
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function busySettler(guard) {
|
|
46
|
+
let rose = false;
|
|
47
|
+
let runAtRise;
|
|
48
|
+
return (busy) => {
|
|
49
|
+
if (busy) {
|
|
50
|
+
if (!rose)
|
|
51
|
+
runAtRise = guard.pendingRun();
|
|
52
|
+
rose = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!rose)
|
|
56
|
+
return;
|
|
57
|
+
rose = false;
|
|
58
|
+
const run = runAtRise ?? guard.pendingRun();
|
|
59
|
+
if (run !== undefined)
|
|
60
|
+
guard.settle(run);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
busySettler,
|
|
65
|
+
createSubmitGuard
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
//# debugId=0F5FC92C3A2BF8E064756E2164756E21
|
|
69
|
+
//# sourceMappingURL=submit-guard.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/submit-guard.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/** How {@link SubmitGuard.once} treats what `run` returns. */\nexport type SubmitOnceOptions = {\n\t/**\n\t * A result that is not a promise (a handler that returned early, say)\n\t * settles the guard at once. Default `false`: the guard then waits for\n\t * `settle()`, which suits a request followed by a busy flag\n\t * ({@link busySettler}). Without one, return the request's promise or set\n\t * this.\n\t */\n\tsettleSync?: boolean;\n};\n\n/** One submit in flight: see {@link createSubmitGuard}. */\nexport type SubmitGuard = {\n\t/**\n\t * `run`, wrapped so it runs only when no submit is on its way. The wrapper\n\t * returns what `run` returned — a promise as one that settles as `run`'s\n\t * does, rejection included — or `undefined` when the call was dropped.\n\t * A dropped call whose first argument is an event (a form's `submit`)\n\t * has its default prevented, so it never falls back to the browser's own\n\t * submission, which would reload the page.\n\t */\n\tonce<A extends unknown[], R>(\n\t\trun: (...args: A) => R,\n\t\toptions?: SubmitOnceOptions,\n\t): (...args: A) => R | undefined;\n\t/**\n\t * Opens the guard again: the next call goes through. Given a run (from\n\t * {@link SubmitGuard.pendingRun}), only while that run is still the one\n\t * on its way: a late `settle(run)` never reopens it over a newer one.\n\t */\n\tsettle(run?: number): void;\n\t/** The run on its way (an increasing number), or `undefined` when open. */\n\tpendingRun(): number | undefined;\n};\n\n/** Prevents the default of a dropped call's event, if it carries one. */\nconst preventDefault = (event: unknown) => {\n\tconst target = event as { preventDefault?: unknown } | null;\n\tif (typeof target?.preventDefault === 'function')\n\t\t(event as Event).preventDefault();\n};\n\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n\ttypeof value === 'object' &&\n\tvalue !== null &&\n\ttypeof (value as { then?: unknown }).then === 'function';\n\n/**\n * Lets one submit through until the request it started settles. A second\n * click, or Enter, before the busy state renders (a disabled or loading\n * button only stops clicks once rendered, and never Enter) would post again,\n * and may abort the first request mid-call.\n *\n * `settle()` opens it again. A `run` that returns a promise settles it when\n * that promise ends, fulfilled or rejected; a `run` that throws settles it\n * and rethrows; any other result keeps it closed until `settle()`, unless\n * `once(run, { settleSync: true })`. So: return the request's promise, or\n * follow the request with a busy flag ({@link busySettler}). The guard never\n * swallows a rejection: the promise the wrapper returns rejects as `run`'s\n * did, for the caller to handle (left unhandled, it is reported as `run`'s\n * would have been), and the guard adds no promise of its own that could\n * reject unhandled.\n *\n * @example\n * ```ts\n * const guard = createSubmitGuard();\n * const submit = guard.once((values: Values) => save(values));\n * form.addEventListener('submit', (event) => {\n * event.preventDefault();\n * submit(read(form)); // a second submit before `save` ends is dropped\n * });\n * ```\n */\nexport function createSubmitGuard(): SubmitGuard {\n\tlet pending = false;\n\t// Which run is on its way: an older run's promise (or busy flag) ending\n\t// late never reopens the guard over a newer one.\n\tlet run = 0;\n\tconst settle = (id?: number) => {\n\t\tif (id === undefined || id === run) pending = false;\n\t};\n\treturn {\n\t\tonce(submit, options = {}) {\n\t\t\treturn (...args) => {\n\t\t\t\tif (pending) {\n\t\t\t\t\tpreventDefault(args[0]);\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tpending = true;\n\t\t\t\trun += 1;\n\t\t\t\tconst id = run;\n\t\t\t\tlet ran: ReturnType<typeof submit>;\n\t\t\t\ttry {\n\t\t\t\t\tran = submit(...args);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsettle(id);\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tif (!isThenable(ran)) {\n\t\t\t\t\tif (options.settleSync) settle(id);\n\t\t\t\t\treturn ran;\n\t\t\t\t}\n\t\t\t\t// The one chain on `ran`, handed back: a rejection reaches the\n\t\t\t\t// caller, never a second, unhandled promise. It is the same\n\t\t\t\t// promise type for a native promise (`Promise.resolve` returns it).\n\t\t\t\tconst settled = Promise.resolve(ran).finally(() => settle(id));\n\t\t\t\treturn settled as unknown as typeof ran;\n\t\t\t};\n\t\t},\n\t\tsettle,\n\t\tpendingRun: () => (pending ? run : undefined),\n\t};\n}\n\n/**\n * Follows a request's busy flag for `guard`: call the function it returns\n * with each value of the flag. Once the flag was true and is false again, it\n * settles the run that was on its way when the flag rose — never a newer one\n * — or, when none was, the run on its way when it falls.\n *\n * @example\n * ```ts\n * const onBusy = busySettler(guard);\n * router.subscribe((state) => onBusy(state.navigation.state !== 'idle'));\n * ```\n */\nexport function busySettler(guard: SubmitGuard): (busy: boolean) => void {\n\tlet rose = false;\n\tlet runAtRise: number | undefined;\n\treturn (busy) => {\n\t\tif (busy) {\n\t\t\tif (!rose) runAtRise = guard.pendingRun();\n\t\t\trose = true;\n\t\t\treturn;\n\t\t}\n\t\tif (!rose) return;\n\t\trose = false;\n\t\tconst run = runAtRise ?? guard.pendingRun();\n\t\tif (run !== undefined) guard.settle(run);\n\t};\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAqCA,IAAM,iBAAiB,CAAC,UAAmB;AAAA,EAC1C,MAAM,SAAS;AAAA,EACf,IAAI,OAAO,QAAQ,mBAAmB;AAAA,IACpC,MAAgB,eAAe;AAAA;AAGlC,IAAM,aAAa,CAAC,UACnB,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAA6B,SAAS;AA4BxC,SAAS,iBAAiB,GAAgB;AAAA,EAChD,IAAI,UAAU;AAAA,EAGd,IAAI,MAAM;AAAA,EACV,MAAM,SAAS,CAAC,OAAgB;AAAA,IAC/B,IAAI,OAAO,aAAa,OAAO;AAAA,MAAK,UAAU;AAAA;AAAA,EAE/C,OAAO;AAAA,IACN,IAAI,CAAC,QAAQ,UAAU,CAAC,GAAG;AAAA,MAC1B,OAAO,IAAI,SAAS;AAAA,QACnB,IAAI,SAAS;AAAA,UACZ,eAAe,KAAK,EAAE;AAAA,UACtB;AAAA,QACD;AAAA,QACA,UAAU;AAAA,QACV,OAAO;AAAA,QACP,MAAM,KAAK;AAAA,QACX,IAAI;AAAA,QACJ,IAAI;AAAA,UACH,MAAM,OAAO,GAAG,IAAI;AAAA,UACnB,OAAO,OAAO;AAAA,UACf,OAAO,EAAE;AAAA,UACT,MAAM;AAAA;AAAA,QAEP,IAAI,CAAC,WAAW,GAAG,GAAG;AAAA,UACrB,IAAI,QAAQ;AAAA,YAAY,OAAO,EAAE;AAAA,UACjC,OAAO;AAAA,QACR;AAAA,QAIA,MAAM,UAAU,QAAQ,QAAQ,GAAG,EAAE,QAAQ,MAAM,OAAO,EAAE,CAAC;AAAA,QAC7D,OAAO;AAAA;AAAA;AAAA,IAGT;AAAA,IACA,YAAY,MAAO,UAAU,MAAM;AAAA,EACpC;AAAA;AAeM,SAAS,WAAW,CAAC,OAA6C;AAAA,EACxE,IAAI,OAAO;AAAA,EACX,IAAI;AAAA,EACJ,OAAO,CAAC,SAAS;AAAA,IAChB,IAAI,MAAM;AAAA,MACT,IAAI,CAAC;AAAA,QAAM,YAAY,MAAM,WAAW;AAAA,MACxC,OAAO;AAAA,MACP;AAAA,IACD;AAAA,IACA,IAAI,CAAC;AAAA,MAAM;AAAA,IACX,OAAO;AAAA,IACP,MAAM,MAAM,aAAa,MAAM,WAAW;AAAA,IAC1C,IAAI,QAAQ;AAAA,MAAW,MAAM,OAAO,GAAG;AAAA;AAAA;",
|
|
8
|
+
"debugId": "0F5FC92C3A2BF8E064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|