@sealcode/sealgen 0.8.3 → 0.8.5

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 (107) hide show
  1. package/@types/forms/controls/autocomplete.d.ts +1 -1
  2. package/@types/forms/controls/checkbox-group.d.ts +3 -2
  3. package/@types/forms/controls/collapsible-checkbox-group.d.ts +1 -1
  4. package/@types/forms/controls/collapsible-field-group.d.ts +1 -1
  5. package/@types/forms/controls/controls.d.ts +3 -0
  6. package/@types/forms/controls/dropdown.d.ts +6 -5
  7. package/@types/forms/controls/field-group.d.ts +3 -3
  8. package/@types/forms/controls/file.d.ts +14 -0
  9. package/@types/forms/controls/form-control.d.ts +4 -1
  10. package/@types/forms/controls/form-field-control.d.ts +1 -1
  11. package/@types/forms/controls/multiple-files.d.ts +31 -0
  12. package/@types/forms/controls/number.d.ts +17 -0
  13. package/@types/forms/controls/simple-input.d.ts +20 -27
  14. package/@types/forms/controls/textarea.d.ts +9 -7
  15. package/@types/forms/fields/field.d.ts +4 -1
  16. package/@types/forms/fields/file.d.ts +16 -0
  17. package/@types/forms/fields/files-multiple.d.ts +12 -0
  18. package/@types/forms/fields/multiple-files.d.ts +14 -0
  19. package/@types/forms/fields/number.d.ts +10 -1
  20. package/@types/forms/form.d.ts +7 -4
  21. package/@types/forms/multiform.d.ts +3 -3
  22. package/@types/page/list.d.ts +1 -1
  23. package/@types/page/mountable-with-fields.d.ts +3 -2
  24. package/@types/page/page.d.ts +1 -1
  25. package/@types/sanitize.d.ts +1 -1
  26. package/lib/forms/controls/autocomplete.js.map +1 -1
  27. package/lib/forms/controls/checkbox-group.js +11 -5
  28. package/lib/forms/controls/checkbox-group.js.map +1 -1
  29. package/lib/forms/controls/collapsible-checkbox-group.js +2 -2
  30. package/lib/forms/controls/collapsible-checkbox-group.js.map +1 -1
  31. package/lib/forms/controls/collapsible-field-group.js.map +1 -1
  32. package/lib/forms/controls/controls.js +3 -0
  33. package/lib/forms/controls/controls.js.map +1 -1
  34. package/lib/forms/controls/derived.js +12 -11
  35. package/lib/forms/controls/derived.js.map +1 -1
  36. package/lib/forms/controls/dropdown.js +25 -18
  37. package/lib/forms/controls/dropdown.js.map +1 -1
  38. package/lib/forms/controls/field-group.js +3 -3
  39. package/lib/forms/controls/field-group.js.map +1 -1
  40. package/lib/forms/controls/file.js +38 -0
  41. package/lib/forms/controls/file.js.map +1 -0
  42. package/lib/forms/controls/form-control.js +1 -0
  43. package/lib/forms/controls/form-control.js.map +1 -1
  44. package/lib/forms/controls/form-field-control.js.map +1 -1
  45. package/lib/forms/controls/multiple-files.js +137 -0
  46. package/lib/forms/controls/multiple-files.js.map +1 -0
  47. package/lib/forms/controls/number.js +34 -0
  48. package/lib/forms/controls/number.js.map +1 -0
  49. package/lib/forms/controls/simple-input.js +20 -10
  50. package/lib/forms/controls/simple-input.js.map +1 -1
  51. package/lib/forms/controls/textarea.js +13 -22
  52. package/lib/forms/controls/textarea.js.map +1 -1
  53. package/lib/forms/fields/enum-multiple.js +4 -1
  54. package/lib/forms/fields/enum-multiple.js.map +1 -1
  55. package/lib/forms/fields/field.js +8 -1
  56. package/lib/forms/fields/field.js.map +1 -1
  57. package/lib/forms/fields/file.js +58 -0
  58. package/lib/forms/fields/file.js.map +1 -0
  59. package/lib/forms/fields/files-multiple.js +18 -0
  60. package/lib/forms/fields/files-multiple.js.map +1 -0
  61. package/lib/forms/fields/multiple-files.js +19 -0
  62. package/lib/forms/fields/multiple-files.js.map +1 -0
  63. package/lib/forms/fields/number.js +6 -2
  64. package/lib/forms/fields/number.js.map +1 -1
  65. package/lib/forms/form.js +38 -21
  66. package/lib/forms/form.js.map +1 -1
  67. package/lib/forms/multiform.js +8 -8
  68. package/lib/forms/multiform.js.map +1 -1
  69. package/lib/page/list.js +9 -6
  70. package/lib/page/list.js.map +1 -1
  71. package/lib/page/mountable-with-fields.js +16 -3
  72. package/lib/page/mountable-with-fields.js.map +1 -1
  73. package/lib/page/page.js +1 -1
  74. package/lib/page/page.js.map +1 -1
  75. package/lib/sanitize.js +3 -0
  76. package/lib/sanitize.js.map +1 -1
  77. package/lib/tests/manual/multiform.js +13 -13
  78. package/lib/tests/manual/multiform.js.map +1 -1
  79. package/package.json +2 -2
  80. package/src/forms/controls/autocomplete.ts +1 -5
  81. package/src/forms/controls/checkbox-group.ts +17 -6
  82. package/src/forms/controls/collapsible-checkbox-group.ts +10 -2
  83. package/src/forms/controls/collapsible-field-group.ts +2 -4
  84. package/src/forms/controls/controls.ts +3 -0
  85. package/src/forms/controls/derived.ts +12 -11
  86. package/src/forms/controls/dropdown.ts +51 -34
  87. package/src/forms/controls/field-group.ts +6 -4
  88. package/src/forms/controls/file.ts +50 -0
  89. package/src/forms/controls/form-control.ts +5 -1
  90. package/src/forms/controls/form-field-control.ts +2 -1
  91. package/src/forms/controls/multiple-files.ts +213 -0
  92. package/src/forms/controls/number.ts +52 -0
  93. package/src/forms/controls/simple-input.ts +67 -37
  94. package/src/forms/controls/textarea.ts +23 -24
  95. package/src/forms/fields/enum-multiple.ts +3 -1
  96. package/src/forms/fields/field.ts +12 -2
  97. package/src/forms/fields/file.ts +86 -0
  98. package/src/forms/fields/multiple-files.ts +20 -0
  99. package/src/forms/fields/number.ts +17 -2
  100. package/src/forms/form.ts +84 -42
  101. package/src/forms/forms.scss +4 -1
  102. package/src/forms/multiform.ts +26 -13
  103. package/src/page/list.ts +10 -7
  104. package/src/page/mountable-with-fields.ts +26 -4
  105. package/src/page/page.ts +3 -1
  106. package/src/sanitize.ts +4 -1
  107. package/src/tests/manual/multiform.ts +19 -13
