@dou-ui/form-builder 0.0.0-stage → 0.1.0-alpha.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 (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/dist/builder/canvas.js +695 -0
  4. package/dist/builder/canvas.js.map +1 -0
  5. package/dist/builder/completion.js +49 -0
  6. package/dist/builder/completion.js.map +1 -0
  7. package/dist/builder/condition-editor.js +6 -0
  8. package/dist/builder/condition-editor.js.map +1 -0
  9. package/dist/builder/condition-editor.vue_vue_type_script_setup_true_lang.js +410 -0
  10. package/dist/builder/condition-editor.vue_vue_type_script_setup_true_lang.js.map +1 -0
  11. package/dist/builder/condition.js +159 -0
  12. package/dist/builder/condition.js.map +1 -0
  13. package/dist/builder/context.js +477 -0
  14. package/dist/builder/context.js.map +1 -0
  15. package/dist/builder/core/commands.js +373 -0
  16. package/dist/builder/core/commands.js.map +1 -0
  17. package/dist/builder/core/history.js +76 -0
  18. package/dist/builder/core/history.js.map +1 -0
  19. package/dist/builder/core/transaction.js +298 -0
  20. package/dist/builder/core/transaction.js.map +1 -0
  21. package/dist/builder/drag.js +466 -0
  22. package/dist/builder/drag.js.map +1 -0
  23. package/dist/builder/expression-box.js +19 -0
  24. package/dist/builder/expression-box.js.map +1 -0
  25. package/dist/builder/expression-input.js +6 -0
  26. package/dist/builder/expression-input.js.map +1 -0
  27. package/dist/builder/expression-input.vue_vue_type_script_setup_true_lang.js +117 -0
  28. package/dist/builder/expression-input.vue_vue_type_script_setup_true_lang.js.map +1 -0
  29. package/dist/builder/extension.js +47 -0
  30. package/dist/builder/extension.js.map +1 -0
  31. package/dist/builder/form-builder.js +6 -0
  32. package/dist/builder/form-builder.js.map +1 -0
  33. package/dist/builder/form-builder.vue_vue_type_script_setup_true_lang.js +450 -0
  34. package/dist/builder/form-builder.vue_vue_type_script_setup_true_lang.js.map +1 -0
  35. package/dist/builder/index.js +22 -0
  36. package/dist/builder/index.js.map +1 -0
  37. package/dist/builder/inspector.js +6 -0
  38. package/dist/builder/inspector.js.map +1 -0
  39. package/dist/builder/inspector.vue_vue_type_script_setup_true_lang.js +442 -0
  40. package/dist/builder/inspector.vue_vue_type_script_setup_true_lang.js.map +1 -0
  41. package/dist/builder/json-dialog.js +6 -0
  42. package/dist/builder/json-dialog.js.map +1 -0
  43. package/dist/builder/json-dialog.vue_vue_type_script_setup_true_lang.js +281 -0
  44. package/dist/builder/json-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
  45. package/dist/builder/locale.js +1271 -0
  46. package/dist/builder/locale.js.map +1 -0
  47. package/dist/builder/logic-fields.js +18 -0
  48. package/dist/builder/logic-fields.js.map +1 -0
  49. package/dist/builder/logic.js +332 -0
  50. package/dist/builder/logic.js.map +1 -0
  51. package/dist/builder/menu.js +92 -0
  52. package/dist/builder/menu.js.map +1 -0
  53. package/dist/builder/outline-row.js +6 -0
  54. package/dist/builder/outline-row.js.map +1 -0
  55. package/dist/builder/outline-row.vue_vue_type_script_setup_true_lang.js +88 -0
  56. package/dist/builder/outline-row.vue_vue_type_script_setup_true_lang.js.map +1 -0
  57. package/dist/builder/outline.js +6 -0
  58. package/dist/builder/outline.js.map +1 -0
  59. package/dist/builder/outline.vue_vue_type_script_setup_true_lang.js +111 -0
  60. package/dist/builder/outline.vue_vue_type_script_setup_true_lang.js.map +1 -0
  61. package/dist/builder/palette.js +6 -0
  62. package/dist/builder/palette.js.map +1 -0
  63. package/dist/builder/palette.vue_vue_type_script_setup_true_lang.js +96 -0
  64. package/dist/builder/palette.vue_vue_type_script_setup_true_lang.js.map +1 -0
  65. package/dist/builder/preview.js +6 -0
  66. package/dist/builder/preview.js.map +1 -0
  67. package/dist/builder/preview.vue_vue_type_script_setup_true_lang.js +111 -0
  68. package/dist/builder/preview.vue_vue_type_script_setup_true_lang.js.map +1 -0
  69. package/dist/builder/settings-binding.js +57 -0
  70. package/dist/builder/settings-binding.js.map +1 -0
  71. package/dist/builder/settings.js +487 -0
  72. package/dist/builder/settings.js.map +1 -0
  73. package/dist/builder/span-field.js +104 -0
  74. package/dist/builder/span-field.js.map +1 -0
  75. package/dist/code/expression-input.js +6 -0
  76. package/dist/code/expression-input.js.map +1 -0
  77. package/dist/code/expression-input.vue_vue_type_script_setup_true_lang.js +88 -0
  78. package/dist/code/expression-input.vue_vue_type_script_setup_true_lang.js.map +1 -0
  79. package/dist/code/extension.js +202 -0
  80. package/dist/code/extension.js.map +1 -0
  81. package/dist/code/index.js +19 -0
  82. package/dist/code/index.js.map +1 -0
  83. package/dist/code/language.js +43 -0
  84. package/dist/code/language.js.map +1 -0
  85. package/dist/collaboration/index.js +382 -0
  86. package/dist/collaboration/index.js.map +1 -0
  87. package/dist/collaboration/order.js +88 -0
  88. package/dist/collaboration/order.js.map +1 -0
  89. package/dist/core/check.js +314 -0
  90. package/dist/core/check.js.map +1 -0
  91. package/dist/core/document.js +112 -0
  92. package/dist/core/document.js.map +1 -0
  93. package/dist/core/expression.js +854 -0
  94. package/dist/core/expression.js.map +1 -0
  95. package/dist/core/formulas.js +242 -0
  96. package/dist/core/formulas.js.map +1 -0
  97. package/dist/core/rules.js +83 -0
  98. package/dist/core/rules.js.map +1 -0
  99. package/dist/core/values.js +80 -0
  100. package/dist/core/values.js.map +1 -0
  101. package/dist/fields/builtin.js +167 -0
  102. package/dist/fields/builtin.js.map +1 -0
  103. package/dist/fields/components.js +155 -0
  104. package/dist/fields/components.js.map +1 -0
  105. package/dist/fields/define.js +44 -0
  106. package/dist/fields/define.js.map +1 -0
  107. package/dist/fields/format.js +125 -0
  108. package/dist/fields/format.js.map +1 -0
  109. package/dist/fields/options.js +54 -0
  110. package/dist/fields/options.js.map +1 -0
  111. package/dist/index.js +20 -0
  112. package/dist/locale.js +161 -0
  113. package/dist/locale.js.map +1 -0
  114. package/dist/renderer/containers.js +430 -0
  115. package/dist/renderer/containers.js.map +1 -0
  116. package/dist/renderer/context.js +21 -0
  117. package/dist/renderer/context.js.map +1 -0
  118. package/dist/renderer/form-renderer.js +6 -0
  119. package/dist/renderer/form-renderer.js.map +1 -0
  120. package/dist/renderer/form-renderer.vue_vue_type_script_setup_true_lang.js +272 -0
  121. package/dist/renderer/form-renderer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  122. package/dist/renderer/index.js +19 -0
  123. package/dist/renderer/index.js.map +1 -0
  124. package/dist/renderer/node-view.js +385 -0
  125. package/dist/renderer/node-view.js.map +1 -0
  126. package/dist/style.css +1 -0
  127. package/dist/style.d.ts +1 -0
  128. package/dist/style.js +27 -0
  129. package/dist/types/builder/canvas.d.ts +5 -0
  130. package/dist/types/builder/completion.d.ts +35 -0
  131. package/dist/types/builder/condition-editor.vue.d.ts +17 -0
  132. package/dist/types/builder/condition.d.ts +37 -0
  133. package/dist/types/builder/context.d.ts +149 -0
  134. package/dist/types/builder/core/commands.d.ts +74 -0
  135. package/dist/types/builder/core/history.d.ts +23 -0
  136. package/dist/types/builder/core/transaction.d.ts +108 -0
  137. package/dist/types/builder/drag.d.ts +52 -0
  138. package/dist/types/builder/expression-box.d.ts +8 -0
  139. package/dist/types/builder/expression-input.vue.d.ts +29 -0
  140. package/dist/types/builder/extension.d.ts +149 -0
  141. package/dist/types/builder/form-builder.vue.d.ts +24 -0
  142. package/dist/types/builder/index.d.ts +76 -0
  143. package/dist/types/builder/inspector.vue.d.ts +3 -0
  144. package/dist/types/builder/json-dialog.vue.d.ts +10 -0
  145. package/dist/types/builder/locale.d.ts +195 -0
  146. package/dist/types/builder/logic-fields.d.ts +2 -0
  147. package/dist/types/builder/logic.d.ts +57 -0
  148. package/dist/types/builder/menu.d.ts +23 -0
  149. package/dist/types/builder/outline-row.vue.d.ts +19 -0
  150. package/dist/types/builder/outline.vue.d.ts +3 -0
  151. package/dist/types/builder/palette.vue.d.ts +3 -0
  152. package/dist/types/builder/preview.vue.d.ts +3 -0
  153. package/dist/types/builder/settings-binding.d.ts +23 -0
  154. package/dist/types/builder/settings.d.ts +49 -0
  155. package/dist/types/builder/span-field.d.ts +4 -0
  156. package/dist/types/builder/types.d.ts +59 -0
  157. package/dist/types/code/expression-input.vue.d.ts +23 -0
  158. package/dist/types/code/extension.d.ts +45 -0
  159. package/dist/types/code/index.d.ts +12 -0
  160. package/dist/types/code/language.d.ts +6 -0
  161. package/dist/types/collaboration/index.d.ts +64 -0
  162. package/dist/types/collaboration/order.d.ts +20 -0
  163. package/dist/types/core/check.d.ts +68 -0
  164. package/dist/types/core/document.d.ts +36 -0
  165. package/dist/types/core/expression.d.ts +175 -0
  166. package/dist/types/core/formulas.d.ts +44 -0
  167. package/dist/types/core/rules.d.ts +23 -0
  168. package/dist/types/core/types.d.ts +211 -0
  169. package/dist/types/core/values.d.ts +22 -0
  170. package/dist/types/fields/builtin.d.ts +9 -0
  171. package/dist/types/fields/components.d.ts +177 -0
  172. package/dist/types/fields/define.d.ts +93 -0
  173. package/dist/types/fields/format.d.ts +76 -0
  174. package/dist/types/fields/options.d.ts +11 -0
  175. package/dist/types/index.d.ts +3 -0
  176. package/dist/types/locale.d.ts +45 -0
  177. package/dist/types/renderer/containers.d.ts +20 -0
  178. package/dist/types/renderer/context.d.ts +186 -0
  179. package/dist/types/renderer/form-renderer.vue.d.ts +25 -0
  180. package/dist/types/renderer/index.d.ts +81 -0
  181. package/dist/types/renderer/node-view.d.ts +56 -0
  182. package/package.json +88 -4
@@ -0,0 +1,12 @@
1
+ import type { FormBuilderExtension } from '../builder/extension';
2
+ import CodeExpressionInput from './expression-input.vue';
3
+ export { expressionCodeExtension } from './extension';
4
+ export type { ExpressionCodeOptions, ExpressionFunctionInfo, ExpressionName } from './extension';
5
+ export { formExpression } from './language';
6
+ export { CodeExpressionInput };
7
+ /**
8
+ * Writes the builder's expressions (formulas, checks, conditions as text)
9
+ * in `@dou-ui/code`'s code input: colored, with completions, parameter
10
+ * hints and problems underlined. Needs `@dou-ui/code` and its styles.
11
+ */
12
+ export declare function codeExpressions(): FormBuilderExtension;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The form expression language for `@dou-ui/code`: `values` and its
3
+ * paths, strings, numbers, function calls and operators, colored as in
4
+ * the code editor.
5
+ */
6
+ export declare const formExpression: import("@dou-ui/code").CodeLanguage<Record<string, never>>;
@@ -0,0 +1,64 @@
1
+ import { collaborationColors } from 'dou-ui';
2
+ import * as Y from 'yjs';
3
+ import type { FormDocument } from '../core/types';
4
+ import type { FormBuilderExtension } from '../builder/extension';
5
+ /** The part of an `Awareness` (`y-protocols/awareness`) the builder uses. */
6
+ export interface AwarenessLike {
7
+ clientID: number;
8
+ getLocalState(): Record<string, unknown> | null;
9
+ setLocalStateField(field: string, value: unknown): void;
10
+ getStates(): Map<number, Record<string, unknown>>;
11
+ on(event: 'change', listener: (...args: unknown[]) => void): void;
12
+ off(event: 'change', listener: (...args: unknown[]) => void): void;
13
+ }
14
+ /** How the people editing together see each other. */
15
+ export interface CollaborationUser {
16
+ name: string;
17
+ /** The color of this user's marks, any CSS color. Picked from the client id when left out. */
18
+ color?: string;
19
+ }
20
+ export interface CollaborationOptions {
21
+ /** Where the nodes live, e.g. `ydoc.getMap('nodes')`. */
22
+ nodes: Y.Map<unknown>;
23
+ /** Where the form's own settings live, e.g. `ydoc.getMap('settings')`; in the same `Y.Doc` as `nodes`. */
24
+ settings: Y.Map<unknown>;
25
+ /** Shares what each person has selected (`y-protocols/awareness`). */
26
+ awareness?: AwarenessLike;
27
+ /** This user, shown to the others. Needs `awareness`. */
28
+ user?: CollaborationUser;
29
+ }
30
+ /** What each person shares under the awareness field `formBuilder`. */
31
+ export interface FormBuilderPresence {
32
+ selection: string[];
33
+ }
34
+ export { collaborationColors };
35
+ /** Writes a form into empty maps, e.g. when a form so far saved as JSON starts being edited together. */
36
+ export declare function fillMaps(maps: {
37
+ nodes: Y.Map<unknown>;
38
+ settings: Y.Map<unknown>;
39
+ }, document: FormDocument): void;
40
+ /**
41
+ * The form the maps hold. A node whose container is gone, e.g. one moved
42
+ * into a section while another person deleted the section, goes to the top
43
+ * of the form. So do containers two people moved into each other at once:
44
+ * the one with the smallest id goes to the top, the same for everyone.
45
+ * Both are listed in `lifted`.
46
+ */
47
+ export declare function readMaps(maps: {
48
+ nodes: Y.Map<unknown>;
49
+ settings: Y.Map<unknown>;
50
+ }): {
51
+ document: FormDocument;
52
+ lifted: string[];
53
+ };
54
+ /**
55
+ * Edit a form together through a Yjs document. Each node lives in `nodes`
56
+ * as its own `Y.Map`, each key on its own, with its container and an order
57
+ * key beside them; so two people changing different keys of one field both
58
+ * keep their change, and moving a node only writes that node. The form's
59
+ * settings live in `settings`. The builder's `v-model` only mirrors them:
60
+ * a form set through `v-model` from outside is not written back. Undo only
61
+ * takes back this user's own changes. With `awareness`, each person sees
62
+ * what the others have selected, marked in their colors.
63
+ */
64
+ export declare function collaboration(options: CollaborationOptions): FormBuilderExtension;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Order keys: strings that sort like the nodes they belong to. A key can
3
+ * always be made between two others, so moving or inserting a node only
4
+ * writes that node's key, never its neighbours'.
5
+ */
6
+ /** A key after `a` and before `b`; either may be missing. Needs `a < b`. */
7
+ export declare function keyBetween(a: string | null, b: string | null): string;
8
+ /** `n` keys in order between `a` and `b`, kept short by splitting the range in halves. */
9
+ export declare function keysBetween(a: string | null, b: string | null, n: number): string[];
10
+ /**
11
+ * Which of `keys` (in their wanted order) can stay: the longest strictly
12
+ * increasing run. Missing keys (`null`) never stay. Returns the indexes.
13
+ */
14
+ export declare function keptIndexes(keys: (string | null)[]): Set<number>;
15
+ /**
16
+ * Order keys for items in their wanted order, given each one's current key
17
+ * (`null` when it has none). The longest run already in order keeps its
18
+ * keys; the others get fresh keys between their kept neighbours.
19
+ */
20
+ export declare function orderKeys(keys: (string | null)[]): string[];
@@ -0,0 +1,68 @@
1
+ import type { ExpressionProblem } from './expression';
2
+ import type { FormDocument } from './types';
3
+ import type { FormFieldType } from '../fields/define';
4
+ export interface DocumentProblem {
5
+ code: 'version' | 'duplicate-id' | 'missing-name' | 'duplicate-name' | 'unknown-type' | 'misplaced-page' | 'not-a-page' | 'empty' | 'expression' | 'formula-cycle' | 'unknown-validator' | 'check-and-validator';
6
+ /** The node at fault; none for the document's own problems. */
7
+ nodeId?: string;
8
+ /**
9
+ * Where the problem is, as a path into the document, e.g.
10
+ * `nodes[1].children[0]` or `nodes[2].rules[0].check`; `version` for the
11
+ * version, and `''` for a problem of the whole.
12
+ */
13
+ path: string;
14
+ /** The property at fault, e.g. `visibleWhen`, for an expression. */
15
+ property?: string;
16
+ /** The id, name, type or expression source at fault, for the editor's message. */
17
+ text: string;
18
+ /** The expression's own problem, for `expression`. */
19
+ expression?: ExpressionProblem;
20
+ /** An English description, for logs. */
21
+ message: string;
22
+ }
23
+ export interface CheckDocumentOptions {
24
+ /** Field types besides the built-in ones, as given to the renderer. */
25
+ fields?: readonly FormFieldType<any>[];
26
+ /** Functions the expressions can call besides the built-in ones. */
27
+ functions?: readonly string[];
28
+ /**
29
+ * The names of the validators given to the renderer. Left out, a rule's
30
+ * `validator` is not checked against them.
31
+ */
32
+ validators?: readonly string[];
33
+ }
34
+ /**
35
+ * The problems of a document, for an editor to point at, before it is
36
+ * saved or rendered: ids used twice, fields without a name or sharing one,
37
+ * types nobody registered, pages outside tabs and steps, expressions that
38
+ * do not parse or read fields that are not there, formulas that read
39
+ * each other, and rules naming a validator nobody gave.
40
+ */
41
+ export declare function checkDocument(document: FormDocument, options?: CheckDocumentOptions): DocumentProblem[];
42
+ /**
43
+ * The document in the current version (`1`). Documents of an older
44
+ * version are upgraded step by step; there are none yet, so for now this
45
+ * only checks the version. A document without a version is read as
46
+ * version 1, as the renderer does. Throws for anything that is not a
47
+ * document and for a version it does not know.
48
+ */
49
+ export declare function migrateDocument(document: unknown): FormDocument;
50
+ /** A part of some data that is not shaped like a form document. */
51
+ export interface DocumentShapeProblem {
52
+ /** Where, as a path into the data, e.g. `nodes[2].children[0].label`; `''` for the whole. */
53
+ path: string;
54
+ /** `type`: the value is of another kind; `required`: it is missing. */
55
+ code: 'type' | 'required';
56
+ /** What the value should be. */
57
+ expected: 'object' | 'array' | 'string' | 'number' | 'boolean' | 'span' | 'mark';
58
+ /** An English description, for logs. */
59
+ message: string;
60
+ }
61
+ /**
62
+ * Where some data, e.g. JSON pasted into an editor, is not shaped like a
63
+ * form document: a key holding the wrong kind of value, or a node without
64
+ * its `id`, `type` or `children`. Problems name the path to the value, so
65
+ * they can be found in the text. Run `checkDocument` on the result for
66
+ * the problems of a document that is shaped right.
67
+ */
68
+ export declare function checkDocumentShape(data: unknown): DocumentShapeProblem[];
@@ -0,0 +1,36 @@
1
+ import type { FormContainerNode, FormDocument, FormFieldNode, FormListNode, FormNode, FormPageNode, FormSectionNode, FormStepsNode, FormTabsNode } from './types';
2
+ /** A document with nothing in it. */
3
+ export declare function emptyDocument(): FormDocument;
4
+ /** Warn once, for the whole page, about a document of a version this package does not know. */
5
+ export declare function checkVersion(document: FormDocument): void;
6
+ /** A short id that is unique enough within one document, e.g. `f-lz3k9a-1`. */
7
+ export declare function createId(prefix?: string): string;
8
+ export declare const isSection: (node: FormNode) => node is FormSectionNode;
9
+ export declare const isTabs: (node: FormNode) => node is FormTabsNode;
10
+ export declare const isSteps: (node: FormNode) => node is FormStepsNode;
11
+ export declare const isPage: (node: FormNode) => node is FormPageNode;
12
+ export declare const isList: (node: FormNode) => node is FormListNode;
13
+ /** Whether a node holds other nodes: a section, tabs, steps, a page or a list. */
14
+ export declare const isContainer: (node: FormNode) => node is FormContainerNode;
15
+ /** Whether a node is a field, of a built-in or a registered type. */
16
+ export declare const isField: (node: FormNode) => node is FormFieldNode;
17
+ /** Every node in document order, containers before their children, the fields of list items included. */
18
+ export declare function walkNodes(nodes: readonly FormNode[]): Generator<FormNode>;
19
+ /**
20
+ * The nodes that write a value of their own, in document order: fields
21
+ * with a `name` and lists. Fields inside a list belong to its items, so
22
+ * they are not among them.
23
+ */
24
+ export declare function valueNodes(nodes: readonly FormNode[]): (FormFieldNode | FormListNode)[];
25
+ /** The fields that write a value, in document order, outside lists. */
26
+ export declare function fieldNodes(document: FormDocument): FormFieldNode[];
27
+ /** The node with this id, anywhere in the document. */
28
+ export declare function findNode(document: FormDocument, id: string): FormNode | undefined;
29
+ /** The value at a dotted path, or `undefined` when part of it is missing. */
30
+ export declare function getPath(model: unknown, path: string): unknown;
31
+ /** Write a value at a dotted path, creating the objects on the way. */
32
+ export declare function setPath(model: Record<string, unknown>, path: string, value: unknown): void;
33
+ /** Remove the value at a dotted path, if it is there. */
34
+ export declare function unsetPath(model: Record<string, unknown>, path: string): void;
35
+ /** A deep copy of plain data, so a default is never shared between forms. */
36
+ export declare function cloneValue<T>(value: T): T;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * A small expression language for form logic: show a field when another
3
+ * has some value, compute a total, check one date against another.
4
+ *
5
+ * Expressions come from stored form data, so they are parsed and run by
6
+ * this module and never handed to `eval` or `new Function`. They can read
7
+ * `values` (the whole form) and `value` (the field's own value), call the
8
+ * built-in functions and the ones the app registers, and nothing else.
9
+ *
10
+ * values.type == 'laptop' && values.price > 5000
11
+ * round(values.price * values.count, 2)
12
+ * values.city in ['sh', 'bj'] ? 'east' : 'other'
13
+ */
14
+ export type ExpressionErrorCode = 'unexpected-character' | 'unterminated-string' | 'unexpected-token' | 'unexpected-end' | 'unknown-name' | 'assignment' | 'too-deep' | 'too-long';
15
+ /** Where and why an expression does not parse. `start` and `end` are offsets into the source. */
16
+ export declare class ExpressionError extends Error {
17
+ readonly code: ExpressionErrorCode;
18
+ readonly start: number;
19
+ readonly end: number;
20
+ constructor(code: ExpressionErrorCode, message: string, start: number, end?: number);
21
+ }
22
+ /** The syntax tree of an expression. Each node keeps its place in the source. */
23
+ export type ExpressionNode = {
24
+ kind: 'literal';
25
+ value: string | number | boolean | null;
26
+ start: number;
27
+ end: number;
28
+ } | {
29
+ kind: 'list';
30
+ items: ExpressionNode[];
31
+ start: number;
32
+ end: number;
33
+ } | {
34
+ kind: 'name';
35
+ name: 'values' | 'value' | 'item';
36
+ start: number;
37
+ end: number;
38
+ } | {
39
+ kind: 'member';
40
+ object: ExpressionNode;
41
+ key: ExpressionNode;
42
+ start: number;
43
+ end: number;
44
+ } | {
45
+ kind: 'call';
46
+ name: string;
47
+ args: ExpressionNode[];
48
+ start: number;
49
+ end: number;
50
+ } | {
51
+ kind: 'unary';
52
+ operator: '!' | '-';
53
+ argument: ExpressionNode;
54
+ start: number;
55
+ end: number;
56
+ } | {
57
+ kind: 'binary';
58
+ operator: BinaryOperator;
59
+ left: ExpressionNode;
60
+ right: ExpressionNode;
61
+ start: number;
62
+ end: number;
63
+ } | {
64
+ kind: 'condition';
65
+ test: ExpressionNode;
66
+ whenTrue: ExpressionNode;
67
+ whenFalse: ExpressionNode;
68
+ start: number;
69
+ end: number;
70
+ };
71
+ export type BinaryOperator = '||' | '&&' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'in' | '+' | '-' | '*' | '/' | '%';
72
+ /** A function expressions can call. It gets the values of its arguments and should not change them. */
73
+ export type FormFunction = (...args: any[]) => unknown;
74
+ /** What an expression reads when it runs. */
75
+ export interface ExpressionScope {
76
+ /** The whole form. Defaults to an empty one. */
77
+ values?: Record<string, unknown>;
78
+ /** The value of the field the expression belongs to. */
79
+ value?: unknown;
80
+ /** Inside a list, the item the field belongs to. */
81
+ item?: unknown;
82
+ /** Functions besides the built-in ones; one of the same name replaces the built-in function. */
83
+ functions?: Readonly<Record<string, FormFunction>>;
84
+ }
85
+ export interface CompiledExpression {
86
+ readonly source: string;
87
+ readonly ast: ExpressionNode;
88
+ /**
89
+ * The paths under `values` the expression reads, e.g. `['price', 'contact.phone']`.
90
+ * An empty string means it reads a key that is only known when it runs,
91
+ * so it may read anything.
92
+ */
93
+ readonly fields: readonly string[];
94
+ /** The paths under `item` it reads, like `fields` for `values`. */
95
+ readonly itemFields: readonly string[];
96
+ /** Whether it reads `value`. */
97
+ readonly readsValue: boolean;
98
+ /** The functions it calls. */
99
+ readonly calls: readonly string[];
100
+ /** Run it. A function that throws makes the result `null`. */
101
+ evaluate: (scope: ExpressionScope) => unknown;
102
+ }
103
+ /** Parse an expression. Throws an `ExpressionError` that says where it went wrong. */
104
+ export declare function parseExpression(source: string): ExpressionNode;
105
+ /** Whether a value counts as true: everything but `false`, `null`, `0`, `NaN`, `''` and `[]`. */
106
+ export declare function truthy(value: unknown): boolean;
107
+ /** Whether a value is empty: `null`, blank text, `[]` or `{}`. */
108
+ export declare function isEmptyValue(value: unknown): boolean;
109
+ /** Equal by value: lists and objects compare their items, `null` equals a missing value. */
110
+ export declare function equalValues(a: unknown, b: unknown): boolean;
111
+ /**
112
+ * The functions every expression can call:
113
+ *
114
+ * - `len(x)`: length of text, number of items or of keys; `0` for nothing.
115
+ * - `empty(x)`: whether `x` is `null`, blank text, `[]` or `{}`.
116
+ * - `number(x)`: `x` as a number, or `null` when it is not one.
117
+ * - `sum(list)`, `min(list)`, `max(list)`: over a list, or over the arguments;
118
+ * `sum(list, 'key')` over one key of every item.
119
+ * - `round(x, digits?)`, `abs(x)`.
120
+ * - `today(offset?)`: today, or `offset` days from today, as `YYYY-MM-DD`.
121
+ * - `days(from, to)`: whole days from one date to the other.
122
+ * - `contains(list or text, x)`: like `x in list`.
123
+ */
124
+ export declare const builtinFunctions: Readonly<Record<string, FormFunction>>;
125
+ /**
126
+ * The `values` path a member chain reads, e.g. `contact.phone`, or
127
+ * `undefined` when the chain does not start at `values`. A key computed
128
+ * when it runs stops the path there: `values.rows[i].name` reads `rows`,
129
+ * and `values[key]` reads everything (`''`).
130
+ */
131
+ export declare function pathOf(node: ExpressionNode, root?: 'values' | 'item'): string | undefined;
132
+ /** The computed keys along a member chain, e.g. `i` in `values.rows[i]`. */
133
+ export declare function computedKeys(node: ExpressionNode): ExpressionNode[];
134
+ /**
135
+ * Parse an expression once and get a function that runs it. Results are
136
+ * cached by source text, so every field with the same expression shares
137
+ * one. Throws an `ExpressionError` when it does not parse.
138
+ */
139
+ export declare function compileExpression(source: string): CompiledExpression;
140
+ /**
141
+ * Like `compileExpression`, but warns once and gives `undefined` for an
142
+ * expression that does not parse, for code that renders whatever it is given.
143
+ */
144
+ export declare function tryCompileExpression(source: string): CompiledExpression | undefined;
145
+ /** Parse and run an expression in one go. */
146
+ export declare function evaluateExpression(source: string, scope: ExpressionScope): unknown;
147
+ export interface ExpressionProblem {
148
+ /** What is wrong; an editor shows its own words for each code. */
149
+ code: ExpressionErrorCode | 'unknown-field' | 'unknown-function';
150
+ /** An English description, for logs. */
151
+ message: string;
152
+ /** The source text at fault, e.g. the field path or function name, for the editor's message. */
153
+ text: string;
154
+ start: number;
155
+ end: number;
156
+ }
157
+ /**
158
+ * The problems of an expression, for an editor to point at: a syntax
159
+ * error, or a field or function that does not exist. `fields` are the
160
+ * names of the form's fields; a path inside one (`contact.phone` for a
161
+ * field `contact`) or around one (`contact` for `contact.phone`) counts.
162
+ */
163
+ export interface CheckExpressionOptions {
164
+ /** The fields `values` can read; reading another is a problem. */
165
+ fields?: readonly string[];
166
+ /** The fields of a list item that `item` can read; an empty list where there is no item. */
167
+ itemFields?: readonly string[];
168
+ /** Functions besides the built-in ones. */
169
+ functions?: readonly string[];
170
+ }
171
+ export declare function checkExpression(source: string, options?: CheckExpressionOptions): ExpressionProblem[];
172
+ /** Whether a read of `path` can see the field `name`: the same path, or one inside the other. */
173
+ export declare function related(name: string, path: string): boolean;
174
+ /** Today as `YYYY-MM-DD`, in local time. */
175
+ export declare const formatToday: () => string;
@@ -0,0 +1,44 @@
1
+ import type { CompiledExpression, FormFunction } from './expression';
2
+ import type { FormDocument, FormFieldNode, FormListNode } from './types';
3
+ export interface FormulaField {
4
+ name: string;
5
+ expression: CompiledExpression;
6
+ }
7
+ /** Every formula of a document, worked out once. */
8
+ export interface FormulaAnalysis {
9
+ /** The document's formulas that can run, in document order. */
10
+ formulas: FormulaField[];
11
+ /** The item formulas that can run, per list. */
12
+ items: Map<FormListNode, FormulaField[]>;
13
+ /** The circles, each as the formulas on it. */
14
+ cycles: {
15
+ node: FormFieldNode;
16
+ label: string;
17
+ }[][];
18
+ /** The ids of the fields on a circle, which are not worked out. */
19
+ blocked: Set<string>;
20
+ }
21
+ /**
22
+ * The fields whose formulas read one another in a circle, each circle as
23
+ * the names on it, e.g. `[['total', 'tax']]`. A formula reading its own
24
+ * field is a circle of one. The items' fields are named with their list's,
25
+ * e.g. `rows.total`; a circle can go through the document's formulas and
26
+ * the items' alike. Pass the form's `functions` (or their names) when it
27
+ * has its own `sum`, `min`, `max`, `len` or `empty`, which may read any key of the items.
28
+ */
29
+ export declare function findFormulaCycles(document: FormDocument, functions?: Readonly<Record<string, FormFunction>> | readonly string[]): string[][];
30
+ /**
31
+ * Every formula of a document, the items' included, as one set: a
32
+ * formula reading a list reads all its items' formulas, and an item's
33
+ * formula reading `values` reads the document's.
34
+ */
35
+ export declare function analyseFormulas(document: FormDocument, functions?: Readonly<Record<string, FormFunction>> | readonly string[]): FormulaAnalysis;
36
+ /**
37
+ * Works out every formula of the form at once, the items' included, and
38
+ * writes the results into `values`, going over them again until nothing
39
+ * changes. Formulas on a circle are left out. The renderer's watchers keep
40
+ * results up to date in the page; on the server, where watchers run only
41
+ * once, this gives the same values the page will have, so a formula reading
42
+ * the items' formulas is not left behind.
43
+ */
44
+ export declare function settleFormulas(document: FormDocument, values: Record<string, unknown>, functions?: Readonly<Record<string, FormFunction>>, analysis?: FormulaAnalysis): void;
@@ -0,0 +1,23 @@
1
+ import type { FormRule } from 'dou-ui';
2
+ import type { FormFunction } from './expression';
3
+ import type { FormFieldNode, FormListNode, FormRuleSpec, FormValidator } from './types';
4
+ /** One rule as data, as the form's own rule. */
5
+ export declare function toFormRule(spec: FormRuleSpec, options?: RulesOptions): FormRule;
6
+ export interface RulesOptions {
7
+ /** Whether the field is required, when that is decided elsewhere. Defaults to the node's `required`. */
8
+ required?: boolean;
9
+ /** What `check` expressions can call besides the built-in functions. */
10
+ functions?: Readonly<Record<string, FormFunction>>;
11
+ /** The checks a rule's `validator` names. */
12
+ validators?: Readonly<Record<string, FormValidator>>;
13
+ /** Inside a list, the item the field belongs to, read as `item` by `check`. */
14
+ item?: () => unknown;
15
+ /** The field's path in the form, handed to validators. */
16
+ path?: () => string;
17
+ }
18
+ /**
19
+ * The rules of a field node, `required` first. Cached per node while its
20
+ * `rules`, whether it is required and the other options stay the same, so
21
+ * re-renders hand the form item the same array.
22
+ */
23
+ export declare function rulesOf(node: FormFieldNode | FormListNode, options?: RulesOptions): FormRule[];
@@ -0,0 +1,211 @@
1
+ import type { FormLabelAlign, FormLayout, FormModel, FormRequiredMark, FormValidatorResult, Size } from 'dou-ui';
2
+ /** The form widths a responsive value switches at: 480, 640, 800 and 960px. */
3
+ export type FormBreakpoint = 'sm' | 'md' | 'lg' | 'xl';
4
+ /**
5
+ * A value that may change with the width of the form (not of the
6
+ * viewport, so a form in a narrow dialog stacks like it would on a
7
+ * phone). Each breakpoint applies from its width up, until a larger one
8
+ * takes over; `base` applies below every breakpoint.
9
+ */
10
+ export type FormResponsive<T> = T | ({
11
+ base?: T;
12
+ } & Partial<Record<FormBreakpoint, T>>);
13
+ /**
14
+ * A whole form, as plain data that survives `JSON.stringify`, so it can
15
+ * be stored, sent and edited by the builder.
16
+ */
17
+ export interface FormDocument {
18
+ /** The shape of this data, for future migrations. Always `1` for now. */
19
+ version: 1;
20
+ /** Passed to the form: labels above (`vertical`), beside (`horizontal`) or a row of fields (`inline`). */
21
+ layout?: FormLayout;
22
+ labelAlign?: FormLabelAlign;
23
+ /** Width of the label column in the horizontal layout, in pixels. Defaults to 96. */
24
+ labelWidth?: number;
25
+ size?: Size;
26
+ requiredMark?: FormRequiredMark;
27
+ /** How many columns the layout grid has. Defaults to 12. */
28
+ columns?: number;
29
+ /** The fields and groups, in order. */
30
+ nodes: FormNode[];
31
+ }
32
+ export type FormNode = FormFieldNode | FormSectionNode | FormTabsNode | FormStepsNode | FormPageNode | FormListNode;
33
+ /** The nodes that hold other nodes. */
34
+ export type FormContainerNode = FormSectionNode | FormTabsNode | FormStepsNode | FormPageNode | FormListNode;
35
+ interface FormNodeBase {
36
+ /** Unique within the document, and kept when the node moves or changes. */
37
+ id: string;
38
+ /** How many grid columns the node covers. Defaults to the whole row. */
39
+ span?: FormResponsive<number>;
40
+ /**
41
+ * An expression; the node shows only while it is true, e.g.
42
+ * `values.type == 'laptop'`. A hidden field is not checked on submit.
43
+ */
44
+ visibleWhen?: string;
45
+ /** An expression; the node, and everything in a group, is disabled while it is true. */
46
+ disabledWhen?: string;
47
+ }
48
+ export interface FormFieldNode extends FormNodeBase {
49
+ /** The field type, e.g. `input` or `select`, or one registered with `defineFormField`. */
50
+ type: string;
51
+ /**
52
+ * The path the value is written to, e.g. `name` or `contact.phone`.
53
+ * Needed by every field that holds a value; display-only types such as
54
+ * `text` or `divider` go without.
55
+ */
56
+ name?: string;
57
+ label?: string;
58
+ /** A hint shown below the field. */
59
+ help?: string;
60
+ /** The value the field starts with. Defaults to the type's empty value. */
61
+ default?: unknown;
62
+ /** Shorthand for a `{ required: true }` rule. */
63
+ required?: boolean;
64
+ /** An expression; the field is required while it is true. Takes over from `required`. */
65
+ requiredWhen?: string;
66
+ /**
67
+ * An expression whose result the field holds, e.g.
68
+ * `values.price * values.count`. It is worked out again whenever a value
69
+ * it reads changes, and the field cannot be edited.
70
+ */
71
+ formula?: string;
72
+ /**
73
+ * Where the options come from, for a type with options such as `select`:
74
+ * the name of a source given to the renderer's `sources`, and an
75
+ * expression for what to ask it, e.g. `{ name: 'cities', arg: 'values.province' }`.
76
+ * Asked again when the result of `arg` changes; a value the new options
77
+ * no longer offer is cleared, unless `keep` is set.
78
+ */
79
+ source?: FormOptionSource;
80
+ /** Checks the value must pass, besides `required`. */
81
+ rules?: FormRuleSpec[];
82
+ /** Settings of the field type: a placeholder, the options of a select, a range… */
83
+ props?: Record<string, unknown>;
84
+ }
85
+ /** A titled group of nodes with its own grid. */
86
+ export interface FormSectionNode extends FormNodeBase {
87
+ type: 'section';
88
+ title?: string;
89
+ description?: string;
90
+ /** Draw a border around the group. */
91
+ bordered?: boolean;
92
+ /** Columns of the group's own grid. Defaults to the document's. */
93
+ columns?: number;
94
+ children: FormNode[];
95
+ }
96
+ export interface FormOptionSource {
97
+ /** The key of the source in the renderer's `sources`. */
98
+ name: string;
99
+ /**
100
+ * An expression; its result is handed to the source. To hand over
101
+ * several values, make it a list: `[values.province, values.kind]`.
102
+ */
103
+ arg?: string;
104
+ /** Keep the field's value when the options change and no longer offer it. */
105
+ keep?: boolean;
106
+ }
107
+ /** Pages shown one at a time, picked from a row of tabs. */
108
+ export interface FormTabsNode extends FormNodeBase {
109
+ type: 'tabs';
110
+ /** The pages, one per tab. */
111
+ children: FormPageNode[];
112
+ }
113
+ /**
114
+ * Pages filled in one after another. Each page is checked before the
115
+ * next one opens; going back is always allowed.
116
+ */
117
+ export interface FormStepsNode extends FormNodeBase {
118
+ type: 'steps';
119
+ /** The pages, one per step. */
120
+ children: FormPageNode[];
121
+ }
122
+ /** One page of `tabs` or `steps`. A hidden page (`visibleWhen`) is left out. */
123
+ export interface FormPageNode extends FormNodeBase {
124
+ type: 'page';
125
+ /** The text of the tab or step. */
126
+ title: string;
127
+ description?: string;
128
+ /** Columns of the page's own grid. Defaults to the document's. */
129
+ columns?: number;
130
+ children: FormNode[];
131
+ }
132
+ /**
133
+ * A list of items that people add, remove and reorder, each made of the
134
+ * same fields, e.g. the people on a booking. The value at `name` is a
135
+ * list of objects; the fields inside name their own key in an item
136
+ * (`name: 'phone'` writes `item.phone`) and read the item in
137
+ * expressions as `item`.
138
+ */
139
+ export interface FormListNode extends FormNodeBase {
140
+ type: 'list';
141
+ /** The path of the list, e.g. `guests`. */
142
+ name: string;
143
+ label?: string;
144
+ help?: string;
145
+ /** The items the list starts with. Defaults to `min` empty items. */
146
+ default?: Record<string, unknown>[];
147
+ /** Fewest items; below it items cannot be removed. */
148
+ min?: number;
149
+ /** Most items; at it no more can be added. */
150
+ max?: number;
151
+ /** Let people drag items into another order. Defaults to `true`. */
152
+ sortable?: boolean;
153
+ /** Text of the add button. Defaults to the locale's. */
154
+ addText?: string;
155
+ /** Checks of the list itself, e.g. `required` for at least one item. */
156
+ required?: boolean;
157
+ rules?: FormRuleSpec[];
158
+ /** Columns of each item's grid. Defaults to the document's. */
159
+ columns?: number;
160
+ /** The fields of one item. */
161
+ children: FormNode[];
162
+ }
163
+ /**
164
+ * A check as plain data. Turned into a `FormRule` of the form, so it
165
+ * behaves the same as rules written by hand.
166
+ */
167
+ export interface FormRuleSpec {
168
+ /** The value must not be empty. */
169
+ required?: boolean;
170
+ type?: 'string' | 'number' | 'integer' | 'email' | 'url';
171
+ /** The least length of text, number of items, or number. */
172
+ min?: number;
173
+ /** The greatest length of text, number of items, or number. */
174
+ max?: number;
175
+ /** A regular expression text must match, as source text without slashes, e.g. `^\\d{6}$`. */
176
+ pattern?: string;
177
+ /** Flags for `pattern`, e.g. `i`. */
178
+ flags?: string;
179
+ /**
180
+ * An expression the value must make true, e.g. `value >= values.start`.
181
+ * `value` is the field's value and `values` the whole form. Like `min`
182
+ * and `pattern` it skips an empty value; that is `required`'s job.
183
+ */
184
+ check?: string;
185
+ /**
186
+ * The name of a check given to the renderer's `validators`, for what an
187
+ * expression cannot do, e.g. asking the server whether a name is taken.
188
+ * Skips an empty value too. Ignored when the rule has a `check`.
189
+ */
190
+ validator?: string;
191
+ /** Replaces the default message. */
192
+ message?: string;
193
+ trigger?: 'change' | 'blur';
194
+ }
195
+ /** What a validator gets besides the value. */
196
+ export interface FormValidatorContext {
197
+ /** The whole form's values. */
198
+ values: FormModel;
199
+ /** Inside a list, the item the field belongs to; else `null`. */
200
+ item: unknown;
201
+ /** The path of the field, e.g. `username` or `guests.2.phone`. */
202
+ path: string;
203
+ }
204
+ /**
205
+ * A check of a field's value. Return `true` or nothing to pass, `false` to
206
+ * fail with the rule's `message`, or a string to fail with that string.
207
+ * May be async. Like the other checks of a rule, it is skipped while the
208
+ * value is empty; that is `required`'s job.
209
+ */
210
+ export type FormValidator = (value: unknown, context: FormValidatorContext) => FormValidatorResult | Promise<FormValidatorResult>;
211
+ export {};