331 lines
8.6 KiB
Text
331 lines
8.6 KiB
Text
// templui component toast - version: main installed by templui v0.71.0
|
|
package toast
|
|
|
|
import (
|
|
"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/utils"
|
|
"strconv"
|
|
)
|
|
|
|
type Variant string
|
|
type Position string
|
|
|
|
const (
|
|
VariantDefault Variant = "default"
|
|
VariantSuccess Variant = "success"
|
|
VariantError Variant = "error"
|
|
VariantWarning Variant = "warning"
|
|
VariantInfo Variant = "info"
|
|
)
|
|
|
|
const (
|
|
PositionTopRight Position = "top-right"
|
|
PositionTopLeft Position = "top-left"
|
|
PositionTopCenter Position = "top-center"
|
|
PositionBottomRight Position = "bottom-right"
|
|
PositionBottomLeft Position = "bottom-left"
|
|
PositionBottomCenter Position = "bottom-center"
|
|
)
|
|
|
|
type Props struct {
|
|
ID string
|
|
Class string
|
|
Attributes templ.Attributes
|
|
Title string
|
|
Description string
|
|
Variant Variant
|
|
Position Position
|
|
Duration int
|
|
Dismissible bool
|
|
ShowIndicator bool
|
|
Icon bool
|
|
}
|
|
|
|
templ Toast(props ...Props) {
|
|
@Script()
|
|
@ToastCSS()
|
|
{{ var p Props }}
|
|
if len(props) > 0 {
|
|
{{ p = props[0] }}
|
|
}
|
|
if p.ID == "" {
|
|
{{ p.ID = utils.RandomID() }}
|
|
}
|
|
{{ p = p.defaults() }}
|
|
{{ isTop := p.Position == PositionTopRight || p.Position == PositionTopLeft || p.Position == PositionTopCenter }}
|
|
{{ isBottom := p.Position == PositionBottomRight || p.Position == PositionBottomLeft || p.Position == PositionBottomCenter }}
|
|
<div
|
|
id={ p.ID }
|
|
data-toast
|
|
data-duration={ strconv.Itoa(p.Duration) }
|
|
class={ utils.TwMerge(
|
|
"z-50 fixed pointer-events-auto p-4",
|
|
"opacity-0 transform transition-all duration-300 ease-out",
|
|
utils.If(isTop, "top-0"),
|
|
utils.If(isBottom, "bottom-0"),
|
|
utils.If(isTop, "translate-y-4"),
|
|
utils.If(isBottom, "-translate-y-4"),
|
|
utils.If(p.Position == PositionTopRight || p.Position == PositionBottomRight, "right-0"),
|
|
utils.If(p.Position == PositionTopLeft || p.Position == PositionBottomLeft, "left-0"),
|
|
utils.If(p.Position == PositionTopCenter || p.Position == PositionBottomCenter, "left-1/2 -translate-x-1/2"),
|
|
"w-full md:max-w-[420px]",
|
|
p.Class,
|
|
) }
|
|
{ p.Attributes... }
|
|
>
|
|
<div class="w-full bg-background rounded-lg shadow-xs border pt-5 pb-4 px-4 flex items-center justify-center relative overflow-hidden">
|
|
if p.ShowIndicator {
|
|
@indicator(p)
|
|
}
|
|
if p.Icon {
|
|
@toastIcon(p)
|
|
}
|
|
<span class="flex-1 min-w-0">
|
|
@title(p)
|
|
@description(p)
|
|
</span>
|
|
if p.Dismissible {
|
|
@dismissButton()
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ indicator(p Props) {
|
|
<div class="absolute top-0 left-0 right-0 h-1">
|
|
<div
|
|
data-toast-progress
|
|
class={ utils.TwMerge(
|
|
"absolute inset-0",
|
|
typeClass(p.Variant),
|
|
) }
|
|
></div>
|
|
</div>
|
|
}
|
|
|
|
templ toastIcon(p Props) {
|
|
if p.Variant == VariantSuccess {
|
|
@icon.CircleCheck(icon.Props{Size: 22, Class: "text-green-500 mr-3 flex-shrink-0"})
|
|
} else if p.Variant == VariantError {
|
|
@icon.CircleX(icon.Props{Size: 22, Class: "text-red-500 mr-3 flex-shrink-0"})
|
|
} else if p.Variant == VariantWarning {
|
|
@icon.TriangleAlert(icon.Props{Size: 22, Class: "text-yellow-500 mr-3 flex-shrink-0"})
|
|
} else if p.Variant == VariantInfo {
|
|
@icon.Info(icon.Props{Size: 22, Class: "text-blue-500 mr-3 flex-shrink-0"})
|
|
}
|
|
}
|
|
|
|
templ title(p Props) {
|
|
if p.Title != "" {
|
|
<p class="text-sm font-semibold truncate">{ p.Title }</p>
|
|
}
|
|
}
|
|
|
|
templ description(p Props) {
|
|
if p.Description != "" {
|
|
<p class="text-sm opacity-90 mt-1">{ p.Description }</p>
|
|
}
|
|
}
|
|
|
|
templ dismissButton() {
|
|
@button.Button(button.Props{
|
|
Size: button.SizeIcon,
|
|
Variant: button.VariantGhost,
|
|
Attributes: templ.Attributes{
|
|
"aria-label": "Close",
|
|
"data-toast-dismiss": "",
|
|
"type": "button",
|
|
},
|
|
}) {
|
|
@icon.X(icon.Props{
|
|
Size: 18,
|
|
Class: "opacity-75 hover:opacity-100",
|
|
})
|
|
}
|
|
}
|
|
|
|
func (p Props) defaults() Props {
|
|
if p.Variant == "" {
|
|
p.Variant = VariantDefault
|
|
}
|
|
if p.Position == "" {
|
|
p.Position = PositionBottomRight
|
|
}
|
|
if p.Duration == 0 {
|
|
p.Duration = 3000
|
|
}
|
|
return p
|
|
}
|
|
|
|
func typeClass(t Variant) string {
|
|
switch t {
|
|
case VariantDefault:
|
|
return "bg-gray-500"
|
|
case VariantSuccess:
|
|
return "bg-green-500"
|
|
case VariantError:
|
|
return "bg-red-500"
|
|
case VariantWarning:
|
|
return "bg-yellow-500"
|
|
case VariantInfo:
|
|
return "bg-blue-500"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
var cssHandle = templ.NewOnceHandle()
|
|
|
|
templ ToastCSS() {
|
|
@cssHandle.Once() {
|
|
<style nonce={ templ.GetNonce(ctx) }>
|
|
[data-toast].toast-enter {
|
|
opacity: 0;
|
|
/* Initial vertical offset is handled by classes in the component */
|
|
}
|
|
[data-toast].toast-enter-active {
|
|
opacity: 1;
|
|
transform: translateY(0); /* Only handle vertical transition */
|
|
}
|
|
[data-toast].toast-leave {
|
|
opacity: 1;
|
|
transform: translateY(0); /* Start leave from final vertical position */
|
|
}
|
|
[data-toast].toast-leave-active {
|
|
opacity: 0;
|
|
/* Apply final vertical offset based on position */
|
|
}
|
|
[data-toast][class*=" top-"].toast-leave-active {
|
|
transform: translateY(1rem); /* Move down */
|
|
}
|
|
[data-toast][class*=" bottom-"].toast-leave-active {
|
|
transform: translateY(-1rem); /* Move up */
|
|
}
|
|
</style>
|
|
}
|
|
}
|
|
|
|
var handle = templ.NewOnceHandle()
|
|
|
|
templ Script() {
|
|
@handle.Once() {
|
|
<script nonce={ templ.GetNonce(ctx) }>
|
|
(function() { // IIFE
|
|
if (typeof window.toastHandler === 'undefined') {
|
|
window.toastHandler = true;
|
|
window.toasts = new Map();
|
|
|
|
function initToast(toast) {
|
|
if (window.toasts.has(toast)) return;
|
|
|
|
const duration = parseInt(toast.dataset.duration || '0');
|
|
const progress = toast.querySelector('[data-toast-progress]');
|
|
const dismiss = toast.querySelector('[data-toast-dismiss]');
|
|
|
|
const state = {
|
|
timer: null,
|
|
remaining: duration,
|
|
startTime: Date.now(),
|
|
progress: progress,
|
|
paused: false
|
|
};
|
|
window.toasts.set(toast, state);
|
|
|
|
function removeToast() {
|
|
clearTimeout(state.timer);
|
|
toast.classList.remove('toast-enter-active');
|
|
toast.classList.add('toast-leave-active');
|
|
|
|
toast.addEventListener('transitionend', () => {
|
|
toast.remove();
|
|
window.toasts.delete(toast);
|
|
}, { once: true });
|
|
}
|
|
|
|
function startTimer(time) {
|
|
if (time <= 0) return;
|
|
|
|
clearTimeout(state.timer);
|
|
state.startTime = Date.now();
|
|
state.remaining = time;
|
|
state.paused = false;
|
|
state.timer = setTimeout(removeToast, time);
|
|
|
|
if (state.progress) {
|
|
state.progress.style.transition = `width ${time}ms linear`;
|
|
void state.progress.offsetWidth;
|
|
state.progress.style.width = '0%';
|
|
}
|
|
}
|
|
|
|
function pauseTimer() {
|
|
if (state.paused || state.remaining <= 0) return;
|
|
|
|
clearTimeout(state.timer);
|
|
state.remaining -= (Date.now() - state.startTime);
|
|
state.paused = true;
|
|
|
|
if (state.progress) {
|
|
const width = window.getComputedStyle(state.progress).width;
|
|
state.progress.style.transition = 'none';
|
|
state.progress.style.width = width;
|
|
}
|
|
}
|
|
|
|
function resumeTimer() {
|
|
if (!state.paused || state.remaining <= 0) return;
|
|
startTimer(state.remaining);
|
|
}
|
|
|
|
if (duration > 0) {
|
|
toast.addEventListener('mouseenter', pauseTimer);
|
|
toast.addEventListener('mouseleave', resumeTimer);
|
|
}
|
|
|
|
if (dismiss) {
|
|
dismiss.addEventListener('click', removeToast);
|
|
}
|
|
|
|
setTimeout(() => {
|
|
toast.classList.add('toast-enter-active');
|
|
if (state.progress) {
|
|
state.progress.style.width = '100%';
|
|
}
|
|
startTimer(duration);
|
|
}, 50);
|
|
}
|
|
|
|
function initAllComponents(root = document) {
|
|
const toastsToInit = [];
|
|
if (root instanceof Element && root.matches('[data-toast]')) {
|
|
if (!window.toasts.has(root)) {
|
|
toastsToInit.push(root);
|
|
}
|
|
}
|
|
if (root && typeof root.querySelectorAll === 'function') {
|
|
root.querySelectorAll('[data-toast]').forEach(toast => {
|
|
if (!window.toasts.has(toast)) {
|
|
toastsToInit.push(toast);
|
|
}
|
|
});
|
|
}
|
|
toastsToInit.forEach(initToast);
|
|
}
|
|
|
|
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>
|
|
}
|
|
}
|