@@ -13,6 +13,7 @@ export type DefaultCheckboxOptions = {
13
13
  readonly?: boolean;
14
14
  name?: string;
15
15
  checkboxNameToLabel?: (name: string) => string;
16
+ classes?: string[];
16
17
  };
17
18
 
18
19
  export class CheckboxGroup<
@@ -31,14 +32,22 @@ export class CheckboxGroup<
31
32
  messages: FormMessage[],
32
33
  field_prefix: string,
33
34
  form_id: string,
34
- checkboxes: CheckboxWithValue[]
35
+ checkboxes: CheckboxWithValue[],
36
+ show_field_errors: boolean
35
37
  ): Promise<FlatTemplatable> {
36
- return new FieldGroup(checkboxes, { label: this.options.label }).render(
38
+ return new FieldGroup(checkboxes, {
39
+ label: this.options.label,
40
+ classes: [
41
+ ...(this.options.classes || []),
42
+ "field-group--checkboxes",
43
+ ],
44
+ }).render(
37
45
  ctx,
38
46
  data,
39
47
  messages,
40
48
  field_prefix,
41
- form_id
49
+ form_id,
50
+ show_field_errors
42
51
  );
43
52
  }
44
53
 
@@ -47,7 +56,8 @@ export class CheckboxGroup<
47
56
  data: Record<string, FormDataValue>,
48
57
  messages: FormMessage[],
49
58
  field_prefix: string,
50
- form_id: string
59
+ form_id: string,
60
+ show_field_errors: boolean
51
61
  ): Promise<FlatTemplatable> {
52
62
  const { parsed: values } = await this.field.getValue(ctx, data);
53
63
  const checkboxes = this.field.options.map(
@@ -55,7 +65,7 @@ export class CheckboxGroup<
55
65
  new CheckboxWithValue(
56
66
  new ProxyFormField(
57
67
  false,
58
- this.field.name + "[]",
68
+ this.field.name,
59
69
  false,
60
70
  values.includes(option)
61
71
  ),
@@ -75,7 +85,8 @@ export class CheckboxGroup<
75
85
  messages,
76
86
  field_prefix,
77
87
  form_id,
78
- checkboxes
88
+ checkboxes,
89
+ show_field_errors
79
90
  );
80
91
  }
81
92
  }
