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

362 lines
9 KiB
Text

// templui component modal - version: main installed by templui v0.71.0
package modal
import (
"git.jmbit.de/jmb/scanfile/server/web/templui/utils"
"strconv"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
DisableClickAway bool
DisableESC bool
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
Disabled bool
ModalID string // ID of the modal to trigger
}
type CloseProps struct {
ID string
Class string
Attributes templ.Attributes
ModalID string // ID of the modal to close (optional, defaults to closest modal)
}
type HeaderProps struct {
ID string
Class string
Attributes templ.Attributes
}
type BodyProps struct {
ID string
Class string
Attributes templ.Attributes
}
type FooterProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Modal(props ...Props) {
@Script()
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = "modal-" + utils.RandomID() }}
}
<div
id={ p.ID }
data-modal
data-disable-click-away={ strconv.FormatBool(p.DisableClickAway) }
data-disable-esc={ strconv.FormatBool(p.DisableESC) }
class="modal-container fixed inset-0 z-50 flex items-center justify-center overflow-y-auto opacity-0 transition-opacity duration-300 ease-out hidden"
aria-labelledby={ p.ID + "-title" }
role="dialog"
aria-modal="true"
{ p.Attributes... }
>
<div data-modal-backdrop class="fixed inset-0 bg-background/70 bg-opacity-50" aria-hidden="true"></div>
<div
id={ p.ID + "-content" }
data-modal-content
class={
utils.TwMerge(
"modal-content relative bg-background rounded-lg border text-left overflow-hidden shadow-xl transform transition-all sm:my-8 w-full scale-95 opacity-0", // Base classes + transition start
"duration-300 ease-out", // Enter duration
p.Class,
),
}
>
{ children... }
</div>
</div>
}
templ Trigger(props ...TriggerProps) {
{{ var p TriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
data-modal-trigger
if p.ModalID != "" {
data-modal-target-id={ p.ModalID }
}
class={
utils.TwMerge(
utils.IfElse(p.Disabled, "cursor-not-allowed opacity-50", "cursor-pointer"),
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</span>
}
templ Close(props ...CloseProps) {
{{ var p CloseProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
data-modal-close
if p.ModalID != "" {
data-modal-target-id={ p.ModalID }
}
class={ utils.TwMerge("cursor-pointer", p.Class) }
{ p.Attributes... }
>
{ children... }
</span>
}
templ Header(props ...HeaderProps) {
{{ var p HeaderProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("px-4 pt-5 pb-4 sm:p-6 sm:pb-4 text-lg leading-6 font-medium text-foreground", p.Class) }
{ p.Attributes... }
>
<h3 class="text-lg leading-6 font-medium text-foreground" id={ p.ID + "-title" }>
// Ensure title ID matches aria-labelledby
{ children... }
</h3>
</div>
}
templ Body(props ...BodyProps) {
{{ var p BodyProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("px-4 pt-5 pb-4 sm:p-6 sm:pb-4", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Footer(props ...FooterProps) {
{{ var p FooterProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
var handle = templ.NewOnceHandle()
templ Script() {
@handle.Once() {
<script nonce={ templ.GetNonce(ctx) }>
if (typeof window.modalState === 'undefined') {
window.modalState = {
openModalId: null
};
}
(function() { // IIFE
function closeModal(modal, immediate = false) {
if (!modal || modal.style.display === 'none') return;
const content = modal.querySelector('[data-modal-content]');
const modalId = modal.id;
// Apply leaving transitions
modal.classList.remove('opacity-100');
modal.classList.add('opacity-0');
if (content) {
content.classList.remove('scale-100', 'opacity-100');
content.classList.add('scale-95', 'opacity-0');
}
function hideModal() {
modal.style.display = 'none';
if (window.modalState.openModalId === modalId) {
window.modalState.openModalId = null;
document.body.style.overflow = '';
}
}
if (immediate) {
hideModal();
} else {
setTimeout(hideModal, 300);
}
}
function openModal(modal) {
if (!modal) return;
// Close any open modal first
if (window.modalState.openModalId) {
const openModal = document.getElementById(window.modalState.openModalId);
if (openModal && openModal !== modal) {
closeModal(openModal, true);
}
}
const content = modal.querySelector('[data-modal-content]');
// Display and prepare for animation
modal.style.display = 'flex';
// Store as currently open modal
window.modalState.openModalId = modal.id;
document.body.style.overflow = 'hidden';
// Force reflow before adding transition classes
void modal.offsetHeight;
// Start animations
modal.classList.remove('opacity-0');
modal.classList.add('opacity-100');
if (content) {
content.classList.remove('scale-95', 'opacity-0');
content.classList.add('scale-100', 'opacity-100');
// Focus first focusable element
setTimeout(() => {
const focusable = content.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusable) focusable.focus();
}, 50);
}
}
function closeModalById(modalId, immediate = false) {
const modal = document.getElementById(modalId);
if (modal) closeModal(modal, immediate);
}
function openModalById(modalId) {
const modal = document.getElementById(modalId);
if (modal) openModal(modal);
}
function handleClickAway(e) {
const openModalId = window.modalState.openModalId;
if (!openModalId) return;
const modal = document.getElementById(openModalId);
if (!modal || modal.getAttribute('data-disable-click-away') === 'true') return;
const content = modal.querySelector('[data-modal-content]');
const trigger = e.target.closest('[data-modal-trigger]');
if (content && !content.contains(e.target) &&
(!trigger || trigger.getAttribute('data-modal-target-id') !== openModalId)) {
closeModal(modal);
}
}
function handleEscKey(e) {
if (e.key !== 'Escape' || !window.modalState.openModalId) return;
const modal = document.getElementById(window.modalState.openModalId);
if (modal && modal.getAttribute('data-disable-esc') !== 'true') {
closeModal(modal);
}
}
function initTrigger(trigger) {
const targetId = trigger.getAttribute('data-modal-target-id');
if (!targetId) return;
trigger.addEventListener('click', () => {
if (!trigger.hasAttribute('disabled') &&
!trigger.classList.contains('opacity-50')) {
openModalById(targetId);
}
});
}
function initCloseButton(closeBtn) {
closeBtn.addEventListener('click', () => {
const targetId = closeBtn.getAttribute('data-modal-target-id');
if (targetId) {
closeModalById(targetId);
} else {
const modal = closeBtn.closest('[data-modal]');
if (modal && modal.id) {
closeModal(modal);
}
}
});
}
function initAllComponents(root = document) {
if (root instanceof Element && root.matches('[data-modal-trigger]')) {
initTrigger(root);
}
for (const trigger of root.querySelectorAll('[data-modal-trigger]')) {
initTrigger(trigger);
}
if (root instanceof Element && root.matches('[data-modal-close]')) {
initCloseButton(root);
}
for (const closeBtn of root.querySelectorAll('[data-modal-close]')) {
initCloseButton(closeBtn);
}
}
const handleHtmxSwap = (event) => {
const target = event.detail.target || event.detail.elt;
if (target instanceof Element) {
requestAnimationFrame(() => initAllComponents(target));
}
};
if (typeof window.modalEventsInitialized === 'undefined') {
document.addEventListener('click', handleClickAway);
document.addEventListener('keydown', handleEscKey);
window.modalEventsInitialized = true;
}
initAllComponents();
document.addEventListener('DOMContentLoaded', () => initAllComponents());
document.body.addEventListener('htmx:afterSwap', handleHtmxSwap);
document.body.addEventListener('htmx:oobAfterSwap', handleHtmxSwap);
})(); // End of IIFE
</script>
}
}