@sealcode/sealgen 0.8.0 → 0.8.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/@types/forms/controls/autocomplete.d.ts +20 -0
- package/@types/forms/controls/checkbox-group.d.ts +21 -0
- package/@types/forms/controls/checkbox.d.ts +8 -1
- package/@types/forms/controls/collapsible-checkbox-group.d.ts +12 -0
- package/@types/forms/controls/collapsible-field-group.d.ts +21 -0
- package/@types/forms/controls/controls.d.ts +4 -0
- package/@types/forms/controls/field-group.d.ts +12 -4
- package/@types/forms/controls/simple-input.d.ts +9 -2
- package/@types/forms/controls/tickable.d.ts +9 -12
- package/@types/forms/fields/enum-multiple.d.ts +18 -0
- package/@types/forms/fields/existing-sealious-value.d.ts +6 -0
- package/@types/forms/fields/field.d.ts +8 -4
- package/@types/forms/fields/proxy-field.d.ts +13 -0
- package/@types/page/list.d.ts +3 -3
- package/@types/utils/input-wrapper.d.ts +2 -1
- package/lib/forms/controls/autocomplete.js +27 -0
- package/lib/forms/controls/autocomplete.js.map +1 -0
- package/lib/forms/controls/checkbox-group.js +27 -0
- package/lib/forms/controls/checkbox-group.js.map +1 -0
- package/lib/forms/controls/checkbox.js +12 -1
- package/lib/forms/controls/checkbox.js.map +1 -1
- package/lib/forms/controls/collapsible-checkbox-group.js +17 -0
- package/lib/forms/controls/collapsible-checkbox-group.js.map +1 -0
- package/lib/forms/controls/collapsible-field-group.js +82 -0
- package/lib/forms/controls/collapsible-field-group.js.map +1 -0
- package/lib/forms/controls/controls.js +4 -0
- package/lib/forms/controls/controls.js.map +1 -1
- package/lib/forms/controls/field-group.js +39 -8
- package/lib/forms/controls/field-group.js.map +1 -1
- package/lib/forms/controls/simple-input.js +64 -29
- package/lib/forms/controls/simple-input.js.map +1 -1
- package/lib/forms/controls/tickable.js +4 -2
- package/lib/forms/controls/tickable.js.map +1 -1
- package/lib/forms/fields/boolean.js +7 -0
- package/lib/forms/fields/boolean.js.map +1 -1
- package/lib/forms/fields/enum-multiple.js +45 -0
- package/lib/forms/fields/enum-multiple.js.map +1 -0
- package/lib/forms/fields/existing-sealious-value.js +20 -0
- package/lib/forms/fields/existing-sealious-value.js.map +1 -0
- package/lib/forms/fields/field.js +7 -0
- package/lib/forms/fields/field.js.map +1 -1
- package/lib/forms/fields/pick-from-list.js +1 -1
- package/lib/forms/fields/pick-from-list.js.map +1 -1
- package/lib/forms/fields/proxy-field.js +24 -0
- package/lib/forms/fields/proxy-field.js.map +1 -0
- package/lib/mount.js +1 -0
- package/lib/mount.js.map +1 -1
- package/lib/page/list.js +9 -6
- package/lib/page/list.js.map +1 -1
- package/lib/sanitize.js +1 -1
- package/lib/sanitize.js.map +1 -1
- package/lib/utils/input-wrapper.js.map +1 -1
- package/package.json +2 -2
- package/src/forms/controls/autocomplete.ts +46 -0
- package/src/forms/controls/checkbox-group.ts +81 -0
- package/src/forms/controls/checkbox.ts +16 -1
- package/src/forms/controls/collapsible-checkbox-group.ts +31 -0
- package/src/forms/controls/collapsible-field-group.ts +96 -0
- package/src/forms/controls/controls.ts +4 -0
- package/src/forms/controls/field-group.ts +60 -9
- package/src/forms/controls/simple-input.ts +90 -42
- package/src/forms/controls/tickable.ts +14 -10
- package/src/forms/fields/boolean.ts +6 -0
- package/src/forms/fields/enum-multiple.ts +58 -0
- package/src/forms/fields/existing-sealious-value.ts +22 -0
- package/src/forms/fields/field.ts +14 -2
- package/src/forms/fields/pick-from-list.ts +1 -1
- package/src/forms/fields/proxy-field.ts +24 -0
- package/src/forms/forms.scss +11 -0
- package/src/mount.ts +1 -0
- package/src/page/list.ts +12 -10
- package/src/sanitize.ts +1 -1
- package/src/utils/input-wrapper.ts +5 -2
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable } from "tempstream";
|
|
3
|
+
import { FormDataValue, FormMessage } from "../form";
|
|
4
|
+
import { CheckboxWithValue } from "./checkbox";
|
|
5
|
+
import { FieldGroup, FormFieldControl } from "./controls";
|
|
6
|
+
import { CheckboxGroup, DefaultCheckboxOptions } from "./checkbox-group";
|
|
7
|
+
import { CollapsibleFieldGroup } from "./collapsible-field-group";
|
|
8
|
+
|
|
9
|
+
export class CollapsibleCheckboxGroup extends CheckboxGroup<
|
|
10
|
+
DefaultCheckboxOptions & {
|
|
11
|
+
textWhenOpen?: string;
|
|
12
|
+
textWhenClosed?: string;
|
|
13
|
+
groupID?: string;
|
|
14
|
+
}
|
|
15
|
+
> {
|
|
16
|
+
wrapGroup(
|
|
17
|
+
ctx: Context,
|
|
18
|
+
data: Record<string, FormDataValue>,
|
|
19
|
+
messages: FormMessage[],
|
|
20
|
+
field_prefix: string,
|
|
21
|
+
form_id: string,
|
|
22
|
+
checkboxes: CheckboxWithValue[]
|
|
23
|
+
): Promise<FlatTemplatable> {
|
|
24
|
+
return new CollapsibleFieldGroup(checkboxes, {
|
|
25
|
+
label: this.options.label,
|
|
26
|
+
textWhenOpen: this.options.textWhenOpen,
|
|
27
|
+
textWhenClosed: this.options.textWhenClosed,
|
|
28
|
+
groupID: this.options.groupID,
|
|
29
|
+
}).render(ctx, data, messages, field_prefix, form_id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
+
import { FormDataValue, FormMessage } from "../form";
|
|
4
|
+
import { FieldGroup } from "./field-group";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { FormControl } from "./form-control";
|
|
7
|
+
|
|
8
|
+
type CollapsibleFieldGroupOptions = {
|
|
9
|
+
label?: string;
|
|
10
|
+
classes?: string;
|
|
11
|
+
openOnStart?: boolean;
|
|
12
|
+
textWhenOpen?: string;
|
|
13
|
+
textWhenClosed?: string;
|
|
14
|
+
groupID?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class CollapsibleFieldGroup extends FieldGroup {
|
|
18
|
+
public collapseID: string;
|
|
19
|
+
constructor(
|
|
20
|
+
public controls: FormControl[],
|
|
21
|
+
public options: CollapsibleFieldGroupOptions = {}
|
|
22
|
+
) {
|
|
23
|
+
super([]);
|
|
24
|
+
if (this.options.groupID == undefined) {
|
|
25
|
+
this.options.groupID = "A" + randomUUID(); // "A" is needed as ids always have to start with a letter
|
|
26
|
+
}
|
|
27
|
+
this.collapseID = this.options.groupID + "collapse";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getGroupLabelProps() {
|
|
31
|
+
return super.getGroupLabelProps() + ` for="${this.collapseID}"`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
preLabelContainer(): FlatTemplatable {
|
|
35
|
+
return /* HTML */ `<style>
|
|
36
|
+
input#${this.collapseID} {
|
|
37
|
+
display: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
input#${this.collapseID} ~ .field-group__fields {
|
|
41
|
+
display: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
input#${this.collapseID}:checked ~ .field-group__fields {
|
|
45
|
+
display: block;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
input#${this.collapseID}
|
|
49
|
+
~ .field-group__label-container
|
|
50
|
+
.field-group__collapse-label-text--open {
|
|
51
|
+
display: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
input#${this.collapseID}:checked
|
|
55
|
+
~ .field-group__label-container
|
|
56
|
+
.field-group__collapse-label-text--open {
|
|
57
|
+
display: inline-block;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
input#${this.collapseID}
|
|
61
|
+
~ .field-group__label-container
|
|
62
|
+
.field-group__collapse-label-text--closed {
|
|
63
|
+
display: inline-block;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
input#${this.collapseID}:checked
|
|
67
|
+
~ .field-group__label-container
|
|
68
|
+
.field-group__collapse-label-text--closed {
|
|
69
|
+
display: none;
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
72
|
+
<input
|
|
73
|
+
type="checkbox"
|
|
74
|
+
id="${this.collapseID}"
|
|
75
|
+
${this.options.openOnStart ? "checked" : ""}
|
|
76
|
+
/>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
postLabel(): FlatTemplatable {
|
|
80
|
+
return /* HTML */ `<label
|
|
81
|
+
class="field-group__collapse-label"
|
|
82
|
+
for="${this.collapseID}"
|
|
83
|
+
>
|
|
84
|
+
<span
|
|
85
|
+
class="field-group__collapse-label-text field-group__collapse-label-text--open"
|
|
86
|
+
>
|
|
87
|
+
${this.options.textWhenOpen || "close"}
|
|
88
|
+
</span>
|
|
89
|
+
<span
|
|
90
|
+
class="field-group__collapse-label-text field-group__collapse-label-text--closed"
|
|
91
|
+
>
|
|
92
|
+
${this.options.textWhenClosed || "open"}
|
|
93
|
+
</span>
|
|
94
|
+
</label>`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -16,3 +16,7 @@ export * from "./tickable";
|
|
|
16
16
|
export * from "./checkbox";
|
|
17
17
|
export * from "./radio";
|
|
18
18
|
export * from "./big-radio-group";
|
|
19
|
+
export * from "./checkbox-group";
|
|
20
|
+
export * from "./collapsible-field-group";
|
|
21
|
+
export * from "./collapsible-checkbox-group";
|
|
22
|
+
export * from "./autocomplete";
|
|
@@ -1,32 +1,83 @@
|
|
|
1
1
|
import { Context } from "koa";
|
|
2
2
|
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
-
import { FormDataValue } from "../form";
|
|
3
|
+
import { FormDataValue, FormMessage } from "../form";
|
|
4
4
|
import { FormControl } from "./form-control";
|
|
5
|
+
import { FormFieldControl } from "./form-field-control";
|
|
5
6
|
|
|
6
|
-
export class FieldGroup extends
|
|
7
|
+
export class FieldGroup extends FormFieldControl {
|
|
7
8
|
constructor(
|
|
8
9
|
public controls: FormControl[],
|
|
9
|
-
public options: {
|
|
10
|
+
public options: {
|
|
11
|
+
label?: string;
|
|
12
|
+
classes?: string;
|
|
13
|
+
containerID?: string;
|
|
14
|
+
} = {}
|
|
10
15
|
) {
|
|
11
|
-
super();
|
|
16
|
+
super([]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
preLabel(): FlatTemplatable {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
preLabelContainer(): FlatTemplatable {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
postLabel(): FlatTemplatable {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
preFields(): FlatTemplatable {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
postFields(): FlatTemplatable {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getGroupLabelProps(): FlatTemplatable {
|
|
40
|
+
return `class="field-group__label"`;
|
|
12
41
|
}
|
|
13
42
|
|
|
14
43
|
async render(
|
|
15
44
|
ctx: Context,
|
|
16
|
-
data: Record<string, FormDataValue
|
|
45
|
+
data: Record<string, FormDataValue>,
|
|
46
|
+
messages: FormMessage[],
|
|
47
|
+
field_prefix: string,
|
|
48
|
+
form_id: string
|
|
17
49
|
): Promise<FlatTemplatable> {
|
|
18
50
|
const { label, classes } = this.options;
|
|
19
51
|
|
|
20
52
|
return tempstream/* HTML */ `
|
|
21
|
-
<div
|
|
22
|
-
${
|
|
53
|
+
<div
|
|
54
|
+
class="field-group ${classes || ""}"
|
|
55
|
+
${this.options.containerID
|
|
56
|
+
? `id="${this.options.containerID}"`
|
|
57
|
+
: ""}
|
|
58
|
+
>
|
|
59
|
+
${this.preLabelContainer()}
|
|
60
|
+
<div class="field-group__label-container">
|
|
61
|
+
${this.preLabel()}
|
|
62
|
+
${label
|
|
63
|
+
? `<label ${this.getGroupLabelProps()}>${label}</label>`
|
|
64
|
+
: ""}
|
|
65
|
+
${this.postLabel()}
|
|
66
|
+
</div>
|
|
67
|
+
${this.preFields()}
|
|
23
68
|
<div class="field-group__fields">
|
|
24
69
|
${this.controls.map((control) =>
|
|
25
|
-
control.render(
|
|
70
|
+
control.render(
|
|
71
|
+
ctx,
|
|
72
|
+
data,
|
|
73
|
+
messages,
|
|
74
|
+
field_prefix,
|
|
75
|
+
form_id
|
|
76
|
+
)
|
|
26
77
|
)}
|
|
27
78
|
</div>
|
|
79
|
+
${this.postFields()}
|
|
28
80
|
</div>
|
|
29
81
|
`;
|
|
30
82
|
}
|
|
31
|
-
role = <const>"decoration";
|
|
32
83
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Context } from "koa";
|
|
2
|
-
import { FlatTemplatable } from "tempstream";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
3
|
import { attribute } from "../../sanitize";
|
|
4
4
|
import { getRequiredClass, inputWrapper } from "../../utils/input-wrapper";
|
|
5
5
|
import { FormField } from "../fields/field";
|
|
6
|
-
import { FormDataValue } from "../form";
|
|
6
|
+
import { FormDataValue, FormMessage } from "../form";
|
|
7
7
|
import { FormFieldControl } from "./controls";
|
|
8
8
|
|
|
9
9
|
export class SimpleInput extends FormFieldControl {
|
|
@@ -37,58 +37,106 @@ export class SimpleInput extends FormFieldControl {
|
|
|
37
37
|
super([field]);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
getType(): string {
|
|
41
|
+
return this.options.type || "text";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getLabel(): string {
|
|
45
|
+
return this.options.label != undefined
|
|
46
|
+
? this.options.label
|
|
47
|
+
: this.field.name;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getID(): string {
|
|
51
|
+
return this.options.id || this.field.name;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getPlaceholder(): string {
|
|
55
|
+
return this.options.placeholder || this.getType();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getInputAttributes(
|
|
59
|
+
ctx: Context,
|
|
60
|
+
data: Record<string, FormDataValue>,
|
|
61
|
+
field_prefix: string,
|
|
62
|
+
form_id: string
|
|
63
|
+
): Promise<Record<string, string | true>> {
|
|
64
|
+
const readonly = this.options.readonly || false;
|
|
65
|
+
const required = this.field.required;
|
|
66
|
+
const { parsed, raw, valid } = await this.field.getValue(ctx, data);
|
|
67
|
+
return Object.fromEntries([
|
|
68
|
+
<const>["id", this.getID()],
|
|
69
|
+
<const>["type", this.getType()],
|
|
70
|
+
<const>["name", `${field_prefix}${this.field.name}`],
|
|
71
|
+
<const>[
|
|
72
|
+
"value",
|
|
73
|
+
!valid && raw
|
|
74
|
+
? raw.toString()
|
|
75
|
+
: parsed !== undefined && parsed !== null
|
|
76
|
+
? ((parsed as any)?.toString() as string) || ""
|
|
77
|
+
: "",
|
|
78
|
+
],
|
|
79
|
+
...(form_id ? [["form", form_id]] : []),
|
|
80
|
+
<const>["placeholder", this.getPlaceholder()],
|
|
81
|
+
...(readonly ? [["readonly", true]] : []),
|
|
82
|
+
...(required ? [["required", true]] : []),
|
|
83
|
+
...(!this.options.autocomplete ? [["autocomplete", "off"]] : []),
|
|
84
|
+
...(this.options.step ? [["step", this.options.step]] : []),
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
renderAttributes(attrs: Record<string, string | true>) {
|
|
89
|
+
return Object.entries(attrs)
|
|
90
|
+
.map(([key, value]) => {
|
|
91
|
+
if (value === true) {
|
|
92
|
+
return key;
|
|
93
|
+
} else {
|
|
94
|
+
return `${key}="${attribute(value)}"`;
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.join(" ");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async postInput(ctx: Context): Promise<FlatTemplatable> {
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
|
|
40
104
|
async render<Fields extends Record<string, FormField>>(
|
|
41
105
|
ctx: Context,
|
|
42
106
|
data: Record<string, FormDataValue>,
|
|
43
|
-
|
|
107
|
+
_messages: FormMessage[],
|
|
44
108
|
field_prefix: string,
|
|
45
109
|
form_id: string
|
|
46
110
|
): Promise<FlatTemplatable> {
|
|
47
|
-
const id = this.
|
|
48
|
-
const label =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
: this.field.name;
|
|
52
|
-
const type = this.options.type || "text";
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
111
|
+
const id = this.getID();
|
|
112
|
+
const label = this.getLabel();
|
|
113
|
+
const type = this.getType();
|
|
114
|
+
|
|
54
115
|
const { parsed, raw, valid, message } = await this.field.getValue(
|
|
55
116
|
ctx,
|
|
56
117
|
data
|
|
57
118
|
);
|
|
58
|
-
|
|
59
|
-
const placeholder = this.options.placeholder || type;
|
|
60
|
-
const readonly = this.options.readonly || false;
|
|
61
|
-
const required = this.field.required;
|
|
119
|
+
const placeholder = this.getPlaceholder();
|
|
62
120
|
|
|
63
|
-
return
|
|
64
|
-
[id, type, getRequiredClass(required)],
|
|
65
|
-
`
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
${form_id ? `form="${form_id}"` : ""}
|
|
80
|
-
placeholder="${placeholder}"
|
|
81
|
-
${readonly ? "readonly" : ""}
|
|
82
|
-
${required ? "required" : ""}
|
|
83
|
-
${!this.options.autocomplete ? `autocomplete="off"` : ""}
|
|
84
|
-
${this.options.step ? `step="${this.options.step}"` : ""}
|
|
85
|
-
/>
|
|
86
|
-
${
|
|
87
|
-
~valid && !this.options.hide_errors
|
|
121
|
+
return inputWrapper(
|
|
122
|
+
[id, type, getRequiredClass(this.field.required)],
|
|
123
|
+
tempstream/* HTML */ `
|
|
124
|
+
<label for="${id}">${label}</label>
|
|
125
|
+
<input
|
|
126
|
+
${this.renderAttributes(
|
|
127
|
+
await this.getInputAttributes(
|
|
128
|
+
ctx,
|
|
129
|
+
data,
|
|
130
|
+
field_prefix,
|
|
131
|
+
form_id
|
|
132
|
+
)
|
|
133
|
+
)}
|
|
134
|
+
/>
|
|
135
|
+
${this.postInput(ctx)}
|
|
136
|
+
${~valid && !this.options.hide_errors
|
|
88
137
|
? `<div class="input__error">${message}</div>`
|
|
89
|
-
: ""
|
|
90
|
-
|
|
91
|
-
`
|
|
138
|
+
: ""}
|
|
139
|
+
`
|
|
92
140
|
);
|
|
93
141
|
}
|
|
94
142
|
}
|
|
@@ -6,15 +6,18 @@ import { FormField } from "../fields/field";
|
|
|
6
6
|
import { FormDataValue } from "../form";
|
|
7
7
|
import { FormFieldControl } from "./controls";
|
|
8
8
|
|
|
9
|
+
export type TickableOptions = {
|
|
10
|
+
id?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
hide_errors?: boolean;
|
|
14
|
+
readonly?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
9
17
|
export abstract class Tickable<T extends unknown> extends FormFieldControl {
|
|
10
18
|
constructor(
|
|
11
19
|
public field: FormField<boolean, T>,
|
|
12
|
-
public options: {
|
|
13
|
-
id?: string;
|
|
14
|
-
label?: string;
|
|
15
|
-
hide_errors?: boolean;
|
|
16
|
-
readonly?: boolean;
|
|
17
|
-
} = {}
|
|
20
|
+
public options: TickableOptions = {}
|
|
18
21
|
) {
|
|
19
22
|
super([field]);
|
|
20
23
|
}
|
|
@@ -69,6 +72,8 @@ export abstract class Tickable<T extends unknown> extends FormFieldControl {
|
|
|
69
72
|
const readonly = this.options.readonly || false;
|
|
70
73
|
const required = this.field.required;
|
|
71
74
|
|
|
75
|
+
const value = this.getValueAttribute(ctx, data, parsed);
|
|
76
|
+
|
|
72
77
|
return inputWrapper(
|
|
73
78
|
this.getWrapperClasses(ctx, data, parsed),
|
|
74
79
|
|
|
@@ -76,10 +81,9 @@ export abstract class Tickable<T extends unknown> extends FormFieldControl {
|
|
|
76
81
|
<input
|
|
77
82
|
id="${id}"
|
|
78
83
|
type="${this.type}"
|
|
79
|
-
name="${field_prefix}${this.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)}"
|
|
84
|
+
name="${field_prefix}${this.options.name ||
|
|
85
|
+
this.field.name}"
|
|
86
|
+
${value ? `value="${attribute(value)}"` : ""}
|
|
83
87
|
${form_id ? `form="${form_id}"` : ""}
|
|
84
88
|
${this.isChecked(ctx, data, parsed) ? "checked" : ""}
|
|
85
89
|
${readonly ? "readonly" : ""}
|
|
@@ -40,6 +40,12 @@ export class Boolean<Required extends boolean> extends FormField<
|
|
|
40
40
|
parsed_value: truthy.includes(value),
|
|
41
41
|
error: null,
|
|
42
42
|
};
|
|
43
|
+
} else if (is(value, predicates.undefined)) {
|
|
44
|
+
return {
|
|
45
|
+
parsable: true,
|
|
46
|
+
parsed_value: false,
|
|
47
|
+
error: null,
|
|
48
|
+
};
|
|
43
49
|
} else {
|
|
44
50
|
return {
|
|
45
51
|
parsable: false,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { is, predicates } from "@sealcode/ts-predicates";
|
|
2
|
+
import { Context } from "koa";
|
|
3
|
+
import { FormDataValue } from "../form";
|
|
4
|
+
import { FieldParseResult, FormField } from "./field";
|
|
5
|
+
|
|
6
|
+
export class EnumMultipleField<
|
|
7
|
+
Options extends readonly string[],
|
|
8
|
+
Required extends boolean
|
|
9
|
+
> extends FormField<Required, string[]> {
|
|
10
|
+
constructor(
|
|
11
|
+
required: Required,
|
|
12
|
+
public options: Options,
|
|
13
|
+
public error_message: (options: Options) => string = (options) =>
|
|
14
|
+
options.join(", ")
|
|
15
|
+
) {
|
|
16
|
+
super(required);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
predicate = predicates.enum<Options>(this.options);
|
|
20
|
+
|
|
21
|
+
public async isValueValid(
|
|
22
|
+
_: Context,
|
|
23
|
+
values: string[]
|
|
24
|
+
): Promise<{ valid: boolean; message: string }> {
|
|
25
|
+
for (const value of values) {
|
|
26
|
+
if (!is(value, predicates.string)) {
|
|
27
|
+
return { valid: false, message: "expected a string" };
|
|
28
|
+
}
|
|
29
|
+
if (!this.options.includes(value)) {
|
|
30
|
+
return { valid: false, message: "invalid value" };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { valid: true, message: "" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async parse(
|
|
37
|
+
_: Context,
|
|
38
|
+
value: FormDataValue
|
|
39
|
+
): Promise<FieldParseResult<string[]>> {
|
|
40
|
+
if (is(value, predicates.array(predicates.string))) {
|
|
41
|
+
return {
|
|
42
|
+
parsable: true,
|
|
43
|
+
parsed_value: value,
|
|
44
|
+
error: null,
|
|
45
|
+
};
|
|
46
|
+
} else {
|
|
47
|
+
return {
|
|
48
|
+
parsable: false,
|
|
49
|
+
parsed_value: null,
|
|
50
|
+
error: "Please enter a string",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public getEmptyValue(): string[] {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Field } from "sealious";
|
|
2
|
+
import { PickFromListField } from "./pick-from-list";
|
|
3
|
+
|
|
4
|
+
export class ExistingSealiousValue<
|
|
5
|
+
Required extends boolean
|
|
6
|
+
> extends PickFromListField<Required> {
|
|
7
|
+
constructor(required: Required, field: Field) {
|
|
8
|
+
super(required, async (ctx) => {
|
|
9
|
+
const { items } = await field.collection.list(ctx.$context).fetch();
|
|
10
|
+
return Object.fromEntries(
|
|
11
|
+
items.map((item) => {
|
|
12
|
+
const value = item.get(field.name);
|
|
13
|
+
return [value, value];
|
|
14
|
+
})
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getEmptyValue() {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -37,12 +37,16 @@ export function fieldsToShape<Fields extends Record<string, FormField>>(
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export type FieldParseResult<T> =
|
|
40
|
+
| {
|
|
41
|
+
parsable: true;
|
|
42
|
+
error: null;
|
|
43
|
+
parsed_value: T;
|
|
44
|
+
}
|
|
40
45
|
| {
|
|
41
46
|
parsable: false;
|
|
42
47
|
error: string;
|
|
43
48
|
parsed_value: null;
|
|
44
|
-
}
|
|
45
|
-
| { parsable: true; error: null; parsed_value: T };
|
|
49
|
+
};
|
|
46
50
|
|
|
47
51
|
export abstract class FormField<
|
|
48
52
|
Required extends boolean = boolean,
|
|
@@ -51,6 +55,7 @@ export abstract class FormField<
|
|
|
51
55
|
name: string;
|
|
52
56
|
constructor(public readonly required: Required) {}
|
|
53
57
|
predicate: Predicate = predicates.unknown;
|
|
58
|
+
mapToFilter: (parsed: ParsedValue) => unknown = (value) => value;
|
|
54
59
|
|
|
55
60
|
init(fieldname: string): void {
|
|
56
61
|
this.name = fieldname;
|
|
@@ -135,6 +140,11 @@ export abstract class FormField<
|
|
|
135
140
|
raw: all_form_values[this.name],
|
|
136
141
|
};
|
|
137
142
|
}
|
|
143
|
+
|
|
144
|
+
setMapToFilter(fn: (parsed: ParsedValue) => unknown) {
|
|
145
|
+
this.mapToFilter = fn;
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
export * from "./simple-form-field";
|
|
@@ -146,3 +156,5 @@ export * from "./regexp";
|
|
|
146
156
|
export * from "./email";
|
|
147
157
|
export * from "./enum";
|
|
148
158
|
export * from "./boolean";
|
|
159
|
+
export * from "./enum-multiple";
|
|
160
|
+
export * from "./existing-sealious-value";
|
|
@@ -38,7 +38,7 @@ export class PickFromListField<
|
|
|
38
38
|
if (!is(value, predicates.string)) {
|
|
39
39
|
return { valid: false, message: "not a string" };
|
|
40
40
|
}
|
|
41
|
-
if (!Object.keys(options).includes(value)) {
|
|
41
|
+
if (this.required && !Object.keys(options).includes(value)) {
|
|
42
42
|
return {
|
|
43
43
|
valid: false,
|
|
44
44
|
message: `"${value}" is not one of the options`,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FormField } from "./field";
|
|
2
|
+
|
|
3
|
+
export class ProxyFormField<T> extends FormField<boolean, T> {
|
|
4
|
+
constructor(
|
|
5
|
+
required: boolean,
|
|
6
|
+
public name: string,
|
|
7
|
+
public empty_value: T,
|
|
8
|
+
public parsed_value: T
|
|
9
|
+
) {
|
|
10
|
+
super(required);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getEmptyValue() {
|
|
14
|
+
return this.empty_value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async parse() {
|
|
18
|
+
return <const>{
|
|
19
|
+
parsable: true,
|
|
20
|
+
error: null,
|
|
21
|
+
parsed_value: this.parsed_value,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/forms/forms.scss
CHANGED
package/src/mount.ts
CHANGED
|
@@ -35,6 +35,7 @@ export function mount(router: Router, url: string, mountable: Mountable): void {
|
|
|
35
35
|
ctx.set("content-type", "text/html;charset=utf-8");
|
|
36
36
|
const access_result = await mountable.canAccess(ctx);
|
|
37
37
|
if (!access_result.canAccess) {
|
|
38
|
+
ctx.status = 403;
|
|
38
39
|
ctx.body = access_result.message || "no access";
|
|
39
40
|
return;
|
|
40
41
|
}
|