@sealcode/sealgen 0.8.48 → 0.8.49
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/.log/ti-107027.log +576 -0
- package/.log/ti-129414.log +1355 -0
- package/.log/ti-135707.log +1049 -0
- package/.log/ti-136226.log +1049 -0
- package/.log/ti-147652.log +533 -0
- package/.log/ti-157088.log +2938 -0
- package/.log/ti-250667.log +548 -0
- package/.log/ti-29438.log +165 -0
- package/.log/ti-29894.log +149 -0
- package/.log/ti-33785.log +149 -0
- package/.log/ti-34262.log +149 -0
- package/.log/ti-35719.log +149 -0
- package/.log/ti-41629.log +518 -0
- package/.log/ti-441248.log +517 -0
- package/.log/ti-5352.log +517 -0
- package/.log/ti-76335.log +639 -0
- package/.log/ti-811639.log +517 -0
- package/.log/ti-81805.log +564 -0
- package/.log/ti-86459.log +517 -0
- package/.log/tsserver.log +6881 -0
- package/.log/tsserver.sync-conflict-20221010-222251-JUX7UR2.log +9412 -0
- package/package.json +1 -1
- package/src/add-collection.ts +31 -0
- package/src/add-route.test.ts +58 -0
- package/src/add-route.ts +89 -0
- package/src/build.ts +94 -0
- package/src/cli.ts +36 -0
- package/src/components/components.ts +2 -0
- package/src/components/tabs.ts +95 -0
- package/src/components/toggle-modal.ts +52 -0
- package/src/example.test.ts +3 -0
- package/src/find-scss-includes.ts +32 -0
- package/src/forms/controls/array.ts +226 -0
- package/src/forms/controls/autocomplete.ts +33 -0
- package/src/forms/controls/big-radio-group.ts +48 -0
- package/src/forms/controls/checkbox-group.ts +71 -0
- package/src/forms/controls/checkbox.ts +34 -0
- package/src/forms/controls/checkboxed-list-input.ts +50 -0
- package/src/forms/controls/collapsible-checkbox-group.ts +28 -0
- package/src/forms/controls/collapsible-field-group.ts +94 -0
- package/src/forms/controls/controls.ts +30 -0
- package/src/forms/controls/derived-frame.ts +91 -0
- package/src/forms/controls/derived.ts +108 -0
- package/src/forms/controls/dropdown.ts +87 -0
- package/src/forms/controls/editable-collection-subset.ts +97 -0
- package/src/forms/controls/field-group.ts +69 -0
- package/src/forms/controls/file.ts +47 -0
- package/src/forms/controls/form-control.ts +24 -0
- package/src/forms/controls/form-field-control.ts +22 -0
- package/src/forms/controls/form-header.ts +19 -0
- package/src/forms/controls/form-paragraph.ts +11 -0
- package/src/forms/controls/frame.ts +26 -0
- package/src/forms/controls/hidden-input.ts +22 -0
- package/src/forms/controls/html.ts +38 -0
- package/src/forms/controls/multiple-files.ts +256 -0
- package/src/forms/controls/multiple-photos.ts +44 -0
- package/src/forms/controls/number.ts +38 -0
- package/src/forms/controls/photo.ts +74 -0
- package/src/forms/controls/radio.ts +43 -0
- package/src/forms/controls/reverse-single-reference.ts +50 -0
- package/src/forms/controls/simple-input.ts +148 -0
- package/src/forms/controls/textarea.ts +60 -0
- package/src/forms/controls/tickable.ts +127 -0
- package/src/forms/fields/boolean.test.ts +36 -0
- package/src/forms/fields/boolean.ts +63 -0
- package/src/forms/fields/checkboxed-list.ts +66 -0
- package/src/forms/fields/collection-field.ts +62 -0
- package/src/forms/fields/email.test.ts +24 -0
- package/src/forms/fields/email.ts +20 -0
- package/src/forms/fields/enum-multiple.test.ts +31 -0
- package/src/forms/fields/enum-multiple.ts +62 -0
- package/src/forms/fields/enum.test.ts +25 -0
- package/src/forms/fields/enum.ts +56 -0
- package/src/forms/fields/existing-sealious-value.ts +22 -0
- package/src/forms/fields/field.ts +172 -0
- package/src/forms/fields/file.ts +89 -0
- package/src/forms/fields/multiple-files.ts +20 -0
- package/src/forms/fields/number.test.ts +34 -0
- package/src/forms/fields/number.ts +70 -0
- package/src/forms/fields/pick-from-list.test.ts +19 -0
- package/src/forms/fields/pick-from-list.ts +52 -0
- package/src/forms/fields/proxy-field.ts +24 -0
- package/src/forms/fields/regexp.test.ts +25 -0
- package/src/forms/fields/regexp.ts +53 -0
- package/src/forms/fields/reverse-single-reference.ts +20 -0
- package/src/forms/fields/simple-form-field.ts +30 -0
- package/src/forms/fields/structured-array.ts +27 -0
- package/src/forms/form.test.ts +351 -0
- package/src/forms/form.ts +294 -0
- package/src/forms/forms.scss +85 -0
- package/src/forms/multiform.test.ts +540 -0
- package/src/forms/multiform.ts +236 -0
- package/src/generate-collections.ts +77 -0
- package/src/generate-routes.ts +121 -0
- package/src/index.ts +25 -0
- package/src/make-env.ts +15 -0
- package/src/make-hidden-inputs.ts +24 -0
- package/src/mount.ts +85 -0
- package/src/page/access-control.ts +18 -0
- package/src/page/list.ts +240 -0
- package/src/page/mountable-with-fields.ts +158 -0
- package/src/page/mountable.ts +13 -0
- package/src/page/page.ts +46 -0
- package/src/page/props-parser.ts +38 -0
- package/src/page/stateful-page.ts +202 -0
- package/src/run.ts +0 -0
- package/src/sanitize.ts +9 -0
- package/src/templates/collection.ts +11 -0
- package/src/templates/form.ts +60 -0
- package/src/templates/list.ts +75 -0
- package/src/templates/multiform.ts +58 -0
- package/src/templates/page.ts +25 -0
- package/src/templates/post.ts +21 -0
- package/src/templates/redirect.ts +22 -0
- package/src/templates/route-test.ts +27 -0
- package/src/templates/stateful-page.ts +65 -0
- package/src/test_utils/locator_is_visible.ts +17 -0
- package/src/utils/assert-throws.ts +18 -0
- package/src/utils/base64.ts +7 -0
- package/src/utils/browser-creator.js +10 -0
- package/src/utils/escape-url-params.ts +14 -0
- package/src/utils/exec.ts +30 -0
- package/src/utils/format-external-url.ts +13 -0
- package/src/utils/import-path.ts +25 -0
- package/src/utils/input-wrapper.ts +21 -0
- package/src/utils/list-collections.ts +16 -0
- package/src/utils/make-slug.ts +5 -0
- package/src/utils/natural-numbers.ts +5 -0
- package/src/utils/prettier.ts +13 -0
- package/src/utils/question.ts +23 -0
- package/src/utils/render-attributes.ts +17 -0
- package/src/utils/sleep.ts +3 -0
- package/src/utils/translate.ts +13 -0
- package/src/utils/url-with-new-params.ts +9 -0
- package/src/utils/utils.ts +23 -0
- package/src/utils/walk.ts +20 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import Router from "@koa/router";
|
|
2
|
+
import { hasShape, predicates } from "@sealcode/ts-predicates";
|
|
3
|
+
import { Context } from "koa";
|
|
4
|
+
import { ParsedUrlQuery } from "querystring";
|
|
5
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
6
|
+
import { makeSlug } from "../../utils/make-slug";
|
|
7
|
+
import { FormField } from "../fields/field";
|
|
8
|
+
import { Derived } from "./derived";
|
|
9
|
+
import { FormControlContext } from "./form-control";
|
|
10
|
+
|
|
11
|
+
export class DerivedFrame extends Derived<{
|
|
12
|
+
frame_id: string;
|
|
13
|
+
form_id: string;
|
|
14
|
+
field_name_prefix: string;
|
|
15
|
+
}> {
|
|
16
|
+
constructor(
|
|
17
|
+
public frame_name: string,
|
|
18
|
+
fields: FormField[],
|
|
19
|
+
public generateFrameContent: (
|
|
20
|
+
ctx: FormControlContext,
|
|
21
|
+
args: ParsedUrlQuery
|
|
22
|
+
) => Promise<FlatTemplatable>,
|
|
23
|
+
// "a" is to make it a valid DOM id
|
|
24
|
+
public frame_id = "a" + makeSlug(frame_name)
|
|
25
|
+
) {
|
|
26
|
+
super(
|
|
27
|
+
fields,
|
|
28
|
+
async (values, consts) => /* HTML */ `<turbo-frame
|
|
29
|
+
id="${consts.frame_id}"
|
|
30
|
+
src="./${consts.frame_id}?${Object.entries({
|
|
31
|
+
...consts,
|
|
32
|
+
...(values || {}),
|
|
33
|
+
})
|
|
34
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
35
|
+
.join("&")}"
|
|
36
|
+
></turbo-frame>`,
|
|
37
|
+
async (fctx) => ({
|
|
38
|
+
form_id: fctx.form_id,
|
|
39
|
+
field_name_prefix: fctx.field_name_prefix,
|
|
40
|
+
frame_id,
|
|
41
|
+
validate: fctx.validate,
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
renderBackendResponseFrame(_ctx: Context, content: FlatTemplatable) {
|
|
47
|
+
return tempstream/* HTML */ `<turbo-frame
|
|
48
|
+
id="${this.frame_id}"
|
|
49
|
+
target="_top"
|
|
50
|
+
>
|
|
51
|
+
${content || ""}
|
|
52
|
+
</turbo-frame>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
mount(router: Router) {
|
|
56
|
+
router.get(this.frame_id, async (ctx) => {
|
|
57
|
+
const query = ctx.query;
|
|
58
|
+
if (
|
|
59
|
+
!hasShape(
|
|
60
|
+
{
|
|
61
|
+
field_name_prefix: predicates.string,
|
|
62
|
+
form_id: predicates.string,
|
|
63
|
+
validate: predicates.string,
|
|
64
|
+
},
|
|
65
|
+
query
|
|
66
|
+
)
|
|
67
|
+
) {
|
|
68
|
+
ctx.body = this.renderBackendResponseFrame(
|
|
69
|
+
ctx,
|
|
70
|
+
"Missing query params"
|
|
71
|
+
);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
ctx.body = this.renderBackendResponseFrame(
|
|
75
|
+
ctx,
|
|
76
|
+
await this.generateFrameContent(
|
|
77
|
+
new FormControlContext(
|
|
78
|
+
ctx,
|
|
79
|
+
ctx.query,
|
|
80
|
+
[],
|
|
81
|
+
query.field_name_prefix,
|
|
82
|
+
query.form_id,
|
|
83
|
+
query.validate === "true" ? true : false
|
|
84
|
+
),
|
|
85
|
+
ctx.query
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
+
import { FormField } from "../fields/field";
|
|
4
|
+
import { FormDataValue } from "../form";
|
|
5
|
+
import { FormControl, FormControlContext } from "./form-control";
|
|
6
|
+
|
|
7
|
+
export class Derived<
|
|
8
|
+
Consts extends Record<string, unknown | undefined>
|
|
9
|
+
> extends FormControl {
|
|
10
|
+
role = <const>"decoration";
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
public fields: FormField[],
|
|
14
|
+
public _render: (
|
|
15
|
+
values: Record<string, string>,
|
|
16
|
+
consts: Consts
|
|
17
|
+
) => Promise<FlatTemplatable>,
|
|
18
|
+
public getConsts: (
|
|
19
|
+
fctx: FormControlContext
|
|
20
|
+
) => Promise<Consts> = async () => ({} as Consts)
|
|
21
|
+
) {
|
|
22
|
+
super();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async getBackendValues(
|
|
26
|
+
fctx: FormControlContext
|
|
27
|
+
): Promise<Record<string, FormDataValue>> {
|
|
28
|
+
const fieldnames = this.fields.map((f) => f.name);
|
|
29
|
+
const result = Object.fromEntries(
|
|
30
|
+
Object.entries(fctx.data).filter(([key]) =>
|
|
31
|
+
fieldnames.includes(key)
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async render(fctx: FormControlContext): Promise<FlatTemplatable> {
|
|
38
|
+
// the A makes it always a valid id, as it cannot start with a number
|
|
39
|
+
const id = "A" + randomUUID();
|
|
40
|
+
|
|
41
|
+
const consts = this.getConsts(fctx);
|
|
42
|
+
|
|
43
|
+
const frontend_side_refresh_code = (values_code = "getValues()") =>
|
|
44
|
+
`render(${values_code}, consts)
|
|
45
|
+
.then(result=>document.querySelector("#${id}").innerHTML=result)`;
|
|
46
|
+
|
|
47
|
+
return tempstream/* HTML */ `<div>
|
|
48
|
+
<script>
|
|
49
|
+
(function () {
|
|
50
|
+
const consts = ${"{" +
|
|
51
|
+
Object.entries((await consts) || {})
|
|
52
|
+
.map(([key, value]) => {
|
|
53
|
+
return `"${key}": ${
|
|
54
|
+
typeof value === "object"
|
|
55
|
+
? JSON.stringify(value)
|
|
56
|
+
: typeof value == "string"
|
|
57
|
+
? '"' + (value as string).toString() + '"'
|
|
58
|
+
: (value as string).toString()
|
|
59
|
+
}`;
|
|
60
|
+
})
|
|
61
|
+
.join(",") +
|
|
62
|
+
"}"};
|
|
63
|
+
const form_container =
|
|
64
|
+
document.currentScript.closest(".form-container");
|
|
65
|
+
const render = ${this._render.toString()};
|
|
66
|
+
function getValues() {
|
|
67
|
+
return Object.fromEntries(
|
|
68
|
+
${JSON.stringify(
|
|
69
|
+
this.fields.map((field) => field.name)
|
|
70
|
+
)}.map((field_name) => {
|
|
71
|
+
let input = form_container.querySelector(
|
|
72
|
+
\`input[name="\${field_name}"]\`
|
|
73
|
+
);
|
|
74
|
+
if (!input) {
|
|
75
|
+
return [field_name, ""];
|
|
76
|
+
}
|
|
77
|
+
if (input.type === "radio") {
|
|
78
|
+
input = form_container.querySelector(
|
|
79
|
+
\`input[name="\${field_name}"]:checked\`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return [field_name, input.value];
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
function setupWatcher(field_name) {
|
|
87
|
+
form_container
|
|
88
|
+
.querySelectorAll(\`input[name="\${field_name}"]\`)
|
|
89
|
+
.forEach((input) => {
|
|
90
|
+
input.addEventListener("change", () => {
|
|
91
|
+
${frontend_side_refresh_code()};
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
${JSON.stringify(
|
|
96
|
+
this.fields.map((field) => field.name)
|
|
97
|
+
)}.forEach(setupWatcher);
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
${frontend_side_refresh_code(
|
|
100
|
+
JSON.stringify(await this.getBackendValues(fctx))
|
|
101
|
+
)};
|
|
102
|
+
}, 0);
|
|
103
|
+
})();
|
|
104
|
+
</script>
|
|
105
|
+
<div id="${id}">${this._render({}, await consts)}</div>
|
|
106
|
+
</div>`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
+
import { attribute } from "../../sanitize";
|
|
4
|
+
import { FormField, PickFromListField } from "../fields/field";
|
|
5
|
+
import { FormDataValue } from "../form";
|
|
6
|
+
import { FormControlContext } from "./form-control";
|
|
7
|
+
import { SimpleInput } from "./simple-input";
|
|
8
|
+
|
|
9
|
+
export type DropdownOptions = {
|
|
10
|
+
label: string;
|
|
11
|
+
autosubmit?: boolean;
|
|
12
|
+
autocomplete?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type DropdownOption = { value: string; label: string };
|
|
16
|
+
|
|
17
|
+
export class FreeformDropdown extends SimpleInput<string | null> {
|
|
18
|
+
constructor(
|
|
19
|
+
public field: FormField<boolean, string | null>,
|
|
20
|
+
public _generateOptions: (ctx: Context) => Promise<DropdownOption[]>,
|
|
21
|
+
public options: DropdownOptions = {
|
|
22
|
+
label: field.name,
|
|
23
|
+
autosubmit: false,
|
|
24
|
+
autocomplete: true,
|
|
25
|
+
}
|
|
26
|
+
) {
|
|
27
|
+
super(field, options);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async generateOptions(ctx: Context): Promise<DropdownOption[]> {
|
|
31
|
+
return this._generateOptions(ctx);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async getInputAttributes(
|
|
35
|
+
fctx: FormControlContext
|
|
36
|
+
): Promise<Record<string, string | boolean>> {
|
|
37
|
+
const original = await super.getInputAttributes(fctx);
|
|
38
|
+
return {
|
|
39
|
+
...original,
|
|
40
|
+
...Object.fromEntries([
|
|
41
|
+
...(this.options.autosubmit
|
|
42
|
+
? [
|
|
43
|
+
[
|
|
44
|
+
"onchange",
|
|
45
|
+
(original.onchange || "") +
|
|
46
|
+
";this.form.submit();",
|
|
47
|
+
],
|
|
48
|
+
]
|
|
49
|
+
: []),
|
|
50
|
+
]),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async renderInput(
|
|
55
|
+
ctx: Context,
|
|
56
|
+
attributes_str: string,
|
|
57
|
+
data: Record<string, FormDataValue>
|
|
58
|
+
): Promise<FlatTemplatable> {
|
|
59
|
+
const { parsed: picked_value } = await this.field.getValue(ctx, data);
|
|
60
|
+
return /* HTML */ tempstream`<select ${attributes_str}>
|
|
61
|
+
${
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
63
|
+
Promise.resolve(this.generateOptions(ctx)).then((options) =>
|
|
64
|
+
options.map(
|
|
65
|
+
({ value, label }) =>
|
|
66
|
+
`<option value="${attribute(value)}" ${
|
|
67
|
+
(value || "") == picked_value ? "selected" : ""
|
|
68
|
+
}>${label}</option>`
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
</select>`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class Dropdown extends FreeformDropdown {
|
|
77
|
+
constructor(
|
|
78
|
+
public field: PickFromListField<boolean>,
|
|
79
|
+
public options: DropdownOptions = {
|
|
80
|
+
label: field.name,
|
|
81
|
+
autosubmit: false,
|
|
82
|
+
autocomplete: true,
|
|
83
|
+
}
|
|
84
|
+
) {
|
|
85
|
+
super(field, (ctx) => field.generateOptions(ctx), options);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
2
|
+
import { attribute } from "../../sanitize";
|
|
3
|
+
import { PickFromListField } from "../fields/field";
|
|
4
|
+
import { FormFieldControl } from "./controls";
|
|
5
|
+
import { FormControlContext } from "./form-control";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This control has own forms in it so if you want to use it you
|
|
9
|
+
* probably shouldn't use `await super.render(ctx, data, path)` in
|
|
10
|
+
* render method of you from implementation and you should write
|
|
11
|
+
* your own implementation of this method. See forms that uses
|
|
12
|
+
* this control for reference.
|
|
13
|
+
*/
|
|
14
|
+
export class EditableCollectionSubset extends FormFieldControl {
|
|
15
|
+
constructor(
|
|
16
|
+
public field: PickFromListField<false>,
|
|
17
|
+
public actionname: string,
|
|
18
|
+
public listLabel?: string,
|
|
19
|
+
public selectLabel?: string
|
|
20
|
+
) {
|
|
21
|
+
super([field]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async render(fctx: FormControlContext): Promise<FlatTemplatable> {
|
|
25
|
+
const { parsed: values } = await this.field.getValue(
|
|
26
|
+
fctx.ctx,
|
|
27
|
+
fctx.data
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return tempstream/* HTML */ `<div>
|
|
31
|
+
<ul>
|
|
32
|
+
${Promise.resolve(this.field.generateOptions(fctx.ctx)).then(
|
|
33
|
+
(options) =>
|
|
34
|
+
Object.entries(options)
|
|
35
|
+
.filter(([value]) =>
|
|
36
|
+
(values || ([] as string[])).includes(value)
|
|
37
|
+
)
|
|
38
|
+
.map(
|
|
39
|
+
([value, text]) => /* HTML */ `
|
|
40
|
+
<li>
|
|
41
|
+
<form
|
|
42
|
+
method="POST"
|
|
43
|
+
action="${fctx.ctx.path}"
|
|
44
|
+
>
|
|
45
|
+
<span>${text}</span>
|
|
46
|
+
<input
|
|
47
|
+
type="hidden"
|
|
48
|
+
name="${this.field.name}"
|
|
49
|
+
value="${attribute(value)}"
|
|
50
|
+
/>
|
|
51
|
+
<input
|
|
52
|
+
type="hidden"
|
|
53
|
+
name="${this.actionname}"
|
|
54
|
+
value="list"
|
|
55
|
+
/>
|
|
56
|
+
<input
|
|
57
|
+
type="submit"
|
|
58
|
+
${this.listLabel
|
|
59
|
+
? `value="${this.listLabel}"`
|
|
60
|
+
: ""}
|
|
61
|
+
/>
|
|
62
|
+
</form>
|
|
63
|
+
</li>
|
|
64
|
+
`
|
|
65
|
+
)
|
|
66
|
+
)}
|
|
67
|
+
</ul>
|
|
68
|
+
<form method="POST" action="${fctx.ctx.path}">
|
|
69
|
+
<select name="${this.field.name}">
|
|
70
|
+
${Promise.resolve(
|
|
71
|
+
this.field.generateOptions(fctx.ctx)
|
|
72
|
+
).then((options) =>
|
|
73
|
+
Object.entries(options)
|
|
74
|
+
.filter(
|
|
75
|
+
([value]) =>
|
|
76
|
+
!(values || ([] as string[])).includes(
|
|
77
|
+
value
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
.map(
|
|
81
|
+
([value, text]) =>
|
|
82
|
+
`<option value="${attribute(
|
|
83
|
+
value
|
|
84
|
+
)}">${text}</option>`
|
|
85
|
+
)
|
|
86
|
+
)}
|
|
87
|
+
</select>
|
|
88
|
+
<input type="hidden" name="${this.actionname}" value="select" />
|
|
89
|
+
<input
|
|
90
|
+
type="submit"
|
|
91
|
+
${this.selectLabel ? `value="${this.selectLabel}"` : ""}
|
|
92
|
+
/>
|
|
93
|
+
</form>
|
|
94
|
+
</div>`;
|
|
95
|
+
}
|
|
96
|
+
role = <const>"input";
|
|
97
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
+
import { FormDataValue, FormMessage } from "../form";
|
|
4
|
+
import { FormControl, FormControlContext } from "./form-control";
|
|
5
|
+
import { FormFieldControl } from "./form-field-control";
|
|
6
|
+
|
|
7
|
+
export class FieldGroup extends FormFieldControl {
|
|
8
|
+
constructor(
|
|
9
|
+
public controls: FormControl[],
|
|
10
|
+
public options: {
|
|
11
|
+
label?: string;
|
|
12
|
+
classes?: string[];
|
|
13
|
+
containerID?: string;
|
|
14
|
+
} = {}
|
|
15
|
+
) {
|
|
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"`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async render(fctx: FormControlContext): Promise<FlatTemplatable> {
|
|
44
|
+
const { label, classes } = this.options;
|
|
45
|
+
|
|
46
|
+
return tempstream/* HTML */ `
|
|
47
|
+
<div
|
|
48
|
+
class="field-group ${(classes || []).join(" ")}"
|
|
49
|
+
${this.options.containerID
|
|
50
|
+
? `id="${this.options.containerID}"`
|
|
51
|
+
: ""}
|
|
52
|
+
>
|
|
53
|
+
${this.preLabelContainer()}
|
|
54
|
+
<div class="field-group__label-container">
|
|
55
|
+
${this.preLabel()}
|
|
56
|
+
${label
|
|
57
|
+
? `<label ${this.getGroupLabelProps()}>${label}</label>`
|
|
58
|
+
: ""}
|
|
59
|
+
${this.postLabel()}
|
|
60
|
+
</div>
|
|
61
|
+
${this.preFields()}
|
|
62
|
+
<div class="field-group__fields">
|
|
63
|
+
${this.controls.map((control) => control.render(fctx))}
|
|
64
|
+
</div>
|
|
65
|
+
${this.postFields()}
|
|
66
|
+
</div>
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
+
import { FileContainer } from "../fields/file";
|
|
4
|
+
import { FormDataValue } from "../form";
|
|
5
|
+
import { FormControlContext } from "./form-control";
|
|
6
|
+
import { SimpleInput } from "./simple-input";
|
|
7
|
+
|
|
8
|
+
export class File extends SimpleInput<FileContainer> {
|
|
9
|
+
getType() {
|
|
10
|
+
return "file";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async renderFilePreview(
|
|
14
|
+
_ctx: Context,
|
|
15
|
+
file_data: { id?: string; filename?: string } | null
|
|
16
|
+
): Promise<FlatTemplatable> {
|
|
17
|
+
return file_data?.filename || "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async preInput(ctx: Context, data: Record<string, FormDataValue>) {
|
|
21
|
+
const { parsed } = await this.field.getValue(ctx, data);
|
|
22
|
+
return tempstream/* HTML */ `${this.renderFilePreview(ctx, parsed.old)}
|
|
23
|
+
<input
|
|
24
|
+
type="hidden"
|
|
25
|
+
name="${this.field.name}[old][id]"
|
|
26
|
+
value="${parsed.old?.id || ""}"
|
|
27
|
+
/>
|
|
28
|
+
<input
|
|
29
|
+
type="hidden"
|
|
30
|
+
name="${this.field.name}[old][filename]"
|
|
31
|
+
value="${parsed.old?.filename || ""}"
|
|
32
|
+
/>`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getInputAttributes(
|
|
36
|
+
fctx: FormControlContext
|
|
37
|
+
): Promise<Record<string, string | boolean>> {
|
|
38
|
+
const original = await super.getInputAttributes(fctx);
|
|
39
|
+
const { parsed } = await this.field.getValue(fctx.ctx, fctx.data);
|
|
40
|
+
return {
|
|
41
|
+
...original,
|
|
42
|
+
name: this.field.name + "[new]",
|
|
43
|
+
required: original.required && !parsed.old,
|
|
44
|
+
value: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Router from "@koa/router";
|
|
2
|
+
import { Context } from "koa";
|
|
3
|
+
import { FlatTemplatable } from "tempstream";
|
|
4
|
+
import { FormDataValue, FormMessage } from "../form";
|
|
5
|
+
|
|
6
|
+
export class FormControlContext {
|
|
7
|
+
constructor(
|
|
8
|
+
public ctx: Context,
|
|
9
|
+
public data: Record<string, FormDataValue>,
|
|
10
|
+
public messages: FormMessage[],
|
|
11
|
+
public field_name_prefix: string,
|
|
12
|
+
public form_id: string,
|
|
13
|
+
public validate: boolean
|
|
14
|
+
) {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export abstract class FormControl {
|
|
18
|
+
abstract render(
|
|
19
|
+
fctx: FormControlContext
|
|
20
|
+
): FlatTemplatable | Promise<FlatTemplatable>;
|
|
21
|
+
abstract role: "input" | "decoration" | "messages" | "submit";
|
|
22
|
+
|
|
23
|
+
mount(router: Router) {}
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable } from "tempstream";
|
|
3
|
+
import { FormField } from "../fields/field";
|
|
4
|
+
import { FieldValueType, FormDataValue } from "../form";
|
|
5
|
+
import { FormControl, FormControlContext } from "./form-control";
|
|
6
|
+
|
|
7
|
+
export type FormFieldsToTypes<FormFields extends readonly FormField[]> = {
|
|
8
|
+
[Index in keyof FormFields]: FormFields[Index] extends FormField
|
|
9
|
+
? FieldValueType<FormFields[Index]>
|
|
10
|
+
: never;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export abstract class FormFieldControl extends FormControl {
|
|
14
|
+
role = <const>"input";
|
|
15
|
+
constructor(public fields: FormField[]) {
|
|
16
|
+
super();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
abstract render(
|
|
20
|
+
fctx: FormControlContext
|
|
21
|
+
): FlatTemplatable | Promise<FlatTemplatable>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FormControl } from "./controls";
|
|
3
|
+
import { FormControlContext } from "./form-control";
|
|
4
|
+
|
|
5
|
+
export class FormHeader extends FormControl {
|
|
6
|
+
role = <const>"decoration";
|
|
7
|
+
constructor(
|
|
8
|
+
public text: string,
|
|
9
|
+
public isVisible: (
|
|
10
|
+
fctx: FormControlContext
|
|
11
|
+
) => Promise<boolean> = async () => true
|
|
12
|
+
) {
|
|
13
|
+
super();
|
|
14
|
+
}
|
|
15
|
+
async render(fctx: FormControlContext) {
|
|
16
|
+
const isVsbl = await this.isVisible(fctx);
|
|
17
|
+
return isVsbl ? `<h2>${this.text}</h2>` : "";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FlatTemplatable, Templatable } from "tempstream";
|
|
2
|
+
import { FormControl } from "./controls";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This class will render `turbo-frame` tag so that u can
|
|
6
|
+
* embed other route inside your form. This will require
|
|
7
|
+
* to add value to `data: FormData` (inside your master form
|
|
8
|
+
* render function) with key `Frame.FRAME_PATH_KEY`. Value
|
|
9
|
+
* needs to be url to route that you want to embed. If you
|
|
10
|
+
* this value wont be provided frame will redner empty string.
|
|
11
|
+
* See `src/back/routes/profile/[id].form.ts` for an example.
|
|
12
|
+
*/
|
|
13
|
+
export class Frame extends FormControl {
|
|
14
|
+
constructor(public src: string) {
|
|
15
|
+
super();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
render(): FlatTemplatable | Promise<FlatTemplatable> {
|
|
19
|
+
return /* HTML */ `<turbo-frame
|
|
20
|
+
id="contrahents"
|
|
21
|
+
loading="lazy"
|
|
22
|
+
src="${this.src}"
|
|
23
|
+
></turbo-frame>`;
|
|
24
|
+
}
|
|
25
|
+
role = <const>"decoration";
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
+
import { attribute } from "../../sanitize";
|
|
4
|
+
import { FormField } from "../fields/field";
|
|
5
|
+
import { FormDataValue } from "../form";
|
|
6
|
+
import { FormControlContext } from "./form-control";
|
|
7
|
+
import { FormFieldControl } from "./form-field-control";
|
|
8
|
+
|
|
9
|
+
export class HiddenInput extends FormFieldControl {
|
|
10
|
+
constructor(public field: FormField) {
|
|
11
|
+
super([field]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async render(fctx: FormControlContext): Promise<FlatTemplatable> {
|
|
15
|
+
const { raw } = await this.field.getValue(fctx.ctx, fctx.data);
|
|
16
|
+
return tempstream`<input type="hidden" name="${
|
|
17
|
+
this.field.name
|
|
18
|
+
}" value="${attribute((raw || "").toString())}" form="${
|
|
19
|
+
fctx.form_id
|
|
20
|
+
}">`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable } from "tempstream";
|
|
3
|
+
import { FormDataValue, FormMessage } from "../form";
|
|
4
|
+
import { FormControl } from "./controls";
|
|
5
|
+
import { FormControlContext } from "./form-control";
|
|
6
|
+
|
|
7
|
+
type Renderer =
|
|
8
|
+
| ((fctx: FormControlContext) => FlatTemplatable)
|
|
9
|
+
| FlatTemplatable;
|
|
10
|
+
|
|
11
|
+
export class HTML extends FormControl {
|
|
12
|
+
role = (<const>"decoration") as "decoration" | "submit";
|
|
13
|
+
public renderer: Renderer;
|
|
14
|
+
|
|
15
|
+
constructor(role: "decoration" | "submit", renderer: Renderer);
|
|
16
|
+
constructor(renderer: Renderer);
|
|
17
|
+
constructor(
|
|
18
|
+
role_or_renderer: "decoration" | "submit" | Renderer,
|
|
19
|
+
renderer_or_undefined?: Renderer | FlatTemplatable
|
|
20
|
+
) {
|
|
21
|
+
super();
|
|
22
|
+
if (arguments.length == 2) {
|
|
23
|
+
this.role = role_or_renderer as "decoration" | "submit";
|
|
24
|
+
this.renderer = renderer_or_undefined as Renderer;
|
|
25
|
+
} else {
|
|
26
|
+
this.renderer = role_or_renderer;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async render(fctx: FormControlContext): Promise<FlatTemplatable> {
|
|
31
|
+
if (typeof this.renderer === "string") {
|
|
32
|
+
return this.renderer;
|
|
33
|
+
}
|
|
34
|
+
return typeof this.renderer == "function"
|
|
35
|
+
? this.renderer(fctx)
|
|
36
|
+
: this.renderer;
|
|
37
|
+
}
|
|
38
|
+
}
|