@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,256 @@
|
|
|
1
|
+
import Router from "@koa/router";
|
|
2
|
+
import {
|
|
3
|
+
Collection,
|
|
4
|
+
CollectionItem,
|
|
5
|
+
File as SealiousFile,
|
|
6
|
+
Field as SealiousField,
|
|
7
|
+
} from "sealious";
|
|
8
|
+
import { Context } from "koa";
|
|
9
|
+
import { inputWrapper } from "../../utils/input-wrapper";
|
|
10
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
11
|
+
import { MultipleFiles as MultipleFilesField } from "../fields/multiple-files";
|
|
12
|
+
import { FormFieldControl } from "./form-field-control";
|
|
13
|
+
import { is, predicates } from "@sealcode/ts-predicates";
|
|
14
|
+
import { renderAttributes } from "../../utils/render-attributes";
|
|
15
|
+
import { FormControlContext } from "./form-control";
|
|
16
|
+
|
|
17
|
+
export type MultipleFilesOptions = {
|
|
18
|
+
label?: string;
|
|
19
|
+
uploadLabel?: string;
|
|
20
|
+
getAdditionalFields: (
|
|
21
|
+
ctx: Context,
|
|
22
|
+
file: SealiousFile
|
|
23
|
+
) => Promise<Record<string, unknown>>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class MultipleFiles extends FormFieldControl {
|
|
27
|
+
public options: MultipleFilesOptions;
|
|
28
|
+
constructor(
|
|
29
|
+
public field: MultipleFilesField,
|
|
30
|
+
options?: Partial<MultipleFilesOptions>
|
|
31
|
+
) {
|
|
32
|
+
super([field]);
|
|
33
|
+
this.options = { getAdditionalFields: async () => ({}), ...options };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getClassModifiers(): string[] {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
mount(router: Router) {
|
|
41
|
+
router.get(this.getFrameRelativeURL(), async (ctx) => {
|
|
42
|
+
ctx.body = this.renderFrame(
|
|
43
|
+
ctx,
|
|
44
|
+
await this.renderFrameContent(ctx)
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
router.post(
|
|
49
|
+
this.getFrameRelativeURL() + "/delete/:file_item_id",
|
|
50
|
+
async (ctx) => {
|
|
51
|
+
await this.deleteFileAssociation(ctx, ctx.params.file_item_id);
|
|
52
|
+
ctx.body = this.renderFrame(
|
|
53
|
+
ctx,
|
|
54
|
+
await this.renderFrameContent(ctx)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
router.post(this.getFrameRelativeURL() + "/add", async (ctx) => {
|
|
60
|
+
let files = ctx.$body.files as SealiousFile | SealiousFile[];
|
|
61
|
+
if (!files || !is(files, predicates.object)) {
|
|
62
|
+
ctx.body = "Missing files";
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(files)) {
|
|
66
|
+
files = [files];
|
|
67
|
+
}
|
|
68
|
+
await Promise.all(
|
|
69
|
+
files.map((file) =>
|
|
70
|
+
this.addFileAssociation(
|
|
71
|
+
ctx,
|
|
72
|
+
file as unknown as SealiousFile
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
ctx.body = this.renderFrame(
|
|
77
|
+
ctx,
|
|
78
|
+
await this.renderFrameContent(ctx)
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async deleteFileAssociation(ctx: Context, file_item_id: string) {
|
|
84
|
+
await ctx.$app.collections[
|
|
85
|
+
this.field.collection_field.referencing_collection
|
|
86
|
+
].removeByID(ctx.$context, file_item_id);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async addFileAssociation(ctx: Context, file: SealiousFile) {
|
|
90
|
+
const file_field = this.getFileField();
|
|
91
|
+
if (!file_field) {
|
|
92
|
+
throw new Error("No file field in referencing collection");
|
|
93
|
+
}
|
|
94
|
+
const body = {
|
|
95
|
+
...(await this.options.getAdditionalFields(ctx, file)),
|
|
96
|
+
[this.field.collection_field.referencing_field]:
|
|
97
|
+
await this.field.getItemId(ctx),
|
|
98
|
+
[file_field.name]: file,
|
|
99
|
+
};
|
|
100
|
+
await ctx.$app.collections[
|
|
101
|
+
this.field.collection_field.referencing_collection
|
|
102
|
+
].create(ctx.$context, body);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getFrameRelativeURL() {
|
|
106
|
+
return `${this.field.name}_files`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getFrameID(): string {
|
|
110
|
+
// the "A" is necessary here
|
|
111
|
+
return `A${this.field.name}__multiple-fields-control`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
renderFrame(ctx: Context, content?: FlatTemplatable) {
|
|
115
|
+
return tempstream/* HTML */ `<turbo-frame
|
|
116
|
+
${content ? "" : `src="./${this.getFrameRelativeURL()}"`}
|
|
117
|
+
id="${this.getFrameID()}"
|
|
118
|
+
target="_top"
|
|
119
|
+
>
|
|
120
|
+
${content || ""}
|
|
121
|
+
</turbo-frame>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
render(
|
|
125
|
+
fctx: FormControlContext
|
|
126
|
+
): FlatTemplatable | Promise<FlatTemplatable> {
|
|
127
|
+
return this.renderFrame(fctx.ctx, "");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getReferencingCollection() {
|
|
131
|
+
const result =
|
|
132
|
+
this.field.collection_field.app.collections[
|
|
133
|
+
this.field.collection_field.referencing_collection
|
|
134
|
+
];
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getFileField(): SealiousField | null {
|
|
139
|
+
for (const [_, field] of Object.entries(
|
|
140
|
+
this.getReferencingCollection().fields
|
|
141
|
+
)) {
|
|
142
|
+
if (field.handles_large_data) {
|
|
143
|
+
return field;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async extractFileFromItem(
|
|
150
|
+
item: CollectionItem
|
|
151
|
+
): Promise<SealiousFile | null> {
|
|
152
|
+
const sealious_field = this.getFileField();
|
|
153
|
+
if (!sealious_field) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return await SealiousFile.fromID(
|
|
157
|
+
(item.collection as Collection).app,
|
|
158
|
+
item.get(sealious_field.name).id
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async renderFileItemPreview(
|
|
163
|
+
_fileItem: CollectionItem,
|
|
164
|
+
file: SealiousFile
|
|
165
|
+
): Promise<FlatTemplatable> {
|
|
166
|
+
return file.filename || "no filename";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async renderFileItem(
|
|
170
|
+
fileItem: CollectionItem,
|
|
171
|
+
file: SealiousFile
|
|
172
|
+
): Promise<FlatTemplatable> {
|
|
173
|
+
return tempstream/* HTML */ `<li class="file-list-item">
|
|
174
|
+
<a
|
|
175
|
+
class="file-list-item__preview"
|
|
176
|
+
href="${file.getURL()}"
|
|
177
|
+
data-turbo="false"
|
|
178
|
+
>
|
|
179
|
+
${this.renderFileItemPreview(fileItem, file)}
|
|
180
|
+
</a>
|
|
181
|
+
${this.renderFileRemoveButton(fileItem)}
|
|
182
|
+
</li>`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
renderFileRemoveButton(fileItem: CollectionItem): FlatTemplatable {
|
|
186
|
+
return /* HTML */ ` <form
|
|
187
|
+
data-turbo-frame="${this.getFrameID()}"
|
|
188
|
+
action="${this.getFrameRelativeURL()}/delete/${fileItem.id}"
|
|
189
|
+
method="POST"
|
|
190
|
+
>
|
|
191
|
+
<input
|
|
192
|
+
type="submit"
|
|
193
|
+
value="X"
|
|
194
|
+
class="file-list-action file-list-item__button file-list-item__button--delete"
|
|
195
|
+
/>
|
|
196
|
+
</form>`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async getFileItems(ctx: Context) {
|
|
200
|
+
const item_id = await this.field.getItemId(ctx);
|
|
201
|
+
const {
|
|
202
|
+
items: [item],
|
|
203
|
+
} = await this.field.collection_field.collection
|
|
204
|
+
.list(ctx.$context)
|
|
205
|
+
.ids([item_id])
|
|
206
|
+
.attach({ [this.field.collection_field.name]: true })
|
|
207
|
+
.fetch();
|
|
208
|
+
return item
|
|
209
|
+
.getAttachments(this.field.collection_field.name)
|
|
210
|
+
.filter((f) => f);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
getInputAttributes() {
|
|
214
|
+
return { type: "file", name: "files", multiple: true };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async renderFrameContent(ctx: Context): Promise<FlatTemplatable> {
|
|
218
|
+
const files = (
|
|
219
|
+
await Promise.all(
|
|
220
|
+
(
|
|
221
|
+
await this.getFileItems(ctx)
|
|
222
|
+
)
|
|
223
|
+
.filter((item) => item) // filter out undefineds
|
|
224
|
+
.map(async (item) => [
|
|
225
|
+
item,
|
|
226
|
+
await this.extractFileFromItem(item),
|
|
227
|
+
])
|
|
228
|
+
)
|
|
229
|
+
).filter(([_, f]) => f !== null) as [CollectionItem, SealiousFile][];
|
|
230
|
+
|
|
231
|
+
return inputWrapper(
|
|
232
|
+
["multiple-files", this.field.name, ...this.getClassModifiers()],
|
|
233
|
+
tempstream/* HTML */ `
|
|
234
|
+
<label>${this.options.label || this.field.name}</label>
|
|
235
|
+
<ul class="multiple-files__list">
|
|
236
|
+
${files.map(([item, file]) =>
|
|
237
|
+
this.renderFileItem(item, file)
|
|
238
|
+
)}
|
|
239
|
+
</ul>
|
|
240
|
+
<form
|
|
241
|
+
action="${this.getFrameRelativeURL()}/add"
|
|
242
|
+
method="POST"
|
|
243
|
+
enctype="multipart/form-data"
|
|
244
|
+
data-turbo-frame="${this.getFrameID()}"
|
|
245
|
+
>
|
|
246
|
+
<input ${renderAttributes(this.getInputAttributes())} />
|
|
247
|
+
<input
|
|
248
|
+
type="submit"
|
|
249
|
+
value="${this.options.uploadLabel || "Upload"}"
|
|
250
|
+
class="file-list-action"
|
|
251
|
+
/>
|
|
252
|
+
</form>
|
|
253
|
+
`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { File as SealiousFile, CollectionItem } from "sealious";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
+
import { MultipleFiles, MultipleFilesOptions } from "./multiple-files";
|
|
4
|
+
import { MultipleFiles as MultipleFilesField } from "../fields/multiple-files";
|
|
5
|
+
import KoaResponsiveImageRouter from "koa-responsive-image-router";
|
|
6
|
+
|
|
7
|
+
export class MultiplePhotos extends MultipleFiles {
|
|
8
|
+
constructor(
|
|
9
|
+
public field: MultipleFilesField,
|
|
10
|
+
public imageRouter: KoaResponsiveImageRouter,
|
|
11
|
+
options?: Partial<MultipleFilesOptions>
|
|
12
|
+
) {
|
|
13
|
+
super(field, options);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getClassModifiers() {
|
|
17
|
+
return ["multiple-files--photos"];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getInputAttributes() {
|
|
21
|
+
return { ...super.getInputAttributes(), accept: "image/*" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async renderFileItem(
|
|
25
|
+
fileItem: CollectionItem,
|
|
26
|
+
file: SealiousFile
|
|
27
|
+
): Promise<FlatTemplatable> {
|
|
28
|
+
const path = file.getDataPath();
|
|
29
|
+
if (!path) {
|
|
30
|
+
return "missing file";
|
|
31
|
+
}
|
|
32
|
+
return tempstream/* HTML */ `<li>
|
|
33
|
+
<a href="${file.getURL()}" data-turbo="false">
|
|
34
|
+
${this.imageRouter.image({
|
|
35
|
+
path,
|
|
36
|
+
resolutions: [150, 300],
|
|
37
|
+
sizes_attr: "150px",
|
|
38
|
+
})}
|
|
39
|
+
</a>
|
|
40
|
+
<span class="filename"> ${file.filename || "no filename"}</span>
|
|
41
|
+
${this.renderFileRemoveButton(fileItem)}
|
|
42
|
+
</li>`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable } from "tempstream";
|
|
3
|
+
import { FormField, NumberField } from "../fields/field";
|
|
4
|
+
import { FormDataValue } from "../form";
|
|
5
|
+
import { FormControlContext } from "./form-control";
|
|
6
|
+
import { SimpleInput, SimpleInputOptions } from "./simple-input";
|
|
7
|
+
|
|
8
|
+
export type NumberOptions = SimpleInputOptions & Partial<{ step: number }>;
|
|
9
|
+
|
|
10
|
+
export class Number extends SimpleInput<number | null> {
|
|
11
|
+
field: NumberField;
|
|
12
|
+
options: NumberOptions;
|
|
13
|
+
constructor(field: NumberField, options: NumberOptions) {
|
|
14
|
+
super(field, options);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getType() {
|
|
18
|
+
return "number";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getInputAttributes(
|
|
22
|
+
fctx: FormControlContext
|
|
23
|
+
): Promise<Record<string, string | boolean>> {
|
|
24
|
+
const original = await super.getInputAttributes(fctx);
|
|
25
|
+
return {
|
|
26
|
+
...original,
|
|
27
|
+
...Object.fromEntries([
|
|
28
|
+
...(this.field.options.min
|
|
29
|
+
? [["min", this.field.options.min]]
|
|
30
|
+
: []),
|
|
31
|
+
...(this.field.options.max
|
|
32
|
+
? [["max", this.field.options.max]]
|
|
33
|
+
: []),
|
|
34
|
+
...(this.options.step ? [["step", this.options.step]] : []),
|
|
35
|
+
]),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
+
import { File } from "./file";
|
|
4
|
+
import { File as SealiousFile } from "sealious";
|
|
5
|
+
import { FileContainer, FormField } from "../fields/field";
|
|
6
|
+
import { SimpleInputOptions } from "./simple-input";
|
|
7
|
+
import KoaResponsiveImageRouter from "koa-responsive-image-router";
|
|
8
|
+
import { FormControlContext } from "./form-control";
|
|
9
|
+
|
|
10
|
+
export class Photo extends File {
|
|
11
|
+
constructor(
|
|
12
|
+
public field: FormField<boolean, FileContainer>,
|
|
13
|
+
public imageRouter: KoaResponsiveImageRouter,
|
|
14
|
+
options?: SimpleInputOptions
|
|
15
|
+
) {
|
|
16
|
+
super(field, options);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async getImgStyle() {
|
|
20
|
+
return "width: 100px; height: 100px; object-fit: contain";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async renderFilePreview(
|
|
24
|
+
ctx: Context,
|
|
25
|
+
file_data: { id?: string; filename?: string } | null
|
|
26
|
+
): Promise<FlatTemplatable> {
|
|
27
|
+
const class_name = "file_preview";
|
|
28
|
+
const empty_container = `<div class="${class_name}"></div>`;
|
|
29
|
+
if (!file_data) {
|
|
30
|
+
return empty_container;
|
|
31
|
+
}
|
|
32
|
+
const { id } = file_data;
|
|
33
|
+
if (!id) {
|
|
34
|
+
return empty_container;
|
|
35
|
+
}
|
|
36
|
+
const file = await SealiousFile.fromID(ctx.$app, id);
|
|
37
|
+
return tempstream/* HTML */ `<div class="${class_name}">
|
|
38
|
+
${this.imageRouter.image({
|
|
39
|
+
resolutions: [100],
|
|
40
|
+
sizes_attr: "100px",
|
|
41
|
+
path: file.getDataPath(),
|
|
42
|
+
img_style: await this.getImgStyle(),
|
|
43
|
+
})}
|
|
44
|
+
</div>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getInputAttributes(fctx: FormControlContext) {
|
|
48
|
+
const original = (await super.getInputAttributes(fctx)) as Record<
|
|
49
|
+
string,
|
|
50
|
+
string | boolean
|
|
51
|
+
>;
|
|
52
|
+
const { parsed } = await this.field.getValue(fctx.ctx, fctx.data);
|
|
53
|
+
return {
|
|
54
|
+
...original,
|
|
55
|
+
accept: "image/*",
|
|
56
|
+
onchange:
|
|
57
|
+
(original.onchange || "") +
|
|
58
|
+
";" +
|
|
59
|
+
`
|
|
60
|
+
const file = this.files[0];
|
|
61
|
+
const preview_container = this.parentElement.querySelector(".file_preview");
|
|
62
|
+
const target = this;
|
|
63
|
+
if (file) {
|
|
64
|
+
const fileReader = new FileReader();
|
|
65
|
+
fileReader.readAsDataURL(file);
|
|
66
|
+
fileReader.addEventListener("load", function () {
|
|
67
|
+
preview_container.innerHTML = \`<img src="\${this.result}" style="${await this.getImgStyle()}" />\`;
|
|
68
|
+
});
|
|
69
|
+
}`
|
|
70
|
+
.replaceAll("\n", " ")
|
|
71
|
+
.replaceAll(`"`, "'"),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FormField } from "../fields/field";
|
|
3
|
+
import { FormDataValue } from "../form";
|
|
4
|
+
import { Tickable } from "./tickable";
|
|
5
|
+
|
|
6
|
+
export class Radio extends Tickable<string> {
|
|
7
|
+
type = <const>"radio";
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
public field: FormField<boolean, string>,
|
|
11
|
+
public value: string,
|
|
12
|
+
options: { label?: string }
|
|
13
|
+
) {
|
|
14
|
+
super(field, options);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
makeInputID(): string {
|
|
18
|
+
return this.field.name + "-" + this.value.replaceAll(/\W/g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getValueAttribute(): string {
|
|
22
|
+
return this.value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getWrapperClasses(
|
|
26
|
+
ctx: Context,
|
|
27
|
+
data: Record<string, FormDataValue>,
|
|
28
|
+
value: string
|
|
29
|
+
): string[] {
|
|
30
|
+
return [
|
|
31
|
+
...super.getWrapperClasses(ctx, data, value),
|
|
32
|
+
this.value.replaceAll(/\W/g, "-"),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isChecked(
|
|
37
|
+
_ctx: Context,
|
|
38
|
+
_data: Record<string, FormDataValue>,
|
|
39
|
+
value: string
|
|
40
|
+
): boolean {
|
|
41
|
+
return this.value === value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// import { FormControl } from "./form-control";
|
|
2
|
+
// import { FormFieldControl } from "./form-field-control";
|
|
3
|
+
// import { ReverseSingleReference as ReverseSingleReferenceField } from "../fields/field";
|
|
4
|
+
// import { Context } from "koa";
|
|
5
|
+
// import { Fields } from "../..";
|
|
6
|
+
// import { ArrayControl } from "./array";
|
|
7
|
+
|
|
8
|
+
// export class ReverseSingleReference extends ArrayControl {
|
|
9
|
+
// //
|
|
10
|
+
// //
|
|
11
|
+
// // this isn't finished yet
|
|
12
|
+
// //
|
|
13
|
+
// //
|
|
14
|
+
|
|
15
|
+
// constructor(
|
|
16
|
+
// public field: ReverseSingleReferenceField,
|
|
17
|
+
// public item_controls: Array<FormControl | UnboundControlStorage>
|
|
18
|
+
// ) {
|
|
19
|
+
// super(field, item_controls);
|
|
20
|
+
// }
|
|
21
|
+
|
|
22
|
+
// async getItems(ctx: Context) {
|
|
23
|
+
// const target_id = await this.getTargetItemID(ctx);
|
|
24
|
+
// const { items } = await this.field.sealious_field
|
|
25
|
+
// .getReferencingCollection()
|
|
26
|
+
// .list(ctx.$context)
|
|
27
|
+
// .filter({
|
|
28
|
+
// [this.field.sealious_field.referencing_field]: target_id,
|
|
29
|
+
// })
|
|
30
|
+
// .fetch();
|
|
31
|
+
// return items;
|
|
32
|
+
// }
|
|
33
|
+
|
|
34
|
+
// makeSubfield(fieldname: string) {
|
|
35
|
+
// const collection = this.field.sealious_field.getReferencingCollection();
|
|
36
|
+
// if (!collection.fields[fieldname]) {
|
|
37
|
+
// return null;
|
|
38
|
+
// }
|
|
39
|
+
// return new Fields.CollectionField(false, collection.fields[fieldname]);
|
|
40
|
+
// }
|
|
41
|
+
|
|
42
|
+
// async add(ctx: Context): Promise<void> {
|
|
43
|
+
// const collection = this.field.sealious_field.getReferencingCollection();
|
|
44
|
+
// await collection.create(ctx.$context, {
|
|
45
|
+
// [this.field.sealious_field.referencing_field]:
|
|
46
|
+
// await this.getTargetItemID(ctx),
|
|
47
|
+
// ...(await this.getDefaultItemBody()),
|
|
48
|
+
// });
|
|
49
|
+
// }
|
|
50
|
+
// }
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
+
import { getRequiredClass, inputWrapper } from "../../utils/input-wrapper";
|
|
4
|
+
import { renderAttributes } from "../../utils/render-attributes";
|
|
5
|
+
import { FormField } from "../fields/field";
|
|
6
|
+
import { FormDataValue } from "../form";
|
|
7
|
+
import { FormFieldControl } from "./controls";
|
|
8
|
+
import { FormControlContext } from "./form-control";
|
|
9
|
+
|
|
10
|
+
export type SimpleInputOptions = {
|
|
11
|
+
id?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
autocomplete?: boolean;
|
|
14
|
+
hide_errors?: boolean;
|
|
15
|
+
type?:
|
|
16
|
+
| "color"
|
|
17
|
+
| "date"
|
|
18
|
+
| "email"
|
|
19
|
+
| "file"
|
|
20
|
+
| "month"
|
|
21
|
+
| "number"
|
|
22
|
+
| "password"
|
|
23
|
+
| "search"
|
|
24
|
+
| "tel"
|
|
25
|
+
| "text"
|
|
26
|
+
| "time"
|
|
27
|
+
| "url"
|
|
28
|
+
| "week";
|
|
29
|
+
value?: string;
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
readonly?: boolean;
|
|
32
|
+
step?: number;
|
|
33
|
+
suffix?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export class SimpleInput<
|
|
37
|
+
ParsedValue,
|
|
38
|
+
Options extends SimpleInputOptions = SimpleInputOptions
|
|
39
|
+
> extends FormFieldControl {
|
|
40
|
+
options: SimpleInputOptions;
|
|
41
|
+
constructor(
|
|
42
|
+
public field: FormField<boolean, ParsedValue>,
|
|
43
|
+
options?: Options
|
|
44
|
+
) {
|
|
45
|
+
super([field]);
|
|
46
|
+
this.options = options || {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async preInput(
|
|
50
|
+
_ctx: Context,
|
|
51
|
+
_data: Record<string, FormDataValue>
|
|
52
|
+
): Promise<FlatTemplatable> {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getType(): string {
|
|
57
|
+
return this.options.type || "text";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getLabel(): string {
|
|
61
|
+
return this.options.label != undefined
|
|
62
|
+
? this.options.label
|
|
63
|
+
: this.field.name;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getID(): string {
|
|
67
|
+
return this.options.id || this.field.name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getPlaceholder(): string {
|
|
71
|
+
return this.options.placeholder === ""
|
|
72
|
+
? ""
|
|
73
|
+
: this.options.placeholder || this.getType();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getInputAttributes(
|
|
77
|
+
fctx: FormControlContext
|
|
78
|
+
): Promise<Record<string, string | boolean>> {
|
|
79
|
+
const readonly = this.options.readonly || false;
|
|
80
|
+
const required = this.field.required;
|
|
81
|
+
const { parsed, raw, valid } = await this.field.getValue(
|
|
82
|
+
fctx.ctx,
|
|
83
|
+
fctx.data
|
|
84
|
+
);
|
|
85
|
+
return Object.fromEntries([
|
|
86
|
+
<const>["id", this.getID()],
|
|
87
|
+
<const>["type", this.getType()],
|
|
88
|
+
<const>["name", `${fctx.field_name_prefix}${this.field.name}`],
|
|
89
|
+
<const>[
|
|
90
|
+
"value",
|
|
91
|
+
!valid && raw
|
|
92
|
+
? raw.toString()
|
|
93
|
+
: parsed !== undefined && parsed !== null
|
|
94
|
+
? ((parsed as any)?.toString() as string) || ""
|
|
95
|
+
: "",
|
|
96
|
+
],
|
|
97
|
+
...(fctx.form_id ? [["form", fctx.form_id]] : []),
|
|
98
|
+
<const>["placeholder", this.getPlaceholder()],
|
|
99
|
+
...(readonly ? [["readonly", true]] : []),
|
|
100
|
+
...(required ? [["required", true]] : []),
|
|
101
|
+
...(!this.options.autocomplete ? [["autocomplete", "off"]] : []),
|
|
102
|
+
...(this.options.step ? [["step", this.options.step]] : []),
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async postInput(_: Context): Promise<FlatTemplatable> {
|
|
107
|
+
return this.options.suffix
|
|
108
|
+
? /* HTML */ `<span class="suffix">${this.options.suffix}</span>`
|
|
109
|
+
: "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async renderInput(
|
|
113
|
+
_ctx: Context,
|
|
114
|
+
attributes_str: string,
|
|
115
|
+
_data: Record<string, FormDataValue>
|
|
116
|
+
): Promise<FlatTemplatable> {
|
|
117
|
+
return `<input ${attributes_str} />`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async render<Fields extends Record<string, FormField>>(
|
|
121
|
+
fctx: FormControlContext
|
|
122
|
+
): Promise<FlatTemplatable> {
|
|
123
|
+
const id = this.getID();
|
|
124
|
+
const label = this.getLabel();
|
|
125
|
+
const type = this.getType();
|
|
126
|
+
|
|
127
|
+
const { valid, message } = await this.field.getValue(
|
|
128
|
+
fctx.ctx,
|
|
129
|
+
fctx.data
|
|
130
|
+
);
|
|
131
|
+
return inputWrapper(
|
|
132
|
+
[id, type, getRequiredClass(this.field.required)],
|
|
133
|
+
tempstream/* HTML */ `
|
|
134
|
+
${label ? `<label for="${id}">${label}</label>` : ""}
|
|
135
|
+
${await this.preInput(fctx.ctx, fctx.data)}
|
|
136
|
+
${this.renderInput(
|
|
137
|
+
fctx.ctx,
|
|
138
|
+
renderAttributes(await this.getInputAttributes(fctx)),
|
|
139
|
+
fctx.data
|
|
140
|
+
)}
|
|
141
|
+
${this.postInput(fctx.ctx)}
|
|
142
|
+
${~valid && !this.options.hide_errors && fctx.validate
|
|
143
|
+
? `<div class="input__error">${message}</div>`
|
|
144
|
+
: ""}
|
|
145
|
+
`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|