@gyral/core 0.3.0 → 0.3.2
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 +57 -0
- package/NOTICE +19 -0
- package/README.md +36 -8
- package/dist/call.d.ts +32 -0
- package/dist/call.js +63 -0
- package/dist/call.js.map +1 -0
- package/dist/children.d.ts +25 -3
- package/dist/children.js +18 -1
- package/dist/children.js.map +1 -1
- package/dist/cli/gyral-types.d.ts +2 -0
- package/dist/cli/gyral-types.js +23 -0
- package/dist/cli/gyral-types.js.map +1 -0
- package/dist/command.d.ts +54 -12
- package/dist/command.js +25 -11
- package/dist/command.js.map +1 -1
- package/dist/compiler/ast.d.ts +2 -0
- package/dist/compiler/ast.js +5 -1
- package/dist/compiler/ast.js.map +1 -1
- package/dist/compiler/compile.d.ts +5 -2
- package/dist/compiler/compile.js +36 -17
- package/dist/compiler/compile.js.map +1 -1
- package/dist/compiler/component-entries.d.ts +7 -0
- package/dist/compiler/component-entries.js +42 -0
- package/dist/compiler/component-entries.js.map +1 -0
- package/dist/compiler/components.d.ts +4 -0
- package/dist/compiler/components.js +190 -0
- package/dist/compiler/components.js.map +1 -0
- package/dist/compiler/facts.d.ts +31 -0
- package/dist/compiler/facts.js +285 -0
- package/dist/compiler/facts.js.map +1 -0
- package/dist/compiler/features.d.ts +34 -0
- package/dist/compiler/features.js +158 -0
- package/dist/compiler/features.js.map +1 -0
- package/dist/compiler/hooks.d.ts +4 -0
- package/dist/compiler/hooks.js +144 -89
- package/dist/compiler/hooks.js.map +1 -1
- package/dist/compiler/loader-source.d.ts +14 -0
- package/dist/compiler/loader-source.js +86 -0
- package/dist/compiler/loader-source.js.map +1 -0
- package/dist/compiler/locate.d.ts +14 -0
- package/dist/compiler/locate.js +90 -0
- package/dist/compiler/locate.js.map +1 -0
- package/dist/compiler/parse5-check.d.ts +1 -1
- package/dist/compiler/parse5-check.js +8 -3
- package/dist/compiler/parse5-check.js.map +1 -1
- package/dist/compiler/prop-schemas.d.ts +4 -0
- package/dist/compiler/prop-schemas.js +108 -0
- package/dist/compiler/prop-schemas.js.map +1 -0
- package/dist/compiler/scan.d.ts +13 -3
- package/dist/compiler/scan.js +21 -13
- package/dist/compiler/scan.js.map +1 -1
- package/dist/compiler/scope.d.ts +8 -0
- package/dist/compiler/scope.js +116 -0
- package/dist/compiler/scope.js.map +1 -0
- package/dist/compiler/tag-map.d.ts +41 -0
- package/dist/compiler/tag-map.js +126 -0
- package/dist/compiler/tag-map.js.map +1 -0
- package/dist/components-ready.d.ts +7 -0
- package/dist/components-ready.js +19 -0
- package/dist/components-ready.js.map +1 -0
- package/dist/define.d.ts +12 -4
- package/dist/define.js +8 -12
- package/dist/define.js.map +1 -1
- package/dist/devtools-events.d.ts +8 -0
- package/dist/devtools-events.js.map +1 -1
- package/dist/devtools-off.d.ts +1 -0
- package/dist/devtools-off.js +1 -0
- package/dist/devtools-off.js.map +1 -1
- package/dist/devtools.d.ts +3 -0
- package/dist/devtools.js +14 -0
- package/dist/devtools.js.map +1 -1
- package/dist/drivers-scope.js.map +1 -1
- package/dist/element-internals.d.ts +1 -0
- package/dist/element-internals.js +11 -0
- package/dist/element-internals.js.map +1 -0
- package/dist/element-types.d.ts +7 -4
- package/dist/element-types.js.map +1 -1
- package/dist/element.js +80 -27
- package/dist/element.js.map +1 -1
- package/dist/error-view.d.ts +10 -0
- package/dist/error-view.js +30 -0
- package/dist/error-view.js.map +1 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +47 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.json +289 -0
- package/dist/eslint/imports.d.ts +2 -0
- package/dist/eslint/imports.js +2 -2
- package/dist/eslint/imports.js.map +1 -1
- package/dist/eslint/index.d.ts +2 -1
- package/dist/eslint/index.js +11 -3
- package/dist/eslint/index.js.map +1 -1
- package/dist/eslint/template-rule.d.ts +1 -0
- package/dist/eslint/template-rule.js +15 -5
- package/dist/eslint/template-rule.js.map +1 -1
- package/dist/eslint/unused-intent-rule.d.ts +2 -0
- package/dist/eslint/unused-intent-rule.js +186 -0
- package/dist/eslint/unused-intent-rule.js.map +1 -0
- package/dist/features.d.ts +2 -2
- package/dist/features.js.map +1 -1
- package/dist/focus.d.ts +6 -0
- package/dist/focus.js +32 -2
- package/dist/focus.js.map +1 -1
- package/dist/form-associated.d.ts +46 -0
- package/dist/form-associated.js +77 -0
- package/dist/form-associated.js.map +1 -0
- package/dist/forms.d.ts +42 -30
- package/dist/forms.js +85 -41
- package/dist/forms.js.map +1 -1
- package/dist/frame-lane.d.ts +11 -0
- package/dist/frame-lane.js +28 -0
- package/dist/frame-lane.js.map +1 -0
- package/dist/head.d.ts +61 -0
- package/dist/head.js +85 -0
- package/dist/head.js.map +1 -0
- package/dist/host-model.d.ts +34 -5
- package/dist/host-model.js +146 -23
- package/dist/host-model.js.map +1 -1
- package/dist/hydration-client.js +2 -2
- package/dist/hydration-client.js.map +1 -1
- package/dist/hydration-loader.d.ts +13 -0
- package/dist/hydration-loader.js +58 -0
- package/dist/hydration-loader.js.map +1 -0
- package/dist/hydration-off.d.ts +8 -0
- package/dist/hydration-off.js +25 -0
- package/dist/hydration-off.js.map +1 -0
- package/dist/hydration.d.ts +0 -12
- package/dist/hydration.js +3 -52
- package/dist/hydration.js.map +1 -1
- package/dist/index.d.ts +21 -24
- package/dist/index.js +10 -19
- package/dist/index.js.map +1 -1
- package/dist/indexeddb-types.d.ts +97 -0
- package/dist/indexeddb-types.js +2 -0
- package/dist/indexeddb-types.js.map +1 -0
- package/dist/indexeddb.d.ts +5 -0
- package/dist/indexeddb.js +118 -0
- package/dist/indexeddb.js.map +1 -0
- package/dist/intent.d.ts +42 -20
- package/dist/intent.js +145 -107
- package/dist/intent.js.map +1 -1
- package/dist/internal/indexeddb-open.d.ts +8 -0
- package/dist/internal/indexeddb-open.js +96 -0
- package/dist/internal/indexeddb-open.js.map +1 -0
- package/dist/internal/indexeddb-transaction.d.ts +4 -0
- package/dist/internal/indexeddb-transaction.js +134 -0
- package/dist/internal/indexeddb-transaction.js.map +1 -0
- package/dist/internal/interpreter.d.ts +12 -4
- package/dist/internal/interpreter.js +36 -48
- package/dist/internal/interpreter.js.map +1 -1
- package/dist/internal/token.d.ts +1 -0
- package/dist/internal/token.js +7 -0
- package/dist/internal/token.js.map +1 -0
- package/dist/internal.d.ts +20 -0
- package/dist/internal.js +22 -0
- package/dist/internal.js.map +1 -0
- package/dist/invoker-fallback-used.d.ts +4 -0
- package/dist/invoker-fallback-used.js +10 -0
- package/dist/invoker-fallback-used.js.map +1 -0
- package/dist/invoker-fallback.d.ts +4 -0
- package/dist/invoker-fallback.js +15 -0
- package/dist/invoker-fallback.js.map +1 -0
- package/dist/light-dom.d.ts +1 -1
- package/dist/light-dom.js.map +1 -1
- package/dist/prop.d.ts +30 -7
- package/dist/prop.js +19 -0
- package/dist/prop.js.map +1 -1
- package/dist/props.d.ts +8 -3
- package/dist/props.js +22 -15
- package/dist/props.js.map +1 -1
- package/dist/retry.d.ts +15 -0
- package/dist/retry.js +48 -0
- package/dist/retry.js.map +1 -0
- package/dist/scheduler.d.ts +7 -3
- package/dist/scheduler.js +38 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/script-json.d.ts +2 -0
- package/dist/script-json.js +13 -0
- package/dist/script-json.js.map +1 -0
- package/dist/server-component.d.ts +1 -1
- package/dist/server-component.js +45 -14
- package/dist/server-component.js.map +1 -1
- package/dist/server.d.ts +9 -3
- package/dist/server.js +12 -3
- package/dist/server.js.map +1 -1
- package/dist/settled.d.ts +4 -3
- package/dist/settled.js +48 -26
- package/dist/settled.js.map +1 -1
- package/dist/spec-features-used.d.ts +21 -0
- package/dist/spec-features-used.js +50 -0
- package/dist/spec-features-used.js.map +1 -0
- package/dist/spec-features.d.ts +28 -0
- package/dist/spec-features.js +42 -0
- package/dist/spec-features.js.map +1 -0
- package/dist/states.d.ts +3 -5
- package/dist/states.js +22 -8
- package/dist/states.js.map +1 -1
- package/dist/storage.d.ts +69 -0
- package/dist/storage.js +158 -0
- package/dist/storage.js.map +1 -0
- package/dist/store-binding.js +2 -2
- package/dist/store-binding.js.map +1 -1
- package/dist/store-scope.d.ts +2 -4
- package/dist/store-scope.js +6 -16
- package/dist/store-scope.js.map +1 -1
- package/dist/store.d.ts +2 -6
- package/dist/store.js +36 -8
- package/dist/store.js.map +1 -1
- package/dist/subscription.d.ts +35 -0
- package/dist/subscription.js +74 -0
- package/dist/subscription.js.map +1 -0
- package/dist/subscriptions.d.ts +26 -0
- package/dist/subscriptions.js +98 -0
- package/dist/subscriptions.js.map +1 -0
- package/dist/transitions.d.ts +4 -5
- package/dist/transitions.js +10 -4
- package/dist/transitions.js.map +1 -1
- package/dist/types.d.ts +141 -32
- package/dist/types.js.map +1 -1
- package/dist/use-dialogs.d.ts +2 -0
- package/dist/use-dialogs.js +9 -0
- package/dist/use-dialogs.js.map +1 -0
- package/dist/use-disposal.d.ts +2 -0
- package/dist/use-disposal.js +9 -0
- package/dist/use-disposal.js.map +1 -0
- package/dist/use-error-view.d.ts +2 -0
- package/dist/use-error-view.js +9 -0
- package/dist/use-error-view.js.map +1 -0
- package/dist/use-errored.d.ts +2 -0
- package/dist/use-errored.js +9 -0
- package/dist/use-errored.js.map +1 -0
- package/dist/use-form-associated.d.ts +2 -0
- package/dist/use-form-associated.js +7 -0
- package/dist/use-form-associated.js.map +1 -0
- package/dist/use-frame-lane.d.ts +2 -0
- package/dist/use-frame-lane.js +10 -0
- package/dist/use-frame-lane.js.map +1 -0
- package/dist/use-invokers.d.ts +2 -0
- package/dist/use-invokers.js +10 -0
- package/dist/use-invokers.js.map +1 -0
- package/dist/use-states.d.ts +2 -0
- package/dist/use-states.js +10 -0
- package/dist/use-states.js.map +1 -0
- package/dist/use-subscriptions.d.ts +2 -0
- package/dist/use-subscriptions.js +10 -0
- package/dist/use-subscriptions.js.map +1 -0
- package/dist/use-transitions.d.ts +2 -0
- package/dist/use-transitions.js +10 -0
- package/dist/use-transitions.js.map +1 -0
- package/dist/view/attributes.d.ts +5 -0
- package/dist/view/attributes.js +5 -0
- package/dist/view/attributes.js.map +1 -1
- package/dist/view/compiled.d.ts +1 -1
- package/dist/view/compiled.js +1 -1
- package/dist/view/compiled.js.map +1 -1
- package/dist/view/index.d.ts +9 -4
- package/dist/view/index.js +9 -3
- package/dist/view/index.js.map +1 -1
- package/dist/view/loc.d.ts +9 -0
- package/dist/view/loc.js +50 -0
- package/dist/view/loc.js.map +1 -0
- package/dist/view/message.d.ts +17 -0
- package/dist/view/message.js +40 -0
- package/dist/view/message.js.map +1 -0
- package/dist/view/messages.d.ts +43 -0
- package/dist/view/messages.js +84 -0
- package/dist/view/messages.js.map +1 -0
- package/dist/view/normalize/check.d.ts +5 -2
- package/dist/view/normalize/check.js +6 -3
- package/dist/view/normalize/check.js.map +1 -1
- package/dist/view/normalize/emit.d.ts +2 -0
- package/dist/view/normalize/emit.js +40 -11
- package/dist/view/normalize/emit.js.map +1 -1
- package/dist/view/normalize/errors.js +1 -1
- package/dist/view/normalize/errors.js.map +1 -1
- package/dist/view/normalize/id.d.ts +5 -2
- package/dist/view/normalize/id.js +9 -2
- package/dist/view/normalize/id.js.map +1 -1
- package/dist/view/normalize/normalize.d.ts +7 -4
- package/dist/view/normalize/normalize.js +15 -9
- package/dist/view/normalize/normalize.js.map +1 -1
- package/dist/view/normalize/rules.d.ts +9 -0
- package/dist/view/normalize/rules.js +35 -5
- package/dist/view/normalize/rules.js.map +1 -1
- package/dist/view/normalize/svg.d.ts +14 -0
- package/dist/view/normalize/svg.js +40 -0
- package/dist/view/normalize/svg.js.map +1 -0
- package/dist/view/normalize/tokens.d.ts +5 -1
- package/dist/view/normalize/tokens.js.map +1 -1
- package/dist/view/normalize/tree.d.ts +8 -2
- package/dist/view/normalize/tree.js +58 -13
- package/dist/view/normalize/tree.js.map +1 -1
- package/dist/view/normalize/types.d.ts +24 -4
- package/dist/view/normalize/types.js +4 -1
- package/dist/view/normalize/types.js.map +1 -1
- package/dist/view/normalize/whitespace.js +4 -3
- package/dist/view/normalize/whitespace.js.map +1 -1
- package/dist/view/prepare-stub.js +2 -2
- package/dist/view/prepare-stub.js.map +1 -1
- package/dist/view/prepare.d.ts +5 -2
- package/dist/view/prepare.js +9 -3
- package/dist/view/prepare.js.map +1 -1
- package/dist/view/registry.d.ts +15 -0
- package/dist/view/registry.js +10 -2
- package/dist/view/registry.js.map +1 -1
- package/dist/view/render/adopt-attr.js +7 -1
- package/dist/view/render/adopt-attr.js.map +1 -1
- package/dist/view/render/adopt.js +21 -4
- package/dist/view/render/adopt.js.map +1 -1
- package/dist/view/render/attr-parts.d.ts +12 -1
- package/dist/view/render/attr-parts.js +38 -5
- package/dist/view/render/attr-parts.js.map +1 -1
- package/dist/view/render/child-part.js +5 -2
- package/dist/view/render/child-part.js.map +1 -1
- package/dist/view/render/dispose.d.ts +3 -0
- package/dist/view/render/dispose.js +76 -0
- package/dist/view/render/dispose.js.map +1 -0
- package/dist/view/render/hook-part.d.ts +3 -1
- package/dist/view/render/hook-part.js +21 -5
- package/dist/view/render/hook-part.js.map +1 -1
- package/dist/view/render/hook-run.d.ts +2 -0
- package/dist/view/render/hook-run.js +72 -0
- package/dist/view/render/hook-run.js.map +1 -0
- package/dist/view/render/hooks.d.ts +74 -6
- package/dist/view/render/hooks.js +79 -17
- package/dist/view/render/hooks.js.map +1 -1
- package/dist/view/render/hydrate.js +2 -2
- package/dist/view/render/hydrate.js.map +1 -1
- package/dist/view/render/keyed.d.ts +6 -0
- package/dist/view/render/keyed.js +15 -0
- package/dist/view/render/keyed.js.map +1 -0
- package/dist/view/render/list.d.ts +1 -1
- package/dist/view/render/list.js.map +1 -1
- package/dist/view/render/mismatch.js +5 -3
- package/dist/view/render/mismatch.js.map +1 -1
- package/dist/view/render/modal.d.ts +3 -0
- package/dist/view/render/modal.js +39 -0
- package/dist/view/render/modal.js.map +1 -0
- package/dist/view/render/raw.d.ts +4 -1
- package/dist/view/render/raw.js +6 -2
- package/dist/view/render/raw.js.map +1 -1
- package/dist/view/render/render.d.ts +4 -1
- package/dist/view/render/render.js +17 -12
- package/dist/view/render/render.js.map +1 -1
- package/dist/view/render/toggles.d.ts +6 -0
- package/dist/view/render/toggles.js +6 -0
- package/dist/view/render/toggles.js.map +1 -0
- package/dist/view/render/values.d.ts +1 -1
- package/dist/view/render/values.js.map +1 -1
- package/dist/view/render/warn.d.ts +14 -0
- package/dist/view/render/warn.js +45 -0
- package/dist/view/render/warn.js.map +1 -1
- package/dist/view/server/component.d.ts +6 -3
- package/dist/view/server/component.js +29 -7
- package/dist/view/server/component.js.map +1 -1
- package/dist/view/server/index.d.ts +20 -0
- package/dist/view/server/index.js +12 -4
- package/dist/view/server/index.js.map +1 -1
- package/dist/view/server/tags.d.ts +13 -2
- package/dist/view/server/tags.js +13 -2
- package/dist/view/server/tags.js.map +1 -1
- package/dist/view/server/values.d.ts +2 -0
- package/dist/view/server/values.js +8 -0
- package/dist/view/server/values.js.map +1 -1
- package/dist/view/server/writer.d.ts +9 -4
- package/dist/view/server/writer.js +33 -18
- package/dist/view/server/writer.js.map +1 -1
- package/dist/view/template-element.d.ts +7 -0
- package/dist/view/template-element.js +25 -5
- package/dist/view/template-element.js.map +1 -1
- package/dist/view/template.d.ts +9 -2
- package/dist/view/template.js +61 -8
- package/dist/view/template.js.map +1 -1
- package/dist/view/trusted-html.d.ts +6 -0
- package/dist/view/trusted-html.js +34 -0
- package/dist/view/trusted-html.js.map +1 -0
- package/dist/vite.d.ts +38 -31
- package/dist/vite.js +206 -31
- package/dist/vite.js.map +1 -1
- package/package.json +34 -3
- package/dist/hooks/labelled-by.d.ts +0 -13
- package/dist/hooks/labelled-by.js +0 -51
- package/dist/hooks/labelled-by.js.map +0 -1
- package/dist/random.d.ts +0 -13
- package/dist/random.js +0 -19
- package/dist/random.js.map +0 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { findVariable, importedAs, isGyral, OPTIONS_SCHEMA, } from './imports.js';
|
|
2
|
+
/**
|
|
3
|
+
* A static `data-intent` or `data-intent-<event>` value in template text (not `data-intent-on`,
|
|
4
|
+
* whose value is events).
|
|
5
|
+
*/
|
|
6
|
+
const STATIC_INTENT = /\sdata-intent(?:-(?!on=)[\w-]+)?=["']?([^"'\s>]+)/g;
|
|
7
|
+
/** Template text that ends where a `data-intent` or `data-intent-<event>` hole starts. */
|
|
8
|
+
const INTENT_HOLE = /\sdata-intent(?:-(?!on=)[\w-]+)?=["']?$/;
|
|
9
|
+
const isFn = (node) => node?.type === 'ArrowFunctionExpression' ||
|
|
10
|
+
node?.type === 'FunctionExpression' ||
|
|
11
|
+
node?.type === 'FunctionDeclaration';
|
|
12
|
+
const keyName = (key) => key.type === 'Identifier'
|
|
13
|
+
? key.name
|
|
14
|
+
: key.type === 'Literal' && typeof key.value === 'string'
|
|
15
|
+
? key.value
|
|
16
|
+
: undefined;
|
|
17
|
+
/** `{ name: value }` entries of an object literal, without spreads or computed keys. */
|
|
18
|
+
function entries(object) {
|
|
19
|
+
if (object.type !== 'ObjectExpression')
|
|
20
|
+
return [];
|
|
21
|
+
return object.properties.flatMap((p) => {
|
|
22
|
+
if (p.type !== 'Property' || p.computed)
|
|
23
|
+
return [];
|
|
24
|
+
const name = keyName(p.key);
|
|
25
|
+
return name === undefined ? [] : [{ name, key: p.key, value: p.value }];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Whether a variable is bound in this module (not imported, not a global). */
|
|
29
|
+
const isLocal = (variable) => variable !== undefined && variable.defs.length > 0 && variable.defs[0]?.type !== 'ImportBinding';
|
|
30
|
+
/** The function a view is: inline, or a module binding to one; undefined when elsewhere. */
|
|
31
|
+
function viewFunction(context, view) {
|
|
32
|
+
if (isFn(view))
|
|
33
|
+
return view;
|
|
34
|
+
if (view.type !== 'Identifier')
|
|
35
|
+
return undefined;
|
|
36
|
+
const variable = findVariable(context.sourceCode.getScope(view), view.name);
|
|
37
|
+
const def = variable?.defs[0];
|
|
38
|
+
if (def?.type === 'FunctionName')
|
|
39
|
+
return def.node;
|
|
40
|
+
const init = def?.type === 'Variable' ? def.node.init : null;
|
|
41
|
+
return isFn(init) ? init : undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Whether `tag` is Gyral's `html` or `svg` (an svg fragment can name intents too). */
|
|
44
|
+
const isTemplateTag = (context, tag) => isGyral(context, tag, 'html') || isGyral(context, tag, 'svg');
|
|
45
|
+
/** Whether `node` sits in a hole of a Gyral html or svg template. */
|
|
46
|
+
function inTemplateHole(context, node) {
|
|
47
|
+
for (let n = node; n !== null; n = n.parent) {
|
|
48
|
+
const parent = n.parent;
|
|
49
|
+
if (parent?.type !== 'TemplateLiteral' || parent.parent.type !== 'TaggedTemplateExpression') {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (isTemplateTag(context, parent.parent.tag))
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Whether a template in the module calls, or passes on, a function imported from a module that
|
|
59
|
+
* isn't a template source: its markup, which this rule can't see, may name intents.
|
|
60
|
+
*/
|
|
61
|
+
function templatesCallImports(context, program) {
|
|
62
|
+
const global = context.sourceCode.getScope(program);
|
|
63
|
+
const module = global.childScopes.find((scope) => scope.type === 'module') ?? global;
|
|
64
|
+
return module.variables.some((v) => {
|
|
65
|
+
const def = v.defs[0];
|
|
66
|
+
if (def?.type !== 'ImportBinding')
|
|
67
|
+
return false;
|
|
68
|
+
if (importedAs(context, def.name) !== undefined)
|
|
69
|
+
return false; // core
|
|
70
|
+
return v.references.some((r) => {
|
|
71
|
+
const id = r.identifier;
|
|
72
|
+
return id.parent?.type === 'CallExpression' && inTemplateHole(context, id);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** Whether `id`'s reference stays in the module: `id.Name`, or an argument to a local function. */
|
|
77
|
+
function staysLocal(context, id) {
|
|
78
|
+
const parent = id.parent;
|
|
79
|
+
if (parent === null)
|
|
80
|
+
return false;
|
|
81
|
+
if (parent.type === 'MemberExpression' && parent.object === id)
|
|
82
|
+
return true;
|
|
83
|
+
if (parent.type !== 'CallExpression' || parent.callee === id)
|
|
84
|
+
return false;
|
|
85
|
+
const callee = parent.callee;
|
|
86
|
+
if (callee.type !== 'Identifier')
|
|
87
|
+
return false;
|
|
88
|
+
return isLocal(findVariable(context.sourceCode.getScope(callee), callee.name));
|
|
89
|
+
}
|
|
90
|
+
/** Whether the view's intents parameter (its second) can reach code outside the module. */
|
|
91
|
+
function viewEscapes(context, fn) {
|
|
92
|
+
const param = fn.params[1];
|
|
93
|
+
if (param?.type !== 'Identifier')
|
|
94
|
+
return false; // absent, or destructured (counted as used)
|
|
95
|
+
const variable = context.sourceCode.getDeclaredVariables(fn).find((v) => v.name === param.name);
|
|
96
|
+
return (variable?.references ?? []).some((r) => !staysLocal(context, r.identifier));
|
|
97
|
+
}
|
|
98
|
+
export const unusedIntentRule = {
|
|
99
|
+
meta: {
|
|
100
|
+
type: 'suggestion',
|
|
101
|
+
docs: {
|
|
102
|
+
description: 'An intent parser that no template in the module names with data-intent: a renamed ' +
|
|
103
|
+
'intent or dead code (view/09-template-rules.md "Warnings").',
|
|
104
|
+
recommended: true,
|
|
105
|
+
url: 'https://github.com/gyraljs/gyral/blob/main/docs/design-docs/view/09-template-rules.md',
|
|
106
|
+
},
|
|
107
|
+
schema: OPTIONS_SCHEMA,
|
|
108
|
+
messages: {
|
|
109
|
+
unused: 'The intent parser "{{name}}" is never named by data-intent in this module, so it never ' +
|
|
110
|
+
'runs: name it in the view (data-intent=${i.{{name}}}), or remove the parser (messages ' +
|
|
111
|
+
'that only come from commands need none) (view/09-template-rules.md "Warnings").',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
create(context) {
|
|
115
|
+
const used = new Set();
|
|
116
|
+
const components = [];
|
|
117
|
+
let exportsIntents = false;
|
|
118
|
+
return {
|
|
119
|
+
MemberExpression(node) {
|
|
120
|
+
const name = node.computed ? keyName(node.property) : undefined;
|
|
121
|
+
if (!node.computed && node.property.type === 'Identifier')
|
|
122
|
+
used.add(node.property.name);
|
|
123
|
+
else if (name !== undefined)
|
|
124
|
+
used.add(name);
|
|
125
|
+
},
|
|
126
|
+
ObjectPattern(node) {
|
|
127
|
+
for (const p of node.properties) {
|
|
128
|
+
const name = p.type === 'Property' && !p.computed ? keyName(p.key) : undefined;
|
|
129
|
+
if (name !== undefined)
|
|
130
|
+
used.add(name);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
TaggedTemplateExpression(node) {
|
|
134
|
+
if (!isTemplateTag(context, node.tag))
|
|
135
|
+
return;
|
|
136
|
+
const { quasis, expressions } = node.quasi;
|
|
137
|
+
quasis.forEach((q, k) => {
|
|
138
|
+
const text = q.value.cooked ?? '';
|
|
139
|
+
for (const m of text.matchAll(STATIC_INTENT))
|
|
140
|
+
used.add(m[1] ?? '');
|
|
141
|
+
const value = expressions[k];
|
|
142
|
+
if (INTENT_HOLE.test(text) && value?.type === 'Literal')
|
|
143
|
+
used.add(String(value.value));
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
CallExpression(node) {
|
|
147
|
+
const callee = node.callee;
|
|
148
|
+
if (isGyral(context, callee, 'intentsOf')) {
|
|
149
|
+
// `export const i = intentsOf<typeof C>()`: other modules may name the intents.
|
|
150
|
+
const declaration = node.parent.parent?.parent;
|
|
151
|
+
if (declaration?.type === 'ExportNamedDeclaration')
|
|
152
|
+
exportsIntents = true;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// `define<S, M>()(tag, spec)`: the spec is the second argument of the second call.
|
|
156
|
+
if (callee.type !== 'CallExpression' ||
|
|
157
|
+
!isGyral(context, callee.callee, 'define')) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const spec = node.arguments[1];
|
|
161
|
+
const fields = spec === undefined ? [] : entries(spec);
|
|
162
|
+
const intent = fields.find((f) => f.name === 'intent')?.value;
|
|
163
|
+
const view = fields.find((f) => f.name === 'view')?.value;
|
|
164
|
+
if (intent === undefined)
|
|
165
|
+
return;
|
|
166
|
+
const parsers = entries(intent).map((e) => ({ name: e.name, node: e.key }));
|
|
167
|
+
components.push({ parsers, view: view && viewFunction(context, view) });
|
|
168
|
+
},
|
|
169
|
+
'Program:exit'(program) {
|
|
170
|
+
if (exportsIntents || components.length === 0)
|
|
171
|
+
return;
|
|
172
|
+
if (templatesCallImports(context, program))
|
|
173
|
+
return;
|
|
174
|
+
for (const { parsers, view } of components) {
|
|
175
|
+
if (view === undefined || viewEscapes(context, view))
|
|
176
|
+
continue;
|
|
177
|
+
for (const { name, node } of parsers) {
|
|
178
|
+
if (!used.has(name))
|
|
179
|
+
context.report({ node, messageId: 'unused', data: { name } });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
//# sourceMappingURL=unused-intent-rule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unused-intent-rule.js","sourceRoot":"","sources":["../../src/eslint/unused-intent-rule.ts"],"names":[],"mappings":"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","sourcesContent":["// gyral/unused-intent: the \"unused intent parser\" warning of view/09-template-rules.md. A parser\n// in `define<S, M>()(tag, { intent: { Name: … } })` runs only when an element names it with\n// `data-intent`, so a parser no template in the module names is a renamed intent, dead code, or\n// (since intent names are inferred from these keys, ADR 0023) a misspelled key.\n// Static, over the whole module, so intents rendered conditionally (a dialog's buttons, an\n// error state) count as used; that is why the warning lives here and not in the runtime, which\n// sees only the templates rendered so far. A name counts as used when the module mentions it as\n// a property (`i.Name`, `{ Name } = i`, `i['Name']`) or as a static or literal `data-intent` or\n// `data-intent-<event>` value in an html or svg template. A component is skipped when its intent\n// names may be used elsewhere: its view is not in the module (an imported function), the view\n// hands its intents to an imported function, the module exports an `intentsOf()` constant, or a\n// template in the module calls (or passes on) a function imported from another module, which\n// may render `data-intent=\"Name\"` itself (a shared table header, a row module).\nimport type { Rule, Scope } from 'eslint';\nimport {\n findVariable,\n importedAs,\n isGyral,\n OPTIONS_SCHEMA,\n type Node,\n type NodeOf,\n} from './imports.js';\n\n/**\n * A static `data-intent` or `data-intent-<event>` value in template text (not `data-intent-on`,\n * whose value is events).\n */\nconst STATIC_INTENT = /\\sdata-intent(?:-(?!on=)[\\w-]+)?=[\"']?([^\"'\\s>]+)/g;\n/** Template text that ends where a `data-intent` or `data-intent-<event>` hole starts. */\nconst INTENT_HOLE = /\\sdata-intent(?:-(?!on=)[\\w-]+)?=[\"']?$/;\n\ninterface Component {\n readonly parsers: readonly { readonly name: string; readonly node: Node }[];\n readonly view: Fn | undefined;\n}\n\ntype Fn =\n NodeOf<'ArrowFunctionExpression'> | NodeOf<'FunctionExpression'> | NodeOf<'FunctionDeclaration'>;\n\nconst isFn = (node: Node | null | undefined): node is Fn =>\n node?.type === 'ArrowFunctionExpression' ||\n node?.type === 'FunctionExpression' ||\n node?.type === 'FunctionDeclaration';\n\nconst keyName = (key: Node): string | undefined =>\n key.type === 'Identifier'\n ? key.name\n : key.type === 'Literal' && typeof key.value === 'string'\n ? key.value\n : undefined;\n\n/** `{ name: value }` entries of an object literal, without spreads or computed keys. */\nfunction entries(object: Node): { name: string; key: Node; value: Node }[] {\n if (object.type !== 'ObjectExpression') return [];\n return object.properties.flatMap((p) => {\n if (p.type !== 'Property' || p.computed) return [];\n const name = keyName(p.key as Node);\n return name === undefined ? [] : [{ name, key: p.key as Node, value: p.value as Node }];\n });\n}\n\n/** Whether a variable is bound in this module (not imported, not a global). */\nconst isLocal = (variable: Scope.Variable | undefined): boolean =>\n variable !== undefined && variable.defs.length > 0 && variable.defs[0]?.type !== 'ImportBinding';\n\n/** The function a view is: inline, or a module binding to one; undefined when elsewhere. */\nfunction viewFunction(context: Rule.RuleContext, view: Node): Fn | undefined {\n if (isFn(view)) return view;\n if (view.type !== 'Identifier') return undefined;\n const variable = findVariable(context.sourceCode.getScope(view), view.name);\n const def = variable?.defs[0];\n if (def?.type === 'FunctionName') return def.node as Fn;\n const init = def?.type === 'Variable' ? (def.node.init as Node | null) : null;\n return isFn(init) ? init : undefined;\n}\n\n/** Whether `tag` is Gyral's `html` or `svg` (an svg fragment can name intents too). */\nconst isTemplateTag = (context: Rule.RuleContext, tag: Node): boolean =>\n isGyral(context, tag, 'html') || isGyral(context, tag, 'svg');\n\n/** Whether `node` sits in a hole of a Gyral html or svg template. */\nfunction inTemplateHole(context: Rule.RuleContext, node: Node): boolean {\n for (let n: Node | null = node; n !== null; n = n.parent) {\n const parent = n.parent;\n if (parent?.type !== 'TemplateLiteral' || parent.parent.type !== 'TaggedTemplateExpression') {\n continue;\n }\n if (isTemplateTag(context, parent.parent.tag as Node)) return true;\n }\n return false;\n}\n\n/**\n * Whether a template in the module calls, or passes on, a function imported from a module that\n * isn't a template source: its markup, which this rule can't see, may name intents.\n */\nfunction templatesCallImports(context: Rule.RuleContext, program: Node): boolean {\n const global = context.sourceCode.getScope(program);\n const module = global.childScopes.find((scope) => scope.type === 'module') ?? global;\n return module.variables.some((v) => {\n const def = v.defs[0];\n if (def?.type !== 'ImportBinding') return false;\n if (importedAs(context, def.name as NodeOf<'Identifier'>) !== undefined) return false; // core\n return v.references.some((r) => {\n const id = r.identifier as Node;\n return id.parent?.type === 'CallExpression' && inTemplateHole(context, id);\n });\n });\n}\n\n/** Whether `id`'s reference stays in the module: `id.Name`, or an argument to a local function. */\nfunction staysLocal(context: Rule.RuleContext, id: Node): boolean {\n const parent = id.parent;\n if (parent === null) return false;\n if (parent.type === 'MemberExpression' && parent.object === id) return true;\n if (parent.type !== 'CallExpression' || parent.callee === id) return false;\n const callee = parent.callee as Node;\n if (callee.type !== 'Identifier') return false;\n return isLocal(findVariable(context.sourceCode.getScope(callee), callee.name));\n}\n\n/** Whether the view's intents parameter (its second) can reach code outside the module. */\nfunction viewEscapes(context: Rule.RuleContext, fn: Fn): boolean {\n const param = fn.params[1] as Node | undefined;\n if (param?.type !== 'Identifier') return false; // absent, or destructured (counted as used)\n const variable = context.sourceCode.getDeclaredVariables(fn).find((v) => v.name === param.name);\n return (variable?.references ?? []).some((r) => !staysLocal(context, r.identifier as Node));\n}\n\nexport const unusedIntentRule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'An intent parser that no template in the module names with data-intent: a renamed ' +\n 'intent or dead code (view/09-template-rules.md \"Warnings\").',\n recommended: true,\n url: 'https://github.com/gyraljs/gyral/blob/main/docs/design-docs/view/09-template-rules.md',\n },\n schema: OPTIONS_SCHEMA,\n messages: {\n unused:\n 'The intent parser \"{{name}}\" is never named by data-intent in this module, so it never ' +\n 'runs: name it in the view (data-intent=${i.{{name}}}), or remove the parser (messages ' +\n 'that only come from commands need none) (view/09-template-rules.md \"Warnings\").',\n },\n },\n create(context) {\n const used = new Set<string>();\n const components: Component[] = [];\n let exportsIntents = false;\n return {\n MemberExpression(node) {\n const name = node.computed ? keyName(node.property as Node) : undefined;\n if (!node.computed && node.property.type === 'Identifier') used.add(node.property.name);\n else if (name !== undefined) used.add(name);\n },\n ObjectPattern(node) {\n for (const p of node.properties) {\n const name = p.type === 'Property' && !p.computed ? keyName(p.key as Node) : undefined;\n if (name !== undefined) used.add(name);\n }\n },\n TaggedTemplateExpression(node) {\n if (!isTemplateTag(context, node.tag as Node)) return;\n const { quasis, expressions } = node.quasi;\n quasis.forEach((q, k) => {\n const text = q.value.cooked ?? '';\n for (const m of text.matchAll(STATIC_INTENT)) used.add(m[1] ?? '');\n const value = expressions[k] as Node | undefined;\n if (INTENT_HOLE.test(text) && value?.type === 'Literal') used.add(String(value.value));\n });\n },\n CallExpression(node) {\n const callee = node.callee as Node;\n if (isGyral(context, callee, 'intentsOf')) {\n // `export const i = intentsOf<typeof C>()`: other modules may name the intents.\n const declaration = node.parent.parent?.parent;\n if (declaration?.type === 'ExportNamedDeclaration') exportsIntents = true;\n return;\n }\n // `define<S, M>()(tag, spec)`: the spec is the second argument of the second call.\n if (\n callee.type !== 'CallExpression' ||\n !isGyral(context, callee.callee as Node, 'define')\n ) {\n return;\n }\n const spec = node.arguments[1] as Node | undefined;\n const fields = spec === undefined ? [] : entries(spec);\n const intent = fields.find((f) => f.name === 'intent')?.value;\n const view = fields.find((f) => f.name === 'view')?.value;\n if (intent === undefined) return;\n const parsers = entries(intent).map((e) => ({ name: e.name, node: e.key }));\n components.push({ parsers, view: view && viewFunction(context, view) });\n },\n 'Program:exit'(program) {\n if (exportsIntents || components.length === 0) return;\n if (templatesCallImports(context, program as Node)) return;\n for (const { parsers, view } of components) {\n if (view === undefined || viewEscapes(context, view)) continue;\n for (const { name, node } of parsers) {\n if (!used.has(name)) context.report({ node, messageId: 'unused', data: { name } });\n }\n }\n },\n };\n },\n};\n"]}
|
package/dist/features.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DriverOverrides } from './command.js';
|
|
2
|
-
import type { Interpreter } from './internal/interpreter.js';
|
|
2
|
+
import type { CommandFailure, Interpreter } from './internal/interpreter.js';
|
|
3
3
|
import type { PropFeature } from './props.js';
|
|
4
4
|
import type { AnyStore, StoreOverrides, StoreRef, StoreSendInput } from './store.js';
|
|
5
5
|
import type { CommandTrace } from './devtools-events.js';
|
|
@@ -30,7 +30,7 @@ export interface LocalDriver {
|
|
|
30
30
|
type Msg = Tagged | IntentRejected;
|
|
31
31
|
export declare const features: {
|
|
32
32
|
/** Set by `command()`: a host's command interpreter, resolving drivers by name (ADR 0006). */
|
|
33
|
-
commands?: (el: FeatureHost,
|
|
33
|
+
commands?: (el: FeatureHost, dispatch: (msg: Msg) => void, trace: CommandTrace | undefined, failed: CommandFailure) => Interpreter<Msg>;
|
|
34
34
|
/** Set by `defineStore()`: binds a host to the stores it declares (ADR 0013). */
|
|
35
35
|
stores?: (el: FeatureHost, tag: string, declared: readonly AnyStore[], onChange: (store: AnyStore, state: unknown, prev: unknown) => void) => StoreLink;
|
|
36
36
|
/** Set by the prop builders: attribute parsing and validation (view/05 "Props"). */
|
package/dist/features.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features.js","sourceRoot":"","sources":["../src/features.ts"],"names":[],"mappings":"AAsCA,MAAM,CAAC,MAAM,QAAQ,GAiBjB,EAAE,CAAC","sourcesContent":["// Features that register themselves when their API is used (ADR 0018 \"Size\", gyral-g1r.18).\n// Core reaches them only through these slots, which the feature's own API fills when called:\n// `command()` the interpreter, `defineStore()` the store binding, a prop builder the prop\n// machinery. An app that never calls one doesn't bundle what it registers. No module does this\n// at load time: bundlers keep `sideEffects: false` and tree-shake as usual.\nimport type { DriverOverrides } from './command.js';\nimport type { Interpreter } from './internal/interpreter.js';\nimport type { PropFeature } from './props.js';\nimport type { AnyStore, StoreOverrides, StoreRef, StoreSendInput } from './store.js';\nimport type { CommandTrace } from './devtools-events.js';\nimport type { IntentRejected, Tagged } from './types.js';\n\n/** A host's element, as features see it. */\nexport type FeatureHost = HTMLElement & { drivers: DriverOverrides; stores: StoreOverrides };\n\n/** What a component's store binding offers its model (store-binding.ts). */\nexport interface StoreLink {\n read<S>(ref: StoreRef<S>): S;\n send(input: StoreSendInput): void;\n connect(): boolean;\n disconnect(): void;\n}\n\n/** The host a marker driver's `local` handler runs against (focus, emit, store send). */\nexport interface LocalHost {\n readonly el: FeatureHost;\n readonly tag: string;\n readonly root: () => ParentNode | undefined;\n readonly stores: () => StoreLink;\n}\n\n/** A marker driver core runs itself, synchronously, without the interpreter. */\nexport interface LocalDriver {\n readonly local: (host: LocalHost, input: unknown) => void;\n}\n\ntype Msg = Tagged | IntentRejected;\n\nexport const features: {\n /** Set by `command()`: a host's command interpreter, resolving drivers by name (ADR 0006). */\n commands?: (\n el: FeatureHost,\n
|
|
1
|
+
{"version":3,"file":"features.js","sourceRoot":"","sources":["../src/features.ts"],"names":[],"mappings":"AAsCA,MAAM,CAAC,MAAM,QAAQ,GAiBjB,EAAE,CAAC","sourcesContent":["// Features that register themselves when their API is used (ADR 0018 \"Size\", gyral-g1r.18).\n// Core reaches them only through these slots, which the feature's own API fills when called:\n// `command()` the interpreter, `defineStore()` the store binding, a prop builder the prop\n// machinery. An app that never calls one doesn't bundle what it registers. No module does this\n// at load time: bundlers keep `sideEffects: false` and tree-shake as usual.\nimport type { DriverOverrides } from './command.js';\nimport type { CommandFailure, Interpreter } from './internal/interpreter.js';\nimport type { PropFeature } from './props.js';\nimport type { AnyStore, StoreOverrides, StoreRef, StoreSendInput } from './store.js';\nimport type { CommandTrace } from './devtools-events.js';\nimport type { IntentRejected, Tagged } from './types.js';\n\n/** A host's element, as features see it. */\nexport type FeatureHost = HTMLElement & { drivers: DriverOverrides; stores: StoreOverrides };\n\n/** What a component's store binding offers its model (store-binding.ts). */\nexport interface StoreLink {\n read<S>(ref: StoreRef<S>): S;\n send(input: StoreSendInput): void;\n connect(): boolean;\n disconnect(): void;\n}\n\n/** The host a marker driver's `local` handler runs against (focus, emit, store send). */\nexport interface LocalHost {\n readonly el: FeatureHost;\n readonly tag: string;\n readonly root: () => ParentNode | undefined;\n readonly stores: () => StoreLink;\n}\n\n/** A marker driver core runs itself, synchronously, without the interpreter. */\nexport interface LocalDriver {\n readonly local: (host: LocalHost, input: unknown) => void;\n}\n\ntype Msg = Tagged | IntentRejected;\n\nexport const features: {\n /** Set by `command()`: a host's command interpreter, resolving drivers by name (ADR 0006). */\n commands?: (\n el: FeatureHost,\n dispatch: (msg: Msg) => void,\n trace: CommandTrace | undefined,\n failed: CommandFailure,\n ) => Interpreter<Msg>;\n /** Set by `defineStore()`: binds a host to the stores it declares (ADR 0013). */\n stores?: (\n el: FeatureHost,\n tag: string,\n declared: readonly AnyStore[],\n onChange: (store: AnyStore, state: unknown, prev: unknown) => void,\n ) => StoreLink;\n /** Set by the prop builders: attribute parsing and validation (view/05 \"Props\"). */\n props?: PropFeature;\n} = {};\n"]}
|
package/dist/focus.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export interface FocusOptions {
|
|
|
11
11
|
readonly preventScroll?: boolean;
|
|
12
12
|
/** Also select the text of an input or textarea. */
|
|
13
13
|
readonly select?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* If nothing matches after this render, keep the request and focus the target as soon as a
|
|
16
|
+
* later render of this component produces it; a newer `focus()` from the component replaces
|
|
17
|
+
* it, and after one second it gives up with the usual warning. `settled()` doesn't wait for it.
|
|
18
|
+
*/
|
|
19
|
+
readonly wait?: boolean;
|
|
14
20
|
}
|
|
15
21
|
/** The input of a `focus()` command. */
|
|
16
22
|
export interface FocusInput extends FocusOptions {
|
package/dist/focus.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterRender, POST_FOCUS } from './scheduler.js';
|
|
2
|
+
import { message } from './view/index.js';
|
|
2
3
|
/** Marker driver: the host runs focus commands itself (`local`). */
|
|
3
4
|
export const FOCUS = {
|
|
4
5
|
name: '@gyral/focus',
|
|
@@ -7,6 +8,10 @@ export const FOCUS = {
|
|
|
7
8
|
queueFocus(host.el, host.root, host.tag, input);
|
|
8
9
|
},
|
|
9
10
|
};
|
|
11
|
+
// How long `focus(selector, { wait: true })` waits for its target to appear.
|
|
12
|
+
const FOCUS_WAIT_MS = 1000;
|
|
13
|
+
// A component's waiting focus request, so the next focus() can cancel it.
|
|
14
|
+
const waiting = new WeakMap();
|
|
10
15
|
/**
|
|
11
16
|
* A command that focuses the first element matching `selector` inside this component (its
|
|
12
17
|
* shadow root, or its own children in light-DOM mode) once the current update has rendered:
|
|
@@ -23,7 +28,7 @@ export function focus(selector, options = {}) {
|
|
|
23
28
|
export function runFocus(root, tag, input) {
|
|
24
29
|
const target = root.querySelector(input.selector);
|
|
25
30
|
if (!(target instanceof HTMLElement || target instanceof SVGElement)) {
|
|
26
|
-
console.warn(
|
|
31
|
+
console.warn(message(13, tag, input.selector));
|
|
27
32
|
return;
|
|
28
33
|
}
|
|
29
34
|
target.focus({ preventScroll: input.preventScroll ?? false });
|
|
@@ -37,9 +42,34 @@ export function runFocus(root, tag, input) {
|
|
|
37
42
|
*/
|
|
38
43
|
export function queueFocus(host, root, tag, input) {
|
|
39
44
|
afterRender(POST_FOCUS, () => {
|
|
45
|
+
waiting.get(host)?.();
|
|
40
46
|
const target = root();
|
|
41
|
-
if (host.isConnected
|
|
47
|
+
if (!host.isConnected || target === undefined)
|
|
48
|
+
return;
|
|
49
|
+
if (input.wait !== true || target.querySelector(input.selector) !== null) {
|
|
42
50
|
runFocus(target, tag, input);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Not rendered yet: watch the component's own DOM (renders are the only writers) until the
|
|
54
|
+
// target appears, a newer focus() cancels, or time runs out. settled() doesn't wait for it.
|
|
55
|
+
const stop = () => {
|
|
56
|
+
observer.disconnect();
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
waiting.delete(host);
|
|
59
|
+
};
|
|
60
|
+
const observer = new MutationObserver(() => {
|
|
61
|
+
if (target.querySelector(input.selector) !== null) {
|
|
62
|
+
stop();
|
|
63
|
+
if (host.isConnected)
|
|
64
|
+
runFocus(target, tag, input);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
stop();
|
|
69
|
+
runFocus(target, tag, input);
|
|
70
|
+
}, FOCUS_WAIT_MS);
|
|
71
|
+
observer.observe(target, { childList: true, subtree: true, attributes: true });
|
|
72
|
+
waiting.set(host, stop);
|
|
43
73
|
});
|
|
44
74
|
}
|
|
45
75
|
//# sourceMappingURL=focus.js.map
|
package/dist/focus.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"focus.js","sourceRoot":"","sources":["../src/focus.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"focus.js","sourceRoot":"","sources":["../src/focus.ts"],"names":[],"mappings":"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","sourcesContent":["// Focus management from the model (gyral-czi.28). Moving focus is a side effect, so the model\n// asks for it with a command; define() queues it as the scheduler's first post-render work\n// (view/04-scheduler.md \"Post-render queue\"), against the component's own root, so the element\n// to focus already exists.\nimport type { Command } from './command.js';\nimport type { LocalHost } from './features.js';\nimport { afterRender, POST_FOCUS } from './scheduler.js';\nimport { message } from './view/index.js';\n\n/** Marker driver: the host runs focus commands itself (`local`). */\nexport const FOCUS = {\n name: '@gyral/focus',\n run: () => undefined,\n local: (host: LocalHost, input: unknown) => {\n queueFocus(host.el, host.root, host.tag, input as FocusInput);\n },\n} as const;\n\nexport interface FocusOptions {\n /** Don't scroll the element into view (default `false`: the browser scrolls if needed). */\n readonly preventScroll?: boolean;\n /** Also select the text of an input or textarea. */\n readonly select?: boolean;\n /**\n * If nothing matches after this render, keep the request and focus the target as soon as a\n * later render of this component produces it; a newer `focus()` from the component replaces\n * it, and after one second it gives up with the usual warning. `settled()` doesn't wait for it.\n */\n readonly wait?: boolean;\n}\n\n// How long `focus(selector, { wait: true })` waits for its target to appear.\nconst FOCUS_WAIT_MS = 1000;\n\n// A component's waiting focus request, so the next focus() can cancel it.\nconst waiting = new WeakMap<Element, () => void>();\n\n/** The input of a `focus()` command. */\nexport interface FocusInput extends FocusOptions {\n readonly selector: string;\n}\n\n/**\n * A command that focuses the first element matching `selector` inside this component (its\n * shadow root, or its own children in light-DOM mode) once the current update has rendered:\n *\n * NextPage: (s, m) => [{ ...s, page: m.page }, [focus('h2', { preventScroll: true })]]\n *\n * Non-focusable targets (a heading) need `tabindex=\"-1\"` in the view.\n */\nexport function focus(selector: string, options: FocusOptions = {}): Command<never> {\n const input: FocusInput = { selector, ...options };\n return { driver: FOCUS, input, onSuccess: () => undefined };\n}\n\n/** Runs a focus command against `root` (the scheduler calls it after the render). */\nexport function runFocus(root: ParentNode, tag: string, input: FocusInput): void {\n const target = root.querySelector(input.selector);\n if (!(target instanceof HTMLElement || target instanceof SVGElement)) {\n console.warn(message(13, tag, input.selector));\n return;\n }\n target.focus({ preventScroll: input.preventScroll ?? false });\n if (input.select === true && 'select' in target && typeof target.select === 'function') {\n (target as HTMLInputElement).select();\n }\n}\n\n/**\n * Runs a focus command after the flush that renders the same update, view transition\n * included: it is post-render work, so the DOM already shows the new state.\n */\nexport function queueFocus(\n host: Element,\n root: () => ParentNode | undefined,\n tag: string,\n input: FocusInput,\n): void {\n afterRender(POST_FOCUS, () => {\n waiting.get(host)?.();\n const target = root();\n if (!host.isConnected || target === undefined) return;\n if (input.wait !== true || target.querySelector(input.selector) !== null) {\n runFocus(target, tag, input);\n return;\n }\n // Not rendered yet: watch the component's own DOM (renders are the only writers) until the\n // target appears, a newer focus() cancels, or time runs out. settled() doesn't wait for it.\n const stop = (): void => {\n observer.disconnect();\n clearTimeout(timer);\n waiting.delete(host);\n };\n const observer = new MutationObserver(() => {\n if (target.querySelector(input.selector) !== null) {\n stop();\n if (host.isConnected) runFocus(target, tag, input);\n }\n });\n const timer = setTimeout(() => {\n stop();\n runFocus(target, tag, input);\n }, FOCUS_WAIT_MS);\n observer.observe(target, { childList: true, subtree: true, attributes: true });\n waiting.set(host, stop);\n });\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Ctx } from './types.js';
|
|
2
|
+
import type { HostModel } from './host-model.js';
|
|
3
|
+
/** A native submitted value, or null to omit this component from the submission. */
|
|
4
|
+
export type FormControlValue = string | File | FormData | null;
|
|
5
|
+
/** Native constraint flags and a validation bubble anchor in the component's render root. */
|
|
6
|
+
export interface FormControlValidity {
|
|
7
|
+
readonly flags: ValidityStateFlags;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly anchor?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Pure model projections applied by Gyral to the host's ElementInternals (ADR 0008). */
|
|
12
|
+
export interface FormAssociation<S, P> {
|
|
13
|
+
readonly value: (state: S, ctx: Ctx<P>) => FormControlValue;
|
|
14
|
+
readonly state?: (state: S, ctx: Ctx<P>) => FormControlValue;
|
|
15
|
+
readonly validity?: (state: S, ctx: Ctx<P>) => FormControlValidity | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Framework message: the owning native form was reset. */
|
|
18
|
+
export interface FormReset {
|
|
19
|
+
readonly _tag: 'FormReset';
|
|
20
|
+
}
|
|
21
|
+
/** Framework message: the host or its fieldset changed the effective disabled state. */
|
|
22
|
+
export interface FormDisabled {
|
|
23
|
+
readonly _tag: 'FormDisabled';
|
|
24
|
+
readonly disabled: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Framework message: native session restoration or autofill supplied control state. */
|
|
27
|
+
export interface FormStateRestore {
|
|
28
|
+
readonly _tag: 'FormStateRestore';
|
|
29
|
+
readonly state: FormControlValue;
|
|
30
|
+
readonly mode: 'restore' | 'autocomplete';
|
|
31
|
+
}
|
|
32
|
+
export type FormMessage = FormReset | FormDisabled | FormStateRestore;
|
|
33
|
+
export interface FormControl {
|
|
34
|
+
receive(msg: FormMessage): void;
|
|
35
|
+
sync<S, P>(projection: FormAssociation<S, P>, state: S, ctx: Ctx<P>, root: ParentNode): void;
|
|
36
|
+
}
|
|
37
|
+
/** Native reactions must be installed before customElements.define() reads the prototype. */
|
|
38
|
+
declare function installFormCallbacks(Element: CustomElementConstructor, enabled: boolean): void;
|
|
39
|
+
/** Attach once in the constructor; defer pre-render reactions until hydration has committed. */
|
|
40
|
+
export declare function createFormControl(host: HTMLElement, model: Pick<HostModel<unknown, unknown>, 'hasReducer' | 'dispatch'>): FormControl;
|
|
41
|
+
export declare const formFeature: {
|
|
42
|
+
create<S, P>(host: HTMLElement, model: Pick<HostModel<unknown, unknown>, "hasReducer" | "dispatch">, projection: FormAssociation<S, P> | undefined): void;
|
|
43
|
+
sync(host: HTMLElement, state: unknown, ctx: Ctx<unknown>, root: ParentNode): void;
|
|
44
|
+
install: typeof installFormCallbacks;
|
|
45
|
+
};
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { internalsOf } from './element-internals.js';
|
|
2
|
+
import { afterRender, POST_HYDRATED } from './scheduler.js';
|
|
3
|
+
const controls = new WeakMap();
|
|
4
|
+
const projections = new WeakMap();
|
|
5
|
+
/** Native reactions must be installed before customElements.define() reads the prototype. */
|
|
6
|
+
function installFormCallbacks(Element, enabled) {
|
|
7
|
+
if (!enabled)
|
|
8
|
+
return;
|
|
9
|
+
Object.defineProperty(Element, 'formAssociated', { value: true });
|
|
10
|
+
Object.assign(Element.prototype, {
|
|
11
|
+
formResetCallback() {
|
|
12
|
+
controls.get(this)?.receive({ _tag: 'FormReset' });
|
|
13
|
+
},
|
|
14
|
+
formDisabledCallback(disabled) {
|
|
15
|
+
controls.get(this)?.receive({ _tag: 'FormDisabled', disabled });
|
|
16
|
+
},
|
|
17
|
+
formStateRestoreCallback(state, mode) {
|
|
18
|
+
controls.get(this)?.receive({ _tag: 'FormStateRestore', state, mode });
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Attach once in the constructor; defer pre-render reactions until hydration has committed. */
|
|
23
|
+
export function createFormControl(host, model) {
|
|
24
|
+
const dispatch = (msg) => {
|
|
25
|
+
if (model.hasReducer(msg._tag))
|
|
26
|
+
model.dispatch(msg);
|
|
27
|
+
};
|
|
28
|
+
const internals = internalsOf(host);
|
|
29
|
+
let live = false;
|
|
30
|
+
const pending = [];
|
|
31
|
+
const control = {
|
|
32
|
+
receive(msg) {
|
|
33
|
+
if (live)
|
|
34
|
+
dispatch(msg);
|
|
35
|
+
else
|
|
36
|
+
pending.push(msg);
|
|
37
|
+
},
|
|
38
|
+
sync(projection, state, ctx, root) {
|
|
39
|
+
const value = projection.value(state, ctx);
|
|
40
|
+
if (projection.state === undefined)
|
|
41
|
+
internals.setFormValue(value);
|
|
42
|
+
else
|
|
43
|
+
internals.setFormValue(value, projection.state(state, ctx));
|
|
44
|
+
const validity = projection.validity?.(state, ctx);
|
|
45
|
+
const anchor = validity?.anchor === undefined ? undefined : root.querySelector(validity.anchor);
|
|
46
|
+
if (anchor != null && !(anchor instanceof HTMLElement)) {
|
|
47
|
+
throw new TypeError('The form validity anchor must select an HTMLElement.');
|
|
48
|
+
}
|
|
49
|
+
internals.setValidity(validity?.flags ?? {}, validity?.message ?? '', anchor ?? undefined);
|
|
50
|
+
live = true;
|
|
51
|
+
if (pending.length > 0) {
|
|
52
|
+
const messages = pending.splice(0);
|
|
53
|
+
afterRender(POST_HYDRATED, () => {
|
|
54
|
+
for (const msg of messages)
|
|
55
|
+
dispatch(msg);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
controls.set(host, control);
|
|
61
|
+
return control;
|
|
62
|
+
}
|
|
63
|
+
export const formFeature = {
|
|
64
|
+
create(host, model, projection) {
|
|
65
|
+
if (projection === undefined)
|
|
66
|
+
return;
|
|
67
|
+
projections.set(host, projection);
|
|
68
|
+
createFormControl(host, model);
|
|
69
|
+
},
|
|
70
|
+
sync(host, state, ctx, root) {
|
|
71
|
+
const projection = projections.get(host);
|
|
72
|
+
if (projection !== undefined)
|
|
73
|
+
controls.get(host)?.sync(projection, state, ctx, root);
|
|
74
|
+
},
|
|
75
|
+
install: installFormCallbacks,
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=form-associated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-associated.js","sourceRoot":"","sources":["../src/form-associated.ts"],"names":[],"mappings":"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","sourcesContent":["import { internalsOf } from './element-internals.js';\nimport { afterRender, POST_HYDRATED } from './scheduler.js';\nimport type { Ctx } from './types.js';\nimport type { HostModel } from './host-model.js';\n\n/** A native submitted value, or null to omit this component from the submission. */\nexport type FormControlValue = string | File | FormData | null;\n\n/** Native constraint flags and a validation bubble anchor in the component's render root. */\nexport interface FormControlValidity {\n readonly flags: ValidityStateFlags;\n readonly message: string;\n readonly anchor?: string;\n}\n\n/** Pure model projections applied by Gyral to the host's ElementInternals (ADR 0008). */\nexport interface FormAssociation<S, P> {\n readonly value: (state: S, ctx: Ctx<P>) => FormControlValue;\n readonly state?: (state: S, ctx: Ctx<P>) => FormControlValue;\n readonly validity?: (state: S, ctx: Ctx<P>) => FormControlValidity | undefined;\n}\n\n/** Framework message: the owning native form was reset. */\nexport interface FormReset {\n readonly _tag: 'FormReset';\n}\n\n/** Framework message: the host or its fieldset changed the effective disabled state. */\nexport interface FormDisabled {\n readonly _tag: 'FormDisabled';\n readonly disabled: boolean;\n}\n\n/** Framework message: native session restoration or autofill supplied control state. */\nexport interface FormStateRestore {\n readonly _tag: 'FormStateRestore';\n readonly state: FormControlValue;\n readonly mode: 'restore' | 'autocomplete';\n}\n\nexport type FormMessage = FormReset | FormDisabled | FormStateRestore;\n\nexport interface FormControl {\n receive(msg: FormMessage): void;\n sync<S, P>(projection: FormAssociation<S, P>, state: S, ctx: Ctx<P>, root: ParentNode): void;\n}\n\nconst controls = new WeakMap<HTMLElement, FormControl>();\nconst projections = new WeakMap<HTMLElement, FormAssociation<unknown, unknown>>();\n\n/** Native reactions must be installed before customElements.define() reads the prototype. */\nfunction installFormCallbacks(Element: CustomElementConstructor, enabled: boolean): void {\n if (!enabled) return;\n Object.defineProperty(Element, 'formAssociated', { value: true });\n Object.assign(Element.prototype, {\n formResetCallback(this: HTMLElement): void {\n controls.get(this)?.receive({ _tag: 'FormReset' });\n },\n formDisabledCallback(this: HTMLElement, disabled: boolean): void {\n controls.get(this)?.receive({ _tag: 'FormDisabled', disabled });\n },\n formStateRestoreCallback(\n this: HTMLElement,\n state: FormControlValue,\n mode: 'restore' | 'autocomplete',\n ): void {\n controls.get(this)?.receive({ _tag: 'FormStateRestore', state, mode });\n },\n });\n}\n\n/** Attach once in the constructor; defer pre-render reactions until hydration has committed. */\nexport function createFormControl(\n host: HTMLElement,\n model: Pick<HostModel<unknown, unknown>, 'hasReducer' | 'dispatch'>,\n): FormControl {\n const dispatch = (msg: FormMessage): void => {\n if (model.hasReducer(msg._tag)) model.dispatch(msg);\n };\n const internals = internalsOf(host);\n let live = false;\n const pending: FormMessage[] = [];\n const control: FormControl = {\n receive(msg) {\n if (live) dispatch(msg);\n else pending.push(msg);\n },\n sync(projection, state, ctx, root) {\n const value = projection.value(state, ctx);\n if (projection.state === undefined) internals.setFormValue(value);\n else internals.setFormValue(value, projection.state(state, ctx));\n const validity = projection.validity?.(state, ctx);\n const anchor =\n validity?.anchor === undefined ? undefined : root.querySelector(validity.anchor);\n if (anchor != null && !(anchor instanceof HTMLElement)) {\n throw new TypeError('The form validity anchor must select an HTMLElement.');\n }\n internals.setValidity(validity?.flags ?? {}, validity?.message ?? '', anchor ?? undefined);\n live = true;\n if (pending.length > 0) {\n const messages = pending.splice(0);\n afterRender(POST_HYDRATED, () => {\n for (const msg of messages) dispatch(msg);\n });\n }\n },\n };\n controls.set(host, control);\n return control;\n}\n\nexport const formFeature = {\n create<S, P>(\n host: HTMLElement,\n model: Pick<HostModel<unknown, unknown>, 'hasReducer' | 'dispatch'>,\n projection: FormAssociation<S, P> | undefined,\n ): void {\n if (projection === undefined) return;\n projections.set(host, projection as unknown as FormAssociation<unknown, unknown>);\n createFormControl(host, model);\n },\n sync(host: HTMLElement, state: unknown, ctx: Ctx<unknown>, root: ParentNode): void {\n const projection = projections.get(host);\n if (projection !== undefined) controls.get(host)?.sync(projection, state, ctx, root);\n },\n install: installFormCallbacks,\n};\n"]}
|
package/dist/forms.d.ts
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
-
import type { FieldIssue, FormFields, IntentParser, IntentRejected } from './types.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import type { FieldIssue, FormFields, IntentInput, IntentParser, IntentRejected } from './types.js';
|
|
3
|
+
type In<Schema extends StandardSchemaV1> = StandardSchemaV1.InferInput<Schema>;
|
|
4
|
+
type Out<Schema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<Schema>;
|
|
5
|
+
/** The definition's field names, when its input is an object; otherwise any string. */
|
|
6
|
+
export type FieldName<Schema extends StandardSchemaV1> = In<Schema> extends Readonly<Record<string, unknown>> ? keyof In<Schema> & string : string;
|
|
7
|
+
export interface FormOptions<Schema extends StandardSchemaV1> {
|
|
8
|
+
/**
|
|
9
|
+
* Fields whose submitted values are never sent back: not in `IntentRejected.values`, not in
|
|
10
|
+
* `values()`, not in the devtools timeline. Any posted field name may be listed, including
|
|
11
|
+
* fields outside the schema. Names containing `password`, `passcode`, `csrf` or `xsrf` are
|
|
12
|
+
* secret without being listed (the browser never restores a password field either, and a
|
|
13
|
+
* CSRF token must come fresh from the server, not from the echo).
|
|
14
|
+
*/
|
|
15
|
+
readonly secret?: readonly (FieldName<Schema> | (string & {}))[];
|
|
7
16
|
}
|
|
8
|
-
export declare function defineForm<Schema extends StandardSchemaV1>(schema: Schema): FormDefinition<Schema>;
|
|
9
|
-
export type FormValue = FormDataEntryValue | readonly FormDataEntryValue[];
|
|
10
17
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
18
|
+
* A form's submitted text values, keyed by the definition's fields, for re-rendering the form
|
|
19
|
+
* with what was typed (sticky fields). The same shape as `IntentRejected.values`: repeated names
|
|
20
|
+
* (and `name[]`) are arrays; `File`s and secret fields are left out.
|
|
14
21
|
*/
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export declare function formFields(data: Readonly<Record<string, FormValue>>): FormFields;
|
|
22
|
+
export type FormValues<Schema extends StandardSchemaV1> = In<Schema> extends Readonly<Record<string, unknown>> ? {
|
|
23
|
+
readonly [K in keyof In<Schema> & string]?: string | readonly string[];
|
|
24
|
+
} : FormFields;
|
|
19
25
|
export type FormResult<T> = {
|
|
20
26
|
readonly ok: true;
|
|
21
27
|
readonly data: T;
|
|
@@ -23,22 +29,35 @@ export type FormResult<T> = {
|
|
|
23
29
|
readonly ok: false;
|
|
24
30
|
readonly rejected: IntentRejected;
|
|
25
31
|
};
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
/** A form's schema, shared by the client component and (with `@gyral/ssr`) the server route. */
|
|
33
|
+
export interface FormDefinition<Schema extends StandardSchemaV1> {
|
|
34
|
+
readonly _tag: 'FormDefinition';
|
|
35
|
+
readonly schema: Schema;
|
|
36
|
+
/** The fields listed as secret (names containing `password`, `passcode`, `csrf` or `xsrf` are secret too). */
|
|
37
|
+
readonly secret: readonly string[];
|
|
38
|
+
/**
|
|
39
|
+
* Validates submitted form data exactly as `form()` does; the server's `formAction` uses it,
|
|
40
|
+
* so both paths produce the same `IntentRejected` (`intent` defaults to `''`).
|
|
41
|
+
*/
|
|
42
|
+
readonly validate: (data: FormData, intent?: string) => FormResult<Out<Schema>> | Promise<FormResult<Out<Schema>>>;
|
|
43
|
+
/** The submitted text values without secrets: what a re-rendered form may show again. */
|
|
44
|
+
readonly values: (data: FormData) => FormValues<Schema>;
|
|
45
|
+
}
|
|
46
|
+
/** Whether `name` is never echoed: declared secret by a form, or a password-like name. */
|
|
47
|
+
export declare function isSecretField(name: string): boolean;
|
|
48
|
+
export declare function defineForm<Schema extends StandardSchemaV1>(schema: Schema, options?: FormOptions<Schema>): FormDefinition<Schema>;
|
|
49
|
+
export type FormValue = FormDataEntryValue | readonly FormDataEntryValue[];
|
|
31
50
|
/**
|
|
32
51
|
* Intent parser for a `<form data-intent>` submission, validated by a schema. `toMsg` also
|
|
33
52
|
* gets the raw `FormData`, for sending the submission to the server (`submitForm` in
|
|
34
|
-
*
|
|
53
|
+
* `@gyral/http`) after it passed client-side validation.
|
|
35
54
|
*/
|
|
36
|
-
export declare function form<Schema extends StandardSchemaV1, M>(definition: FormDefinition<Schema> | Schema, toMsg: (data: Out<Schema>, formData: FormData) => M | undefined): IntentParser<M
|
|
55
|
+
export declare function form<Schema extends StandardSchemaV1, M>(definition: FormDefinition<Schema> | Schema, toMsg: (data: Out<Schema>, formData: FormData) => M | undefined): (input: IntentInput) => ReturnType<IntentParser<M>>;
|
|
37
56
|
/**
|
|
38
57
|
* Intent parser for one control (pair with `data-intent-on="input"` for live checks).
|
|
39
58
|
* Validates `checked` for checkboxes and radios, otherwise `value`.
|
|
40
59
|
*/
|
|
41
|
-
export declare function field<Schema extends StandardSchemaV1, M>(schema: Schema, toMsg: (value: Out<Schema>) => M | undefined): IntentParser<M
|
|
60
|
+
export declare function field<Schema extends StandardSchemaV1, M>(schema: Schema, toMsg: (value: Out<Schema>) => M | undefined): (input: IntentInput) => ReturnType<IntentParser<M>>;
|
|
42
61
|
/**
|
|
43
62
|
* Groups issues by field name. Call it in the `IntentRejected` reducer and keep the result in
|
|
44
63
|
* state: `invalid()` re-applies an error only when it gets a new array.
|
|
@@ -46,14 +65,7 @@ export declare function field<Schema extends StandardSchemaV1, M>(schema: Schema
|
|
|
46
65
|
export declare function fieldErrors(issues: readonly FieldIssue[]): Readonly<Record<string, readonly string[]>>;
|
|
47
66
|
/**
|
|
48
67
|
* A Standard Schema for `IntentRejected` as a server sends it (`formAction` answering JSON).
|
|
49
|
-
* Used by `submitForm` in
|
|
68
|
+
* Used by `submitForm` in `@gyral/http` to recognise a 422 rejection; usable with any decoder.
|
|
50
69
|
*/
|
|
51
70
|
export declare const intentRejectedSchema: StandardSchemaV1<unknown, IntentRejected>;
|
|
52
|
-
/** A server's JSON answer to a valid form post that would have redirected without JS. */
|
|
53
|
-
export interface FormRedirected {
|
|
54
|
-
readonly _tag: 'Redirected';
|
|
55
|
-
readonly location: string;
|
|
56
|
-
}
|
|
57
|
-
/** The `location` of a `FormRedirected` answer (from `formAction` in @gyral/ssr), if it is one. */
|
|
58
|
-
export declare function redirectedTo(body: unknown): string | undefined;
|
|
59
71
|
export {};
|