frm/ui/common.templ

211 lines
8.3 KiB
Text
Raw Normal View History

2024-11-05 16:18:04 +00:00
package ui
import (
"fmt"
"strings"
"context"
"github.com/acaloiaro/frm"
"github.com/acaloiaro/frm/ui/selector"
"github.com/acaloiaro/frm/types"
"encoding/json"
"slices"
)
var loadDependenciesOnce = templ.NewOnceHandle()
templ HeroIcon(style string, name string) {
<svg data-src={ fmt.Sprintf("https://unpkg.com/heroicons/20/%s/%s.svg", style, name) } class="h-5 w-5"></svg>
}
2025-01-28 15:41:46 +00:00
type ButtonArgs struct {
2024-11-05 16:18:04 +00:00
Type string // button type, e.g. 'button' or 'submit'
Label string // the label to show
Classes []string // additional css classes to apply to the button
}
2025-01-28 15:41:46 +00:00
templ Button(args ButtonArgs, attrs templ.Attributes) {
2024-11-05 16:18:04 +00:00
<button { attrs... } class={ fmt.Sprintf("focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:cursor-not-allowed aria-disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 aria-disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 dark:aria-disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center px-8 md:px-4 py-2 %s", strings.Join(args.Classes, " ")) }>
{ children... }
{ args.Label }
</button>
}
2025-01-28 15:41:46 +00:00
templ MutedButton(args ButtonArgs, attrs templ.Attributes) {
2024-11-05 16:18:04 +00:00
<div { attrs... } class={ fmt.Sprintf("btn btn-sm text-black text-md bg-gray-50 focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:cursor-not-allowed aria-disabled:opacity-75 flex-shrink-0 font-medium rounded-md gap-x-1.5 shadow-sm dark:text-gray-900 bg-gray-100 hover:bg-gray-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center px-8 md:px-4 py-2 %s", strings.Join(args.Classes, " ")) }>
{ children... }
{ args.Label }
</div>
}
// SafePath returns mountpoint-aware SafeURL paths for the given path
func SafePath(ctx context.Context, path string) templ.SafeURL {
2025-01-14 15:51:29 +00:00
return templ.SafeURL(frm.CollectorPath(ctx, path))
2024-11-05 16:18:04 +00:00
}
// head simply provides the <head> element
2025-01-28 15:41:46 +00:00
templ Head(pageTitle string) {
2024-11-05 16:18:04 +00:00
<head>
2025-01-22 19:38:53 +00:00
<title>{ pageTitle }</title>
2024-11-05 16:18:04 +00:00
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
@loadDependenciesOnce.Once() {
2025-01-14 15:51:29 +00:00
<link href={ frm.CollectorPath(ctx, "/static/css/styles.css") } rel="stylesheet"/>
<link rel="stylesheet" href={ frm.CollectorPath(ctx, "/static/css/choices.min.css") } nonce={ templ.GetNonce(ctx) }/>
<script type="text/javascript" src={ frm.CollectorPath(ctx, "/static/js/htmx.js") } nonce={ templ.GetNonce(ctx) }></script>
<script type="text/javascript" src={ frm.CollectorPath(ctx, "/static/js/htmx-response-targets.js") } nonce={ templ.GetNonce(ctx) }></script>
<script type="text/javascript" src={ frm.CollectorPath(ctx, "/static/js/hyperscript.js") } nonce={ templ.GetNonce(ctx) }></script>
<script ytpe="text/javascript" src={ frm.CollectorPath(ctx, "/static/js/choices.min.js") } nonce={ templ.GetNonce(ctx) }></script>
2025-01-23 19:28:24 +00:00
<script type="text/javascript" src={ frm.CollectorPath(ctx, "/static/js/svg-loader.min.js") } nonce={ templ.GetNonce(ctx) } async></script>
<script type="text/javascript" src={ frm.CollectorPath(ctx, "/static/js/Sortable.min.js") } nonce={ templ.GetNonce(ctx) }></script>
2024-11-05 16:18:04 +00:00
<script type="text/javascript">
htmx.onLoad(function(content) {
var sortables = content.querySelectorAll(".sortable");
for (var i = 0; i < sortables.length; i++) {
var sortable = sortables[i];
var sortableInstance = new Sortable(sortable, {
animation: 150,
draggable: ".sortme",
onMove: function (evt) {
return evt.related.className.indexOf('htmx-indicator') === -1;
},
onEnd: function (evt) {
this.option("disabled", true);
}
});
// Re-enable sorting on the `htmx:afterSwap` event
sortable.addEventListener("htmx:afterSwap", function() {
sortableInstance.option("disabled", false);
});
}
})
function formValueChanged(fieldID, newValue) {
var formMetadata = JSON.parse(document.getElementById('form-metadata').getAttribute("data-data"));
if (formMetadata == null) {
return
}
// collect the fields that have logic monitoring the changed field
var watchingFields = Object.values(formMetadata.form.fields).filter(function(field) {
return field.logic != null && fieldID === field.logic.target_field_id
});
// no fields watch the one that changed
if (watchingFields.length == 0) {
return
}
var fieldElement = document.getElementById(fieldID)
for (i in watchingFields) {
let watchingField = watchingFields[i]
let match = false
var watcherFieldContainerID = `field-container-${watchingField.id}` // the DOM element that contains the watching field
logic = watchingField.logic
// check whether the new value is coming from a Choices.js field, in which case the new value
// is the array of chosen values, joined by commas, otherwise newValue is used as it was passed in
if (fieldElement != null && fieldElement._choices != null) {
// Choics.getValue() returns scalar for single selects and array for multi. Use Array.of
// to treat everything it returns as an array
newValue = Array.of(fieldElement._choices.getValue(true)).join(',')
}
// find if _any_ trigger values match the new value
switch (logic.field_comparator) {
case 'equal':
match = watchingField.logic.trigger_values.every(val => newValue.localeCompare(val, 'en', {sensitivity: "base"}) == 0)
break;
case 'contains':
match = watchingField.logic.trigger_values.some(val => newValue.toLowerCase().includes(val.toLowerCase()))
break;
}
// Most actions are likely to be performed upon the containing element, such as show/hide/require.
// This may of course change or be expanded, but for now, only the watcher field container is relevant
// to applying actions.
let el = document.getElementById(watcherFieldContainerID)
let actions = watchingField.logic.actions
// we have a match, execute the action
for (i in actions) {
switch (actions[i]) {
case "field_logic_trigger_show":
if (match) {
el.classList.remove("hidden");
} else {
el.classList.add("hidden");
}
break;
}
}
}
}
</script>
}
</head>
}
templ footer() {
// <footer class="relative text-gray text-xs bottom-0 px-6 py-3 bg-base-200 print:hidden"></footer>
}
// App is the primary app with all chrome
templ App(pageTitle string) {
<!DOCTYPE html>
<html lang="eng">
2025-01-28 15:41:46 +00:00
@Head(pageTitle)
2025-01-10 18:52:12 +00:00
<body>
2024-11-05 16:18:04 +00:00
<main hx-ext="response-targets">
{ children... }
</main>
@footer()
</body>
</html>
}
2025-01-28 15:41:46 +00:00
// FormUrl returns form builder URLs for the given form and additional path arguments
func FormUrl[T string | templ.SafeURL](ctx context.Context, form frm.Form, path ...string) T {
2024-11-05 16:18:04 +00:00
p := ""
if len(path) == 1 {
p = path[0]
}
2025-01-28 15:41:46 +00:00
return T(F("%s%s", frm.BuilderPathForm(ctx, form.ID), p))
2024-11-05 16:18:04 +00:00
}
2025-01-28 15:41:46 +00:00
// FieldOptionsAsSelectorOptions returns all of a field's FieldOptions as selector.Options
func FieldOptionsAsSelectorOptions(form frm.Form, field types.FormField) (options []selector.Option) {
2024-11-05 16:18:04 +00:00
for _, option := range field.Options {
selected := false
for _, f := range form.Fields {
if f.Logic != nil && slices.Contains(f.Logic.TriggerValues, option.ID.String()) {
selected = true
}
}
options = append(options, selector.Option{
ID: option.ID,
Label: option.Label,
Value: option.ID.String(),
Selected: selected,
})
}
return
}
// ViewerMetdata contains data needed by the viewer component. It is rendered to JSON and accessed via Javascript.
type ViewerMetadata struct {
Form frm.Form `json:"form"`
}
func (v ViewerMetadata) JSON() string {
b, err := json.Marshal(v)
if err != nil {
return "{}"
}
return string(b)
}
2025-01-28 15:41:46 +00:00
// F formats strings; shorthand for fmt.Sprintf
func F(s string, args ...any) string {
return fmt.Sprintf(s, args...)
}