362 lines
9 KiB
Text
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>
|
|
}
|
|
}
|