mirror of
https://github.com/acaloiaro/frm
synced 2026-07-21 10:12:23 +00:00
258 lines
9 KiB
Text
258 lines
9 KiB
Text
package fields
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/acaloiaro/frm"
|
|
"github.com/acaloiaro/frm/ui/selector"
|
|
"github.com/acaloiaro/frm/types"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// RequiredFieldIndicator visually indicates that a field is required
|
|
templ RequiredFieldIndicator() {
|
|
<span class="text-red-500 required-dot">*</span>
|
|
}
|
|
|
|
// FieldLabel is a HTML label for a field
|
|
templ FieldLabel(field types.FormField) {
|
|
<label for={ field.ID.String() } class="text-slate-700 text-xl pb-5">
|
|
{ field.Label }
|
|
if field.Required {
|
|
@RequiredFieldIndicator()
|
|
}
|
|
</label>
|
|
}
|
|
|
|
templ FieldTypeIcon(fieldType types.FormFieldType) {
|
|
<div class="p-1 rounded-md flex items-center justify-center bg-blue-100 text-blue-900 ml-2">
|
|
switch int(fieldType) {
|
|
case int(types.FormFieldTypeTextSingle), int(types.FormFieldTypeTextMultiple):
|
|
<!-- hericons: bards-3-bottom-left -->
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"></path>
|
|
</svg>
|
|
case int(types.FormFieldTypeSingleSelect), int(types.FormFieldTypeMultiSelect):
|
|
<!-- heroicons: chevron-up-down -->
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"></path>
|
|
</svg>
|
|
case int(types.FormFieldTypeSingleChoice), int(types.FormFieldTypeSingleChoiceSpaced):
|
|
<!-- heroicons: outline star -->
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"></path>
|
|
</svg>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// View displays fields with the appropriate UI component
|
|
templ View(field types.FormField) {
|
|
switch field.Type {
|
|
case types.FormFieldTypeTextSingle:
|
|
@singleLineTextView(field)
|
|
case types.FormFieldTypeTextMultiple:
|
|
@multiLineTextView(field)
|
|
case types.FormFieldTypeSingleSelect, types.FormFieldTypeMultiSelect:
|
|
@selectView(field)
|
|
case types.FormFieldTypeSingleChoice:
|
|
@SingleChoice(field)
|
|
case types.FormFieldTypeSingleChoiceSpaced:
|
|
@SingleChoiceSpaced(field)
|
|
}
|
|
}
|
|
|
|
templ singleLineTextView(field types.FormField) {
|
|
@FieldLabel(field)
|
|
<input
|
|
id={ field.ID.String() }
|
|
name={ field.ID.String() }
|
|
placeholder={ field.Placeholder }
|
|
type="text"
|
|
autocomplete="off"
|
|
if field.Required {
|
|
required
|
|
}
|
|
_={ fmt.Sprintf("on keyup debounced at 250ms trigger field_change(field_id: '%s', value: my.value)", field.ID.String()) }
|
|
class="flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm focus:outline-none focus:ring-2 focus:border-2 focus:ring-opacity-100 px-4 py-2 text-base resize-y block rounded-xl bg-sky-50"
|
|
/>
|
|
<div hx-swap-oob="true" id={ fmt.Sprintf("errors-%s", field.ID.String()) }></div>
|
|
}
|
|
|
|
templ multiLineTextView(field types.FormField) {
|
|
@FieldLabel(field)
|
|
<textarea
|
|
id={ field.ID.String() }
|
|
name={ field.ID.String() }
|
|
class="flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100 px-4 py-2 text-base resize-y block rounded-xl bg-sky-50"
|
|
placeholder={ field.Placeholder }
|
|
autocomplete="off"
|
|
if field.Required {
|
|
required
|
|
}
|
|
_={ fmt.Sprintf("on keyup debounced at 250ms trigger field_change(field_id: '%s', value: my.value)", field.ID.String()) }
|
|
rows="3"
|
|
></textarea>
|
|
<div
|
|
id={ fmt.Sprintf("errors-%s", field.ID.String()) }
|
|
class="text-red-400"
|
|
></div>
|
|
}
|
|
|
|
templ selectView(field types.FormField) {
|
|
@selector.Selector(selector.SelectArgs{
|
|
ID: field.ID.String(),
|
|
Name: field.ID.String(),
|
|
Label: field.Label,
|
|
LabelClass: "text-slate-700 text-xl py-1",
|
|
Required: field.Required,
|
|
Placeholder: field.Placeholder,
|
|
Multiple: field.Type == types.FormFieldTypeMultiSelect,
|
|
Options: ToSelectorOpts(field.Options, false),
|
|
SearchDisabled: true,
|
|
EditItems: false,
|
|
Hyperscript: fmt.Sprintf("on change trigger field_change(field_id: '%s', value: my.value)", field.ID.String()),
|
|
})
|
|
<div
|
|
id={ fmt.Sprintf("errors-%s", field.ID.String()) }
|
|
class="text-red-400"
|
|
></div>
|
|
}
|
|
|
|
// FormFieldTypeLabel is the UI label for FormFieldTypes, e.g. 'text_single' -> "Single line text"
|
|
templ FormFieldTypeLabel(fieldType types.FormFieldType) {
|
|
<div class="flex gap-3">
|
|
@FieldTypeIcon(types.FormFieldType(fieldType))
|
|
<label class="w-full cursor-pointer truncate">
|
|
// TODO: this int conversion is a janky artifact of keeping the public `frm` interface unpolluted by the
|
|
// `db` package. Something should be done to correct this.
|
|
// FIELD_TYPES: field types may be added/modified/removed below
|
|
switch int(fieldType) {
|
|
case int(types.FormFieldTypeTextSingle):
|
|
Single-line text
|
|
case int(types.FormFieldTypeTextMultiple):
|
|
Multi-line text
|
|
case int(types.FormFieldTypeSingleSelect):
|
|
Single select
|
|
case int(types.FormFieldTypeMultiSelect):
|
|
Multi select
|
|
case int(types.FormFieldTypeSingleChoice):
|
|
Single Choice
|
|
case int(types.FormFieldTypeSingleChoiceSpaced):
|
|
Single Choice (spaced)
|
|
}
|
|
</label>
|
|
</div>
|
|
}
|
|
|
|
// ToSelectorOpts converts a slices of types.Option to a slice of selector.Option
|
|
func ToSelectorOpts(opts []types.Option, selectAll bool) (sopts []selector.Option) {
|
|
for _, opt := range opts {
|
|
if selectAll {
|
|
opt.Selected = true
|
|
}
|
|
sopts = append(sopts, (selector.Option)(opt))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// ToSelectorOptsStr converts a slices of types.Option to a slice of selector.Option
|
|
func ToSelectorOptsStr(opts []string, selectAll bool) (sopts []selector.Option) {
|
|
// TODO: Fix -- adding an empty option because the first select <option> is selected by default, for some reason
|
|
sopts = append(sopts, (selector.Option)(selector.Option{
|
|
Value: "",
|
|
}))
|
|
for _, opt := range opts {
|
|
sopts = append(sopts, selector.Option{
|
|
Label: opt,
|
|
Value: opt,
|
|
Selected: selectAll,
|
|
})
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// FieldsAsSelector returns all of a form's fields as selector.Options to be used in a selector.Selector dropdown
|
|
// fieldID is the ID of the field for which the options are being rendered
|
|
func FieldsAsSelectorOptions(form frm.Form, fieldID uuid.UUID) (options []selector.Option) {
|
|
for _, field := range form.Fields {
|
|
// fields should not show themselves as options
|
|
if field.ID == fieldID {
|
|
continue
|
|
}
|
|
selected := false
|
|
for _, f := range form.Fields {
|
|
if f.ID == fieldID && f.Logic != nil && f.Logic.TargetFieldID == field.ID {
|
|
selected = true
|
|
}
|
|
}
|
|
options = append(options, selector.Option{
|
|
ID: field.ID,
|
|
Label: field.Label,
|
|
Value: field.ID.String(),
|
|
Selected: selected,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
// fieldTypeToDefaultValuesJSON converts field types to their devault values.
|
|
// This function takes a fieldType and converts it to the default values for that field type,
|
|
// and encodes it as a JSON string.
|
|
func fieldTypeToDefaultValuesJSON(fieldType types.FormFieldType) string {
|
|
label := ""
|
|
placeholder := ""
|
|
switch int(fieldType) {
|
|
case int(types.FormFieldTypeTextSingle), int(types.FormFieldTypeTextMultiple):
|
|
label = "New Text Field"
|
|
placeholder = "Enter some text"
|
|
case int(types.FormFieldTypeSingleSelect):
|
|
label = "New Single Select Field"
|
|
placeholder = "Choose An Item"
|
|
case int(types.FormFieldTypeMultiSelect):
|
|
label = "New Multi Select Field"
|
|
placeholder = "Choose Items"
|
|
}
|
|
|
|
field := types.FormField{
|
|
Label: label,
|
|
Placeholder: placeholder,
|
|
Required: false,
|
|
Hidden: false,
|
|
Type: fieldType,
|
|
}
|
|
json, err := json.Marshal(field)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(json)
|
|
}
|
|
|
|
// toFrmFields converts frm.FormFields (map[string]frm.FormField) --> []frm.FormField
|
|
func toFrmFields(fields types.FormFields) (ffields []types.FormField) {
|
|
for _, field := range fields {
|
|
ffields = append(ffields, (types.FormField)(field))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Sort form fields by Order
|
|
func SortFields(fields types.FormFields) (sorted []types.FormField) {
|
|
sorted = toFrmFields(fields)
|
|
sort.Sort(types.FormFieldSortByOrder(sorted))
|
|
return
|
|
}
|
|
|
|
// FieldName generates the HTML form field name for form fileds
|
|
func FieldName(field types.FormField, group, name string) string {
|
|
if group == "" {
|
|
return fmt.Sprintf("[%s]%s", field.ID.String(), name)
|
|
}
|
|
|
|
return fmt.Sprintf("[%s][%s]%s", field.ID.String(), group, name)
|
|
}
|