@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +180 -6
  3. package/dist/chunks/index-15vj9xbx.js +26 -0
  4. package/dist/chunks/index-15vj9xbx.js.map +10 -0
  5. package/dist/chunks/index-407b6zs0.js +41 -0
  6. package/dist/chunks/index-407b6zs0.js.map +10 -0
  7. package/dist/chunks/{index-h3vjydz7.js → index-r93kpbtg.js} +8 -23
  8. package/dist/chunks/index-r93kpbtg.js.map +10 -0
  9. package/dist/index.js +5 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/lib/code-errors.d.ts +75 -0
  12. package/dist/lib/code-errors.d.ts.map +1 -0
  13. package/dist/lib/code-errors.js +37 -0
  14. package/dist/lib/code-errors.js.map +10 -0
  15. package/dist/lib/focus-control.js +7 -39
  16. package/dist/lib/focus-control.js.map +3 -4
  17. package/dist/lib/pdf-download.d.ts +1 -6
  18. package/dist/lib/pdf-download.d.ts.map +1 -1
  19. package/dist/lib/pdf-download.js +5 -3
  20. package/dist/lib/pdf-download.js.map +1 -1
  21. package/dist/lib/print-only.d.ts +29 -0
  22. package/dist/lib/print-only.d.ts.map +1 -0
  23. package/dist/lib/print-only.js +39 -0
  24. package/dist/lib/print-only.js.map +10 -0
  25. package/dist/lib/repeatable-group.d.ts +70 -0
  26. package/dist/lib/repeatable-group.d.ts.map +1 -0
  27. package/dist/lib/repeatable-group.js +59 -0
  28. package/dist/lib/repeatable-group.js.map +10 -0
  29. package/dist/lib/save-blob.d.ts +16 -0
  30. package/dist/lib/save-blob.d.ts.map +1 -0
  31. package/dist/lib/submit-guard.d.ts +72 -0
  32. package/dist/lib/submit-guard.d.ts.map +1 -0
  33. package/dist/lib/submit-guard.js +69 -0
  34. package/dist/lib/submit-guard.js.map +10 -0
  35. package/dist/lib/two-factor.d.ts +37 -0
  36. package/dist/lib/two-factor.d.ts.map +1 -0
  37. package/dist/lib/two-factor.js +38 -0
  38. package/dist/lib/two-factor.js.map +10 -0
  39. package/docs/roadmap.md +13 -0
  40. package/package.json +32 -2
  41. 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
- // src/lib/focus-control.ts
2
- var TABBABLE = '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"])';
3
- function firstControl(roots) {
4
- for (const root of roots) {
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=F6055B3989727FBE64756E2164756E21
10
+ //# debugId=3CD7CAE6F251A45964756E2164756E21
43
11
  //# sourceMappingURL=focus-control.js.map
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/lib/focus-control.ts"],
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": ";AACA,IAAM,WACL;AAQM,SAAS,YAAY,CAAC,OAA8C;AAAA,EAC1E,WAAW,QAAQ,OAAO;AAAA,IACzB,MAAM,WAAW,KAAK,QAAQ,QAAQ,IACnC,CAAC,IAAmB,IACpB,KAAK,iBAA8B,QAAQ;AAAA,IAC9C,WAAW,WAAW,UAAU;AAAA,MAC/B,IAAI,QAAQ,eAAe,EAAE;AAAA,QAAQ,OAAO;AAAA,IAC7C;AAAA,EACD;AAAA,EACA,OAAO;AAAA;AAsBD,SAAS,WAAW,CAAC,OAAuC;AAAA,EAClE,OAAO;AAAA,IACN,OAAO,MAAM;AAAA,MACZ,aAAa,KAAK,GAAG,MAAM;AAAA,MAC3B,WAAW,MAAM;AAAA,QAChB,MAAM,OAAO,CAAC,GAAG,KAAK;AAAA,QACtB,MAAM,SAAS,KAAK,IAAI,cAAc,iBAAiB;AAAA,QACvD,IAAI,CAAC,KAAK,KAAK,CAAC,SAAS,KAAK,SAAS,MAAM,CAAC,GAAG;AAAA,UAChD,aAAa,IAAI,GAAG,MAAM;AAAA,QAC3B;AAAA,OACA;AAAA;AAAA,IAEF,QAAQ,MAAM;AAAA,MACb,MAAM,UAAU,aAAa,KAAK;AAAA,MAClC,IAAI,WAAW,YAAY,SAAS;AAAA,QAClC,QAA6B,OAAO;AAAA,MACtC;AAAA;AAAA,IAED,mBAAmB,MAAG;AAAA,MAAG;AAAA;AAAA,IACzB,gBAAgB,MAAM;AAAA,EACvB;AAAA;",
8
- "debugId": "F6055B3989727FBE64756E2164756E21",
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;AAEtB;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,QAAS,CAAC;AAExC,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;AA8EF;;;;;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"}
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"}
@@ -1,8 +1,10 @@
1
1
  import {
2
- OBJECT_URL_TTL_MS2,
3
2
  downloadPdfFile2,
4
3
  printPdfFile2
5
- } from "../chunks/index-h3vjydz7.js";
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=A31116C0601A104A64756E2164756E21
16
+ //# debugId=ACA699EE49816B1764756E2164756E21
15
17
  //# sourceMappingURL=pdf-download.js.map
@@ -4,6 +4,6 @@
4
4
  "sourcesContent": [
5
5
  ],
6
6
  "mappings": "",
7
- "debugId": "A31116C0601A104A64756E2164756E21",
7
+ "debugId": "ACA699EE49816B1764756E2164756E21",
8
8
  "names": []
9
9
  }
@@ -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
+ }