@@ -19,13 +19,21 @@ export class CollapsibleCheckboxGroup extends CheckboxGroup<
19
19
  messages: FormMessage[],
20
20
  field_prefix: string,
21
21
  form_id: string,
22
- checkboxes: CheckboxWithValue[]
22
+ checkboxes: CheckboxWithValue[],
23
+ show_field_errors: boolean
23
24
  ): Promise<FlatTemplatable> {
24
25
  return new CollapsibleFieldGroup(checkboxes, {
25
26
  label: this.options.label,
26
27
  textWhenOpen: this.options.textWhenOpen,
27
28
  textWhenClosed: this.options.textWhenClosed,
28
29
  groupID: this.options.groupID,
29
- }).render(ctx, data, messages, field_prefix, form_id);
30
+ }).render(
31
+ ctx,
32
+ data,
33
+ messages,
34
+ field_prefix,
35
+ form_id,
36
+ show_field_errors
37
+ );
30
38
  }
31
39
  }
@@ -1,13 +1,11 @@
1
- import { Context } from "koa";
2
- import { FlatTemplatable, tempstream } from "tempstream";
3
- import { FormDataValue, FormMessage } from "../form";
1
+ import { FlatTemplatable } from "tempstream";
4
2
  import { FieldGroup } from "./field-group";
5
3
  import { randomUUID } from "crypto";
6
4
  import { FormControl } from "./form-control";
7
5
 
8
6
  type CollapsibleFieldGroupOptions = {
9
7
  label?: string;
10
- classes?: string;
8
+ classes?: string[];
11
9
  openOnStart?: boolean;
12
10
  textWhenOpen?: string;
13
11
  textWhenClosed?: string;
@@ -20,3 +20,6 @@ export * from "./checkbox-group";
20
20
  export * from "./collapsible-field-group";
21
21
  export * from "./collapsible-checkbox-group";
22
22
  export * from "./autocomplete";
23
+ export * from "./file";
24
+ export * from "./multiple-files";
25
+ export * from "./number";
@@ -35,18 +35,19 @@ export class Derived<
35
35
  })
36
36
  .join(",")},
37
37
  };
38
- const form = document.currentScript.closest("form");
38
+ const form_container =
39
+ document.currentScript.closest(".form-container");
39
40
  const render = ${this._render.toString()};
