xtablo-source/go-backend/internal/web/ui/select.templ
2026-05-10 22:04:09 +02:00

251 lines
7.4 KiB
Text

package ui
type SelectOption struct {
Value string
Label string
Disabled bool
}
type SelectProps struct {
ID string
Name string
Placeholder string
Value string
Values []string
Multiple bool
Options []SelectOption
Attrs templ.Attributes
}
templ Select(props SelectProps) {
<div
class="ui-select"
data-ui-select-root
data-ui-select-multiple={ selectBoolData(props.Multiple) }
data-placeholder={ selectPlaceholder(props) }
data-selected-label={ selectSelectedLabel(props) }
>
<select
id={ selectNativeID(props.ID, props.Name) }
name={ props.Name }
class="ui-select-native"
data-ui-select-native
if props.Multiple {
multiple
}
{ props.Attrs... }
>
if !props.Multiple && selectPlaceholder(props) != "" {
<option value="">{ selectPlaceholder(props) }</option>
}
for _, option := range props.Options {
<option
value={ option.Value }
if option.Disabled {
disabled
}
if selectOptionSelected(props, option.Value) {
selected
}
>{ option.Label }</option>
}
</select>
<button
id={ inputID(props.ID, props.Name) }
type="button"
class="ui-select-control"
data-ui-select-trigger
aria-haspopup="listbox"
aria-expanded="false"
aria-controls={ selectMenuID(props.ID, props.Name) }
if selectIsDisabled(props.Attrs) {
disabled
}
>
<span class="ui-select-value-wrapper" data-ui-select-label>
if props.Multiple {
if len(selectSelectedLabels(props)) == 0 {
<span class="ui-select-placeholder">{ selectPlaceholder(props) }</span>
} else {
for _, label := range selectSelectedLabels(props) {
<span class="ui-select-chip">{ label }</span>
}
}
} else if selectSelectedLabel(props) != "" {
{ selectSelectedLabel(props) }
} else {
<span class="ui-select-placeholder">{ selectPlaceholder(props) }</span>
}
</span>
<span class="ui-select-arrow-zone">
<svg class="ui-select-arrow-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m6 9 6 6 6-6"></path>
</svg>
</span>
</button>
<div
id={ selectMenuID(props.ID, props.Name) }
class="ui-select-menu"
data-ui-select-menu
role="listbox"
if props.Multiple {
aria-multiselectable="true"
}
hidden
>
for _, option := range props.Options {
<button
type="button"
class={ selectMenuOptionClass(selectOptionSelected(props, option.Value), option.Disabled) }
data-ui-select-option
data-value={ option.Value }
data-label={ option.Label }
role="option"
aria-selected={ selectBoolData(selectOptionSelected(props, option.Value)) }
if option.Disabled {
disabled
}
>
<span class="ui-select-option-text">{ option.Label }</span>
<span class="ui-select-option-check" aria-hidden="true">✓</span>
</button>
}
</div>
<script>
(function () {
if (!window.__uiSelectInitAll) {
window.__uiSelectSetOpen = function (root, open) {
var trigger = root.querySelector("[data-ui-select-trigger]");
var menu = root.querySelector("[data-ui-select-menu]");
if (!trigger || !menu) {
return;
}
root.classList.toggle("is-open", open);
trigger.setAttribute("aria-expanded", open ? "true" : "false");
menu.hidden = !open;
};
window.__uiSelectCloseAll = function (exceptRoot) {
document.querySelectorAll("[data-ui-select-root].is-open").forEach(function (root) {
if (root !== exceptRoot) {
window.__uiSelectSetOpen(root, false);
}
});
};
window.__uiSelectSync = function (root) {
var nativeSelect = root.querySelector("[data-ui-select-native]");
var outlet = root.querySelector("[data-ui-select-label]");
var placeholder = root.getAttribute("data-placeholder") || "";
var multiple = root.getAttribute("data-ui-select-multiple") === "true";
if (!nativeSelect || !outlet) {
return;
}
var labels = Array.from(nativeSelect.options).filter(function (option) {
return option.selected && option.value !== "";
}).map(function (option) {
return option.textContent;
});
root.setAttribute("data-selected-label", labels.join(", "));
outlet.innerHTML = "";
if (labels.length === 0) {
var placeholderNode = document.createElement("span");
placeholderNode.className = "ui-select-placeholder";
placeholderNode.textContent = placeholder;
outlet.appendChild(placeholderNode);
} else if (multiple) {
labels.forEach(function (label) {
var chip = document.createElement("span");
chip.className = "ui-select-chip";
chip.textContent = label;
outlet.appendChild(chip);
});
} else {
outlet.textContent = labels[0];
}
root.querySelectorAll("[data-ui-select-option]").forEach(function (optionButton) {
var selected = Array.from(nativeSelect.options).some(function (option) {
return option.value === optionButton.getAttribute("data-value") && option.selected;
});
optionButton.classList.toggle("is-selected", selected);
optionButton.setAttribute("aria-selected", selected ? "true" : "false");
});
};
window.__uiSelectToggleValue = function (root, optionButton) {
var nativeSelect = root.querySelector("[data-ui-select-native]");
var multiple = root.getAttribute("data-ui-select-multiple") === "true";
var value = optionButton.getAttribute("data-value");
if (!nativeSelect || value === null) {
return;
}
Array.from(nativeSelect.options).forEach(function (option) {
if (option.value !== value && !multiple) {
option.selected = false;
}
if (option.value === value) {
option.selected = multiple ? !option.selected : true;
}
});
window.__uiSelectSync(root);
nativeSelect.dispatchEvent(new Event("change", { bubbles: true }));
if (!multiple) {
window.__uiSelectSetOpen(root, false);
}
};
window.__uiSelectInitAll = function (scope) {
(scope || document).querySelectorAll("[data-ui-select-root]").forEach(function (root) {
window.__uiSelectSync(root);
});
};
document.addEventListener("click", function (event) {
var optionButton = event.target.closest("[data-ui-select-option]");
if (optionButton) {
var optionRoot = optionButton.closest("[data-ui-select-root]");
if (optionRoot) {
window.__uiSelectToggleValue(optionRoot, optionButton);
}
return;
}
var trigger = event.target.closest("[data-ui-select-trigger]");
if (trigger) {
var root = trigger.closest("[data-ui-select-root]");
var shouldOpen = root && !root.classList.contains("is-open");
window.__uiSelectCloseAll(root);
if (root) {
window.__uiSelectSetOpen(root, shouldOpen);
}
return;
}
if (!event.target.closest("[data-ui-select-root]")) {
window.__uiSelectCloseAll(null);
}
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
window.__uiSelectCloseAll(null);
}
});
document.addEventListener("htmx:afterSwap", function (event) {
window.__uiSelectInitAll(event.target);
});
}
window.__uiSelectInitAll(document);
})();
</script>
</div>
}