scanfile/server/web/templui/components/selectbox/select_box.templ
2025-06-03 15:44:56 +02:00

419 lines
11 KiB
Text

// templui component selectbox - version: main installed by templui v0.71.0
package selectbox
import (
"context"
"fmt"
"git.jmbit.de/jmb/scanfile/server/web/templui/components/button"
"git.jmbit.de/jmb/scanfile/server/web/templui/components/icon"
"git.jmbit.de/jmb/scanfile/server/web/templui/components/popover"
"git.jmbit.de/jmb/scanfile/server/web/templui/utils"
"strconv"
)
type contextKey string
var contentIDKey contextKey = "contentID"
type Props struct {
ID string
Class string
Attributes templ.Attributes
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
Name string
Required bool
Disabled bool
HasError bool
}
type ValueProps struct {
ID string
Class string
Attributes templ.Attributes
Placeholder string
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
type GroupProps struct {
ID string
Class string
Attributes templ.Attributes
}
type LabelProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ItemProps struct {
ID string
Class string
Attributes templ.Attributes
Value string
Selected bool
Disabled bool
}
templ SelectBox(props ...Props) {
@Script()
{{
var p Props
if len(props) > 0 {
p = props[0]
}
wrapperID := p.ID
if wrapperID == "" {
wrapperID = utils.RandomID()
}
contentID := fmt.Sprintf("%s-content", wrapperID)
ctx = context.WithValue(ctx, contentIDKey, contentID)
}}
<div
id={ wrapperID }
class={ utils.TwMerge("select-container w-full relative", p.Class) }
{ p.Attributes... }
>
@popover.Popover() {
{ children... }
}
</div>
}
templ Trigger(props ...TriggerProps) {
{{
var p TriggerProps
if len(props) > 0 {
p = props[0]
}
contentID, ok := ctx.Value(contentIDKey).(string)
if !ok {
contentID = "fallback-select-content-id"
}
}}
@popover.Trigger(popover.TriggerProps{
For: contentID,
TriggerType: popover.TriggerTypeClick,
}) {
@button.Button(button.Props{
ID: p.ID,
Type: "button",
Variant: button.VariantOutline,
Class: utils.TwMerge(
"w-full select-trigger flex items-center justify-between focus:ring-2 focus:ring-offset-2",
utils.If(p.HasError, "border-destructive ring-destructive"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"data-content-id": contentID,
"tabindex": "0",
"required": strconv.FormatBool(p.Required),
},
p.Attributes,
),
}) {
<input
type="hidden"
if p.Name != "" {
name={ p.Name }
}
required?={ p.Required }
/>
{ children... }
<span class="pointer-events-none ml-1">
@icon.ChevronDown(icon.Props{
Size: 16,
Class: "text-muted-foreground",
})
</span>
}
}
}
templ Value(props ...ValueProps) {
{{ var p ValueProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("block truncate select-value text-muted-foreground", p.Class) }
{ p.Attributes... }
>
if p.Placeholder != "" {
{ p.Placeholder }
}
{ children... }
</span>
}
templ Content(props ...ContentProps) {
{{
var p ContentProps
if len(props) > 0 {
p = props[0]
}
contentID, ok := ctx.Value(contentIDKey).(string)
if !ok {
contentID = "fallback-select-content-id"
}
}}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: popover.PlacementBottomStart,
Offset: 4,
MatchWidth: true,
Class: utils.TwMerge(
"p-1 select-content z-50 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md",
"min-w-[var(--popover-trigger-width)] w-[var(--popover-trigger-width)]",
p.Class,
),
Attributes: utils.MergeAttributes(
templ.Attributes{
"role": "listbox",
"tabindex": "-1",
},
p.Attributes,
),
}) {
{ children... }
}
}
templ Group(props ...GroupProps) {
{{ var p GroupProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("p-1", p.Class) }
role="group"
{ p.Attributes... }
>
{ children... }
</div>
}
templ Label(props ...LabelProps) {
{{ var p LabelProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("px-2 py-1.5 text-sm font-medium", p.Class) }
{ p.Attributes... }
>
{ children... }
</span>
}
templ Item(props ...ItemProps) {
{{ var p ItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"select-item relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-light outline-none",
"hover:bg-accent hover:text-accent-foreground",
"focus:bg-accent focus:text-accent-foreground",
utils.If(p.Selected, "bg-accent text-accent-foreground"),
utils.If(p.Disabled, "pointer-events-none opacity-50"),
p.Class,
),
}
role="option"
data-value={ p.Value }
data-selected={ strconv.FormatBool(p.Selected) }
data-disabled={ strconv.FormatBool(p.Disabled) }
tabindex="0"
{ p.Attributes... }
>
<span class="truncate select-item-text">
{ children... }
</span>
<span
class={
utils.TwMerge(
"select-check absolute right-2 flex h-3.5 w-3.5 items-center justify-center",
utils.IfElse(p.Selected, "opacity-100", "opacity-0"),
),
}
>
@icon.Check(icon.Props{Size: 16})
</span>
</div>
}
var handle = templ.NewOnceHandle()
templ Script() {
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
(function() { // IIFE
function initSelect(wrapper) {
if (!wrapper || wrapper.hasAttribute('data-initialized')) return;
wrapper.setAttribute('data-initialized', 'true');
const triggerButton = wrapper.querySelector('button.select-trigger');
if (!triggerButton) {
console.error("Select box: Trigger button (.select-trigger) not found in wrapper", wrapper);
return;
}
const contentID = triggerButton.dataset.contentId;
const content = contentID ? document.getElementById(contentID) : null;
const valueEl = triggerButton.querySelector('.select-value');
const hiddenInput = triggerButton.querySelector('input[type="hidden"]');
if (!content || !valueEl || !hiddenInput) {
console.error("Select box: Missing required elements for initialization.", { wrapper, contentID, contentExists: !!content, valueElExists: !!valueEl, hiddenInputExists: !!hiddenInput });
return;
}
// Initialize display value if an item is pre-selected
const selectedItem = content.querySelector('.select-item[data-selected="true"]');
if (selectedItem) {
const itemText = selectedItem.querySelector('.select-item-text');
if (itemText) {
valueEl.textContent = itemText.textContent;
valueEl.classList.remove('text-muted-foreground');
}
if (hiddenInput) {
hiddenInput.value = selectedItem.getAttribute('data-value') || '';
}
}
// Reset visual state of items
function resetItemStyles() {
content.querySelectorAll('.select-item').forEach(item => {
if (item.getAttribute('data-selected') === 'true') {
item.classList.add('bg-accent', 'text-accent-foreground');
item.classList.remove('bg-muted');
} else {
item.classList.remove('bg-accent', 'text-accent-foreground', 'bg-muted');
}
});
}
// Select an item
function selectItem(item) {
if (!item || item.getAttribute('data-disabled') === 'true') return;
const value = item.getAttribute('data-value');
const itemText = item.querySelector('.select-item-text');
// Reset all items in this content
content.querySelectorAll('.select-item').forEach(el => {
el.setAttribute('data-selected', 'false');
el.classList.remove('bg-accent', 'text-accent-foreground', 'bg-muted');
const check = el.querySelector('.select-check');
if (check) check.classList.replace('opacity-100', 'opacity-0');
});
// Mark new selection
item.setAttribute('data-selected', 'true');
item.classList.add('bg-accent', 'text-accent-foreground');
const check = item.querySelector('.select-check');
if (check) check.classList.replace('opacity-0', 'opacity-100');
// Update display value
if (valueEl && itemText) { // Check if valueEl exists
valueEl.textContent = itemText.textContent;
valueEl.classList.remove('text-muted-foreground');
}
// Update hidden input & trigger change event
if (hiddenInput && value !== null) { // Check if hiddenInput exists
hiddenInput.value = value;
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
}
// Close the popover using the correct contentID
if (window.closePopover) {
window.closePopover(contentID, true);
} else {
console.warn("closePopover function not found");
}
}
// Event Listeners for Items (delegated from content for robustness)
content.addEventListener('click', (e) => {
const item = e.target.closest('.select-item');
if (item) selectItem(item);
});
content.addEventListener('keydown', (e) => {
const item = e.target.closest('.select-item');
if (item && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
selectItem(item);
}
// Add other keyboard navigation (Up/Down/Home/End) if desired
});
// Event: Mouse hover on items (delegated)
content.addEventListener('mouseover', e => {
const item = e.target.closest('.select-item');
if (!item || item.getAttribute('data-disabled') === 'true') return;
// Reset all others first
content.querySelectorAll('.select-item').forEach(el => {
el.classList.remove('bg-accent', 'text-accent-foreground', 'bg-muted');
});
// Apply hover style only if not selected
if (item.getAttribute('data-selected') !== 'true') {
item.classList.add('bg-accent', 'text-accent-foreground');
}
});
// Reset hover styles when mouse leaves the content area
content.addEventListener('mouseleave', resetItemStyles);
}
function initAllComponents(root = document) {
const containers = root.querySelectorAll('.select-container:not([data-initialized])');
if (root instanceof Element && root.matches('.select-container') && !root.hasAttribute('data-initialized')) {
initSelect(root);
} else {
containers.forEach(initSelect);
}
}
const handleHtmxSwap = (event) => {
const target = event.detail.target || event.detail.elt;
if (target instanceof Element) {
requestAnimationFrame(() => initAllComponents(target));
}
};
initAllComponents();
document.addEventListener('DOMContentLoaded', () => initAllComponents());
document.body.addEventListener('htmx:afterSwap', handleHtmxSwap);
document.body.addEventListener('htmx:oobAfterSwap', handleHtmxSwap);
})(); // End of IIFE
</script>
}
}