40
41
  function getValues() {
41
42
  return Object.fromEntries(
42
43
  ${JSON.stringify(
43
44
  this.fields.map((field) => field.name)
44
45
  )}.map((field_name) => {
45
- let input = form.querySelector(
46
+ let input = form_container.querySelector(
46
47
  \`input[name="\${field_name}"]\`
47
48
  );
48
49
  if (input.type === "radio") {
49
- input = form.querySelector(
50
+ input = form_container.querySelector(
50
51
  \`input[name="\${field_name}"]:checked\`
51
52
  );
52
53
  }
@@ -55,14 +56,14 @@ export class Derived<
55
56
  );
56
57
  }
57
58
  function setupWatcher(field_name) {
58
- form.querySelectorAll(
59
- \`input[name="\${field_name}"]\`
60
- ).forEach((input) => {
61
- input.onchange = () => {
62
- document.querySelector("#${id}").innerHTML =
63
- render(getValues(), consts);
64
- };
65
- });
59
+ form_container
60
+ .querySelectorAll(\`input[name="\${field_name}"]\`)
61
+ .forEach((input) => {
62
+ input.onchange = () => {
63
+ document.querySelector("#${id}").innerHTML =
64
+ render(getValues(), consts);
65
+ };
66
+ });
66
67
  }
67
68
  ${JSON.stringify(
68
69
  this.fields.map((field) => field.name)
@@ -1,11 +1,11 @@
1
1
  import { Context } from "koa";
2
+ import { FlatTemplatable, tempstream } from "tempstream";
2
3
  import { attribute } from "../../sanitize";
3
- import { getRequiredClass, inputWrapper } from "../../utils/input-wrapper";
4
- import { FormField, PickFromListField } from "../fields/field";
4
+ import { PickFromListField } from "../fields/field";
5
5
  import { FormDataValue } from "../form";
6
- import { FormFieldControl } from "./controls";
6
+ import { SimpleInput } from "./simple-input";
7
7
 
8
- export class Dropdown extends FormFieldControl {
8
+ export class Dropdown extends SimpleInput<string | null> {
9
9
  constructor(
10
10
  public field: PickFromListField<boolean>,
11
11
  public options: {
@@ -18,41 +18,58 @@ export class Dropdown extends FormFieldControl {
18
18
  autocomplete: true,
19
19
  }
20
20
  ) {
21
- super([field]);
21
+ super(field);
22
22
  }
23
23
 
24
- async render<Fields extends Record<string, FormField>>(
24
+ async getInputAttributes(
25
25
  ctx: Context,
26
+ data: Record<string, FormDataValue>,
27
+ field_prefix: string,
28
+ form_id: string
29
+ ): Promise<Record<string, string | boolean>> {
30
+ const original = await super.getInputAttributes(
31
+ ctx,
32
+ data,
33
+ field_prefix,
34
+ form_id
35
+ );
36
+ return {
37
+ ...original,
38
+ ...Object.fromEntries([
39
+ ...(this.options.autosubmit
40
+ ? [
41
+ [
42
+ "onchange",
43
+ (original.onchange || "") +
44
+ ";this.form.submit();",
45
+ ],
46
+ ]
47
+ : []),
48
+ ]),
49
+ };
50
+ }
51
+
52
+ async renderInput(
53
+ ctx: Context,
54
+ attributes_str: string,
26
55
  data: Record<string, FormDataValue>
27
- ) {
56
+ ): Promise<FlatTemplatable> {
28
57
  const { parsed: picked_value } = await this.field.getValue(ctx, data);
29
- const name = this.field.name;
30
- return inputWrapper(
31
- [name, getRequiredClass(this.field.required)],
32
- /* HTML */ `<label for="${name}">${this.options.label}</label
33
- ><select
34
- name="${this.field.name}"
35
- id="${name}"
36
- ${this.options.autosubmit
37
- ? `onchange='this.form.submit()'`
38
- : ""}
39
- ${!this.options.autocomplete ? `autocomplete="off"` : ""}
40
- >
41
- ${
42
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
43
- Promise.resolve(this.field.generateOptions(ctx)).then(
44
- (options) =>
45
- Object.entries(options).map(
46
- ([value, text]) =>
47
- `<option value="${attribute(value)}" ${
48
- (value || "") == picked_value
49
- ? "selected"
50
- : ""
51
- }>${text}</option>`
52
- )
58
+ return /* HTML */ tempstream`<select ${attributes_str}>
59
+ ${
60
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
61
+ Promise.resolve(this.field.generateOptions(ctx)).then(
62
+ (options) =>
63
+ Object.entries(options).map(
64
+ ([value, text]) =>
65
+ `<option value="${attribute(value)}" ${
66
+ (value || "") == picked_value
67
+ ? "selected"
68
+ : ""
69
+ }>${text}</option>`
53
70
  )
54
- }
55
- </select>`
56
- );
71
+ )
72
+ }
73
+ </select>`;
57
74
  }
58
75
  }
@@ -9,7 +9,7 @@ export class FieldGroup extends FormFieldControl {
9
9
  public controls: FormControl[],
10
10
  public options: {
11
11
  label?: string;
12
- classes?: string;
12
+ classes?: string[];
13
13
  containerID?: string;
14
14
  } = {}
15
15
  ) {
@@ -45,13 +45,14 @@ export class FieldGroup extends FormFieldControl {
45
45
  data: Record<string, FormDataValue>,
46
46
  messages: FormMessage[],
47
47
  field_prefix: string,
48
- form_id: string
48
+ form_id: string,
49
+ show_field_errors: boolean
49
50
  ): Promise<FlatTemplatable> {
50
51
  const { label, classes } = this.options;
51
52
 
52
53
  return tempstream/* HTML */ `
53
54
  <div
54
- class="field-group ${classes || ""}"
55
+ class="field-group ${(classes || []).join(" ")}"
55
56
  ${this.options.containerID
56
57
  ? `id="${this.options.containerID}"`
57
58
  : ""}
@@ -72,7 +73,8 @@ export class FieldGroup extends FormFieldControl {
72
73
  data,
73
74
  messages,
74
75
  field_prefix,
75
- form_id
76
+ form_id,
77
+ show_field_errors
76
78
  )
77
79
  )}
78
80
  </div>
@@ -0,0 +1,50 @@
1
+ import { Context } from "koa";
2
+ import { FileContainer } from "../fields/file";
3
+ import { FormDataValue } from "../form";
4
+ import { SimpleInput } from "./simple-input";
5
+
6
+ export class File extends SimpleInput<FileContainer> {
7
+ getType() {
8
+ return "file";
9
+ }
10
+
11
+ async preInput(ctx: Context, data: Record<string, FormDataValue>) {
12
+ const { parsed } = await this.field.getValue(ctx, data);
13
+ return /* HTML */ ` ${parsed.old?.filename || ""}
14
+ <input
15
+ type="hidden"
16
+ name="${this.field.name}[old][id]"
17
+ value="${parsed.old?.id || ""}"
18
+ />
19
+ <input
20
+ type="hidden"
21
+ name="${this.field.name}[old][filename]"
22
+ value="${parsed.old?.filename || ""}"
23
+ />`;
24
+ }
25
+
26
+ async getInputAttributes(
27
+ ctx: Context,
28
+ data: Record<string, FormDataValue>,
29
+ field_prefix: string,
30
+ form_id: string
31
+ ) {
32
+ const original = await super.getInputAttributes(
33
+ ctx,
34
+ data,
35
+ field_prefix,
36
+ form_id
37
+ );
38
+ const { parsed } = await this.field.getValue(ctx, data);
39
+ return {
40
+ ...original,
41
+ name: this.field.name + "[new]",
42
+ required: original.required && !parsed.old,
43
+ value: false,
44
+ onchange:
45
+ (original.onchange || "") +
46
+ ";" +
47
+ "filename = this.value.split('\\\\').slice(-1)[0]; this.parentElement.style.setProperty('--filename', '\\'Wybrany plik: ' + filename + '\\'')",
48
+ };
49
+ }
50
+ }
@@ -1,3 +1,4 @@
1
+ import Router from "@koa/router";
1
2
  import { Context } from "koa";
2
3
  import { FlatTemplatable } from "tempstream";
3
4
  import { FormDataValue, FormMessage } from "../form";
@@ -8,7 +9,10 @@ export abstract class FormControl {
8
9
  data: Record<string, FormDataValue>,
9
10
  messages: FormMessage[],
10
11
  field_name_prefix: string,
11
- form_id: string
12
+ form_id: string,
13
+ validate: boolean
12
14
  ): FlatTemplatable | Promise<FlatTemplatable>;
13
15
  abstract role: "input" | "decoration" | "messages" | "submit";
16
+
17
+ mount(router: Router) {}
14
18
  }
@@ -21,6 +21,7 @@ export abstract class FormFieldControl extends FormControl {
21
21
  data: Record<string, FormDataValue>,
22
22
  messages: [],
23
23
  field_name_prefix: string,
24
- form_id: string
24
+ form_id: string,
25
+ validate: boolean
25
26
  ): FlatTemplatable | Promise<FlatTemplatable>;
26
27
  }
@@ -0,0 +1,213 @@
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 { FormDataValue } from "../form";
13
+ import { FormFieldControl } from "./form-field-control";
14
+ import { is, predicates } from "@sealcode/ts-predicates";
15
+
16
+ export type MultipleFilesOptions = { label?: string; uploadLabel?: string };
17
+
18
+ export class MultipleFiles extends FormFieldControl {
19
+ public options: MultipleFilesOptions;
20
+ constructor(
21
+ public field: MultipleFilesField,
22
+ options?: MultipleFilesOptions
23
+ ) {
24
+ super([field]);
25
+ this.options = options || {};
26
+ }
27
+
28
+ mount(router: Router) {
29
+ router.get(this.getFrameRelativeURL(), async (ctx) => {
30
+ ctx.body = this.renderFrame(
31
+ ctx,
32
+ await this.renderFrameContent(ctx)
33
+ );
34
+ });
35
+
36
+ router.post(
37
+ this.getFrameRelativeURL() + "/delete/:file_item_id",
38
+ async (ctx) => {
39
+ await this.deleteFileAssociation(ctx, ctx.params.file_item_id);
40
+ ctx.body = this.renderFrame(
41
+ ctx,
42
+ await this.renderFrameContent(ctx)
43
+ );
44
+ }
45
+ );
46
+
47
+ router.post(this.getFrameRelativeURL() + "/add", async (ctx) => {
48
+ const file = ctx.$body.file;
49
+ if (!file || !is(file, predicates.object)) {
50
+ ctx.body = "Missing file";
51
+ return;
52
+ }
53
+ await this.addFileAssociation(ctx, file as unknown as SealiousFile);
54
+ ctx.body = this.renderFrame(
55
+ ctx,
56
+ await this.renderFrameContent(ctx)
57
+ );
58
+ });
59
+ }
60
+
61
+ async deleteFileAssociation(ctx: Context, file_item_id: string) {
62
+ await ctx.$app.collections[
63
+ this.field.collection_field.referencing_collection
64
+ ].removeByID(ctx.$context, file_item_id);
65
+ }
66
+
67
+ async addFileAssociation(ctx: Context, file: SealiousFile) {
68
+ const file_field = this.getFileField();
69
+ if (!file_field) {
70
+ throw new Error("No file field in referencing collection");
71
+ }
72
+ const body = {
73
+ [this.field.collection_field.referencing_field]:
74
+ await this.field.getItemId(ctx),
75
+ [file_field.name]: file,
76
+ };
77
+ await ctx.$app.collections[
78
+ this.field.collection_field.referencing_collection
79
+ ].create(ctx.$context, body);
80
+ }
81
+
82
+ getFrameRelativeURL() {
83
+ return `${this.field.name}_files`;
84
+ }
85
+
86
+ getFrameID(): string {
87
+ // the "A" is necessary here
88
+ return `A${this.field.name}__multiple-fields-control`;
89
+ }
90
+
91
+ renderFrame(ctx: Context, content?: FlatTemplatable) {
92
+ return tempstream/* HTML */ `<turbo-frame
93
+ ${content ? "" : `src="./${this.getFrameRelativeURL()}"`}
94
+ id="${this.getFrameID()}"
95
+ target="_top"
96
+ >
97
+ ${content || ""}
98
+ </turbo-frame>`;
99
+ }
100
+
101
+ render(
102
+ ctx: Context,
103
+ _data: Record<string, FormDataValue>,
104
+ _messages: [],
105
+ field_name_prefix: string,
106
+ form_id: string,
107
+ validate: boolean
108
+ ): FlatTemplatable | Promise<FlatTemplatable> {
109
+ return this.renderFrame(ctx, "");
110
+ }
111
+
112
+ getReferencingCollection() {
113
+ const result =
114
+ this.field.collection_field.app.collections[
115
+ this.field.collection_field.referencing_collection
116
+ ];
117
+ return result;
118
+ }
119
+
120
+ getFileField(): SealiousField | null {
121
+ for (const [field_name, field] of Object.entries(
122
+ this.getReferencingCollection().fields
123
+ )) {
124
+ if (field.handles_large_data) {
125
+ return field;
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+
131
+ async extractFileFromItem(
132
+ item: CollectionItem
133
+ ): Promise<SealiousFile | null> {
134
+ const sealious_field = this.getFileField();
135
+ if (!sealious_field) {
136
+ return null;
137
+ }
138
+ return await SealiousFile.fromID(
139
+ (item.collection as Collection).app,
140
+ item.get(sealious_field.name).id
141
+ );
142
+ }
143
+
144
+ async renderFileItem(
145
+ fileItem: CollectionItem,
146
+ file: SealiousFile
147
+ ): Promise<FlatTemplatable> {
148
+ return /* HTML */ `<li>
149
+ <a href="${file.getURL()}" data-turbo="false">
150
+ ${file.filename || "no filename"}
151
+ </a>
152
+ <form
153
+ data-turbo-frame="${this.getFrameID()}"
154
+ action="${this.getFrameRelativeURL()}/delete/${fileItem.id}"
155
+ method="POST"
156
+ >
157
+ <input type="submit" value="X" class="file-list-action" />
158
+ </form>
159
+ </li>`;
160
+ }
161
+
162
+ async getFileItems(ctx: Context) {
163
+ const item_id = await this.field.getItemId(ctx);
164
+ const {
165
+ items: [item],
166
+ } = await this.field.collection_field.collection
167
+ .list(ctx.$context)
168
+ .ids([item_id])
169
+ .attach({ [this.field.collection_field.name]: true })
170
+ .fetch();
171
+ return item
172
+ .getAttachments(this.field.collection_field.name)
173
+ .filter((f) => f);
174
+ }
175
+
176
+ async renderFrameContent(ctx: Context): Promise<FlatTemplatable> {
177
+ const files = (
178
+ await Promise.all(
179
+ (
180
+ await this.getFileItems(ctx)
181
+ ).map(async (item) => [
182
+ item,
183
+ await this.extractFileFromItem(item),
184
+ ])
185
+ )
186
+ ).filter(([_, f]) => f !== null) as [CollectionItem, SealiousFile][];
187
+
188
+ return inputWrapper(
189
+ ["multiple-files", this.field.name],
190
+ tempstream/* HTML */ `
191
+ <label>${this.options.label || this.field.name}</label>
192
+ <ul>
193
+ ${files.map(([item, file]) =>
194
+ this.renderFileItem(item, file)
195
+ )}
196
+ </ul>
197
+ <form
198
+ action="${this.getFrameRelativeURL()}/add"
199
+ method="POST"
200
+ enctype="multipart/form-data"
201
+ data-turbo-frame="${this.getFrameID()}"
202
+ >
203
+ <input type="file" name="file" />
204
+ <input
205
+ type="submit"
206
+ value="${this.options.uploadLabel || "Upload"}"
207
+ class="file-list-action"
208
+ />
209
+ </form>
210
+ `
211
+ );
212
+ }
213
+ }
@@ -0,0 +1,52 @@
1
+ import { Context } from "koa";
2
+ import { FlatTemplatable } from "tempstream";
3
+ import { FormField, NumberField } from "../fields/field";
4
+ import { FormDataValue } from "../form";
5
+ import { SimpleInput, SimpleInputOptions } from "./simple-input";
6
+
7
+ export type NumberOptions = SimpleInputOptions &
8
+ Partial<{ step: number; suffix: string }>;
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 postInput(_: Context): Promise<FlatTemplatable> {
22
+ return this.options.suffix
23
+ ? /* HTML */ `<span class="suffix">${this.options.suffix}</span>`
24
+ : "";
25
+ }
26
+
27
+ async getInputAttributes(
28
+ ctx: Context,
29
+ data: Record<string, FormDataValue>,
30
+ field_prefix: string,
31
+ form_id: string
32
+ ): Promise<Record<string, string | boolean>> {
33
+ const original = await super.getInputAttributes(
34
+ ctx,
35
+ data,
36
+ field_prefix,
37
+ form_id
38
+ );
39
+ return {
40
+ ...original,
41
+ ...Object.fromEntries([
42
+ ...(this.field.options.min
43
+ ? [["min", this.field.options.min]]
44
+ : []),
45
+ ...(this.field.options.max
46
+ ? [["max", this.field.options.max]]
47
+ : []),
48
+ ...(this.options.step ? [["step", this.options.step]] : []),
49
+ ]),
50
+ };
51
+ }
52
+ }