Skip to content

Commit 736ffe1

Browse files
authored
fix(js): resolve timer registration, timer conflicts, and add IIFE wrapper (#12)
1 parent c72ce38 commit 736ffe1

3 files changed

Lines changed: 206 additions & 136 deletions

File tree

assets/js/script.js

Lines changed: 203 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,212 @@
1-
/** Toast direction constants */
2-
const DIRECTION_TOP = 'top'
3-
const DIRECTION_BOTTOM = 'bottom'
4-
5-
/**
6-
* @typedef {Object} GoasterJSConfig
7-
* @property {number} [dismissTimer] - Time (in ms) before the toast auto-dismisses.
8-
* @property {number} [removeDuration] - Duration (in ms) for the removal animation.
9-
*/
10-
11-
/**
12-
* Initializes the Goaster system with optional configuration for toast timing.
13-
*
14-
* @param {GoasterJSConfig} [config={}] - Configuration options.
15-
* @returns {{ dismissTimer: number, removeDuration: number }} The resolved configuration.
16-
*/
17-
function initGoaster({ dismissTimer = 3000, removeDuration = 500 } = {}) {
18-
return { dismissTimer, removeDuration }
19-
}
20-
21-
/**
22-
* Removes known "show" classes from the toast element.
23-
* @param {HTMLElement} element - The toast element.
24-
*/
25-
function removeShowClasses(element) {
26-
const showClasses = ['gttShow', 'gttShowFromTop', 'gttShowFromBottom']
27-
element.classList.remove(...showClasses)
28-
}
29-
30-
/**
31-
* Performs the exit animation for a toast element and removes it from the DOM after the animation.
32-
*
33-
* @param {HTMLElement} element - The toast element to animate and remove.
34-
* @param {boolean} animated - Whether to apply an exit animation.
35-
* @param {string} positionName - The position of the toast used to determine animation direction.
36-
* @param {number} duration - The duration before removal (in ms).
37-
*/
38-
function performExitAnimationAndRemove(
39-
element,
40-
animated,
41-
positionName,
42-
duration = 500
43-
) {
44-
const classesToAdd = ['gttClose']
45-
46-
if (animated) {
47-
classesToAdd.push(
48-
positionName.startsWith(DIRECTION_TOP)
49-
? 'gttCloseFromTop'
50-
: 'gttCloseFromBottom'
51-
)
1+
(function () {
2+
/** Toast direction constants */
3+
const DIRECTION_TOP = "top";
4+
const DIRECTION_BOTTOM = "bottom";
5+
6+
/**
7+
* @typedef {Object} GoasterJSConfig
8+
* @property {number} [dismissTimer] - Time (in ms) before the toast auto-dismisses.
9+
* @property {number} [removeDuration] - Duration (in ms) for the removal animation.
10+
*/
11+
12+
/**
13+
* Initializes the Goaster system with optional configuration for toast timing.
14+
*
15+
* @param {GoasterJSConfig} [config={}] - Configuration options.
16+
* @returns {{ dismissTimer: number, removeDuration: number }} The resolved configuration.
17+
*/
18+
function initGoaster({ dismissTimer = 3000, removeDuration = 500 } = {}) {
19+
return { dismissTimer, removeDuration };
5220
}
5321

54-
element.classList.add(...classesToAdd)
55-
56-
removeShowClasses(element)
57-
setTimeout(() => element.remove(), duration)
58-
}
22+
/**
23+
* Removes known "show" classes from the toast element.
24+
* @param {HTMLElement} element - The toast element.
25+
*/
26+
function removeShowClasses(element) {
27+
const showClasses = ["gttShow", "gttShowFromTop", "gttShowFromBottom"];
28+
element.classList.remove(...showClasses);
29+
}
5930

60-
/**
61-
* Initializes and animates the progress bar within a toast notification element.
62-
*
63-
* @param {HTMLElement} element - The toast notification element containing the progress bar.
64-
* @param {number} duration - Duration in milliseconds for the progress bar animation.
65-
*/
66-
function animateProgressBar(element, duration = 3000) {
67-
const progressBarElement = element.querySelector('.gttProgressBar')
31+
/**
32+
* Performs the exit animation for a toast element and removes it from the DOM
33+
* after the animation ends. Uses the `animationend` event when animation is
34+
* applied, with a fallback timeout in case the event never fires.
35+
*
36+
* @param {HTMLElement} element - The toast element to animate and remove.
37+
* @param {boolean} animated - Whether to apply an exit animation.
38+
* @param {string} positionName - The position of the toast used to determine animation direction.
39+
* @param {number} duration - The fallback duration before removal (in ms).
40+
*/
41+
function performExitAnimationAndRemove(
42+
element,
43+
animated,
44+
positionName,
45+
duration = 500,
46+
) {
47+
const classesToAdd = ["gttClose"];
48+
49+
if (animated) {
50+
classesToAdd.push(
51+
positionName.startsWith(DIRECTION_TOP)
52+
? "gttCloseFromTop"
53+
: "gttCloseFromBottom",
54+
);
55+
}
56+
57+
element.classList.add(...classesToAdd);
58+
removeShowClasses(element);
59+
60+
if (animated) {
61+
var removed = false;
62+
var fallbackTimer = setTimeout(function () {
63+
if (!removed) {
64+
removed = true;
65+
element.remove();
66+
}
67+
}, duration);
68+
69+
element.addEventListener(
70+
"animationend",
71+
function () {
72+
if (!removed) {
73+
removed = true;
74+
clearTimeout(fallbackTimer);
75+
element.remove();
76+
}
77+
},
78+
{ once: true },
79+
);
80+
} else {
81+
element.remove();
82+
}
83+
}
6884

69-
if (progressBarElement) {
70-
progressBarElement.style.transition = `width ${duration}ms linear`
71-
progressBarElement.style.width = '100%'
85+
/**
86+
* Initializes and animates the progress bar within a toast notification element.
87+
*
88+
* @param {HTMLElement} element - The toast notification element containing the progress bar.
89+
* @param {number} duration - Duration in milliseconds for the progress bar animation.
90+
*/
91+
function animateProgressBar(element, duration = 3000) {
92+
const progressBarElement = element.querySelector(".gttProgressBar");
93+
94+
if (progressBarElement) {
95+
progressBarElement.style.transition = "width " + duration + "ms linear";
96+
progressBarElement.style.width = "100%";
97+
98+
requestAnimationFrame(function () {
99+
progressBarElement.style.width = "0%";
100+
});
101+
}
102+
}
72103

73-
requestAnimationFrame(() => {
74-
progressBarElement.style.width = '0%'
75-
})
104+
/**
105+
* Sets up automatic dismissal for a single toast element with an optional
106+
* progress bar animation. Stores the timer ID on `dataset.timerId` so
107+
* the original `dataset.dismissTimer` config value is preserved.
108+
*
109+
* @param {HTMLElement} toast - The toast element to auto-dismiss.
110+
* @param {boolean} isWithProgressBar - Whether to animate the progress bar.
111+
* @param {number} dismissTimer - Duration before auto-dismiss (in ms).
112+
* @param {number} removeDuration - Duration for removal animation (in ms).
113+
*/
114+
function handleAutoDismiss(
115+
toast,
116+
isWithProgressBar,
117+
dismissTimer,
118+
removeDuration,
119+
) {
120+
if (isWithProgressBar) animateProgressBar(toast, dismissTimer);
121+
122+
toast.dataset.timerId = setTimeout(function () {
123+
performExitAnimationAndRemove(
124+
toast,
125+
toast.dataset.animation === "true",
126+
toast.dataset.position,
127+
removeDuration,
128+
);
129+
}, dismissTimer);
76130
}
77-
}
78-
79-
/**
80-
* Initializes the automatic dismissal of toasts with an optional progress bar animation.
81-
*
82-
* @param {boolean} isWithProgressBar - Whether to animate the progress bar.
83-
* @param {number} dismissTimer - Duration before auto-dismiss (in ms).
84-
* @param {number} removeDuration - Duration for removal animation (in ms).
85-
*/
86-
const handleAutoDismiss = {
87-
init(isWithProgressBar, dismissTimer, removeDuration) {
88-
document
89-
.querySelectorAll('.gttToast[role="alert"][data-auto-dismiss="true"]')
90-
.forEach((toast) => {
91-
if (isWithProgressBar) animateProgressBar(toast, dismissTimer)
92-
93-
toast.dataset.dismissTimer = setTimeout(() => {
94-
performExitAnimationAndRemove(
95-
toast,
96-
toast.dataset.animation === 'true',
97-
toast.dataset.position,
98-
removeDuration
99-
)
100-
}, dismissTimer)
101-
})
131+
132+
/**
133+
* Closes the toast by stopping propagation, clearing timers, and performing
134+
* exit animation. Reads the configured `removeDuration` from the toast's
135+
* data attribute instead of relying on the default.
136+
*
137+
* @param {Event} e - The event object.
138+
*/
139+
function closeToast(e) {
140+
e.stopPropagation();
141+
142+
const toast = e.target.closest('.gttToast[role="alert"]');
143+
if (toast) {
144+
clearTimeout(Number(toast.dataset.timerId));
145+
146+
var removeDuration = Number(toast.dataset.removeDuration) || 500;
147+
148+
performExitAnimationAndRemove(
149+
toast,
150+
toast.dataset.animation === "true",
151+
toast.dataset.position,
152+
removeDuration,
153+
);
154+
}
102155
}
103-
}
104-
105-
/**
106-
* Closes the toast by stopping propagation, clearing timers, and performing exit animation.
107-
* @param {Event} e - The event object.
108-
*/
109-
function closeToast(e) {
110-
e.stopPropagation()
111-
112-
const toast = e.target.closest('.gttToast[role="alert"]')
113-
if (toast) {
114-
clearTimeout(Number(toast.dataset.dismissTimer))
156+
157+
/**
158+
* Dismisses the most recently shown (last in DOM order) visible toast.
159+
* Used by the Escape key handler.
160+
*/
161+
function dismissLastToast() {
162+
var toasts = document.querySelectorAll(
163+
'.gttToast[role="alert"]:not(.gttClose)',
164+
);
165+
if (toasts.length === 0) return;
166+
167+
var lastToast = toasts[toasts.length - 1];
168+
clearTimeout(Number(lastToast.dataset.timerId));
169+
170+
var removeDuration = Number(lastToast.dataset.removeDuration) || 500;
171+
115172
performExitAnimationAndRemove(
116-
toast,
117-
toast.dataset.animation === 'true',
118-
toast.dataset.position
119-
)
173+
lastToast,
174+
lastToast.dataset.animation === "true",
175+
lastToast.dataset.position,
176+
removeDuration,
177+
);
120178
}
121-
}
122-
123-
/** Close toast on button click */
124-
document.body.addEventListener('click', (e) => {
125-
const closeButton = e.target.closest('.gttCloseBtn')
126-
if (closeButton) closeToast(e)
127-
})
128-
129-
/** Initialize auto-dismiss on page load */
130-
document.addEventListener('DOMContentLoaded', () => {
131-
document
132-
.querySelectorAll('.gttToast[data-auto-dismiss="true"]')
133-
.forEach((el) => {
134-
const { dismissTimer, removeDuration } = initGoaster({
135-
dismissTimer: Number(el.dataset.dismissTimer) || undefined,
136-
removeDuration: Number(el.dataset.removeDuration) || undefined
137-
})
138-
139-
const hasProgressBar = !!el.querySelector('.gttProgressBar')
140-
handleAutoDismiss.init(hasProgressBar, dismissTimer, removeDuration)
141-
})
142-
})
179+
180+
/** Close toast on button click */
181+
document.body.addEventListener("click", function (e) {
182+
var closeButton = e.target.closest(".gttCloseBtn");
183+
if (closeButton) closeToast(e);
184+
});
185+
186+
/** Dismiss the most recent toast when Escape is pressed */
187+
document.addEventListener("keydown", function (e) {
188+
if (e.key === "Escape") {
189+
dismissLastToast();
190+
}
191+
});
192+
193+
/** Initialize auto-dismiss on page load */
194+
document.addEventListener("DOMContentLoaded", function () {
195+
document
196+
.querySelectorAll('.gttToast[data-auto-dismiss="true"]')
197+
.forEach(function (el) {
198+
var config = initGoaster({
199+
dismissTimer: Number(el.dataset.dismissTimer) || undefined,
200+
removeDuration: Number(el.dataset.removeDuration) || undefined,
201+
});
202+
203+
var hasProgressBar = !!el.querySelector(".gttProgressBar");
204+
handleAutoDismiss(
205+
el,
206+
hasProgressBar,
207+
config.dismissTimer,
208+
config.removeDuration,
209+
);
210+
});
211+
});
212+
})();

components/js/script.templ

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ templ GoasterJS() {
55
@jsHandle.Once() {
66
<script type="text/javascript" nonce={ templ.GetNonce(ctx) }>
77
/* [tempo] BEGIN - Do not edit! This section is auto-generated. */
8-
const DIRECTION_TOP="top",DIRECTION_BOTTOM="bottom";function initGoaster({dismissTimer=3e3,removeDuration=500}={}){return{dismissTimer,removeDuration}}function removeShowClasses(element){const showClasses=["gttShow","gttShowFromTop","gttShowFromBottom"];element.classList.remove(...showClasses)}function performExitAnimationAndRemove(element,animated,positionName,duration=500){const classesToAdd=["gttClose"];animated&&classesToAdd.push(positionName.startsWith(DIRECTION_TOP)?"gttCloseFromTop":"gttCloseFromBottom"),element.classList.add(...classesToAdd),removeShowClasses(element),setTimeout(()=>element.remove(),duration)}function animateProgressBar(element,duration=3e3){const progressBarElement=element.querySelector(".gttProgressBar");progressBarElement&&(progressBarElement.style.transition=`width ${duration}ms linear`,progressBarElement.style.width="100%",requestAnimationFrame(()=>{progressBarElement.style.width="0%"}))}const handleAutoDismiss={init(isWithProgressBar,dismissTimer,removeDuration){document.querySelectorAll('.gttToast[role="alert"][data-auto-dismiss="true"]').forEach(toast=>{isWithProgressBar&&animateProgressBar(toast,dismissTimer),toast.dataset.dismissTimer=setTimeout(()=>{performExitAnimationAndRemove(toast,toast.dataset.animation==="true",toast.dataset.position,removeDuration)},dismissTimer)})}};function closeToast(e){e.stopPropagation();const toast=e.target.closest('.gttToast[role="alert"]');toast&&(clearTimeout(Number(toast.dataset.dismissTimer)),performExitAnimationAndRemove(toast,toast.dataset.animation==="true",toast.dataset.position))}document.body.addEventListener("click",e=>{e.target.closest(".gttCloseBtn")&&closeToast(e)}),document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll('.gttToast[data-auto-dismiss="true"]').forEach(el=>{const{dismissTimer,removeDuration}=initGoaster({dismissTimer:Number(el.dataset.dismissTimer)||void 0,removeDuration:Number(el.dataset.removeDuration)||void 0}),hasProgressBar=!!el.querySelector(".gttProgressBar");handleAutoDismiss.init(hasProgressBar,dismissTimer,removeDuration)})});
8+
(function(){const DIRECTION_TOP="top",DIRECTION_BOTTOM="bottom";function initGoaster({dismissTimer=3e3,removeDuration=500}={}){return{dismissTimer,removeDuration}}function removeShowClasses(element){const showClasses=["gttShow","gttShowFromTop","gttShowFromBottom"];element.classList.remove(...showClasses)}function performExitAnimationAndRemove(element,animated,positionName,duration=500){const classesToAdd=["gttClose"];if(animated&&classesToAdd.push(positionName.startsWith(DIRECTION_TOP)?"gttCloseFromTop":"gttCloseFromBottom"),element.classList.add(...classesToAdd),removeShowClasses(element),animated){var removed=!1,fallbackTimer=setTimeout(function(){removed||(removed=!0,element.remove())},duration);element.addEventListener("animationend",function(){removed||(removed=!0,clearTimeout(fallbackTimer),element.remove())},{once:!0})}else element.remove()}function animateProgressBar(element,duration=3e3){const progressBarElement=element.querySelector(".gttProgressBar");progressBarElement&&(progressBarElement.style.transition="width "+duration+"ms linear",progressBarElement.style.width="100%",requestAnimationFrame(function(){progressBarElement.style.width="0%"}))}function handleAutoDismiss(toast,isWithProgressBar,dismissTimer,removeDuration){isWithProgressBar&&animateProgressBar(toast,dismissTimer),toast.dataset.timerId=setTimeout(function(){performExitAnimationAndRemove(toast,toast.dataset.animation==="true",toast.dataset.position,removeDuration)},dismissTimer)}function closeToast(e){e.stopPropagation();const toast=e.target.closest('.gttToast[role="alert"]');if(toast){clearTimeout(Number(toast.dataset.timerId));var removeDuration=Number(toast.dataset.removeDuration)||500;performExitAnimationAndRemove(toast,toast.dataset.animation==="true",toast.dataset.position,removeDuration)}}function dismissLastToast(){var toasts=document.querySelectorAll('.gttToast[role="alert"]:not(.gttClose)');if(toasts.length!==0){var lastToast=toasts[toasts.length-1];clearTimeout(Number(lastToast.dataset.timerId));var removeDuration=Number(lastToast.dataset.removeDuration)||500;performExitAnimationAndRemove(lastToast,lastToast.dataset.animation==="true",lastToast.dataset.position,removeDuration)}}document.body.addEventListener("click",function(e){var closeButton=e.target.closest(".gttCloseBtn");closeButton&&closeToast(e)}),document.addEventListener("keydown",function(e){e.key==="Escape"&&dismissLastToast()}),document.addEventListener("DOMContentLoaded",function(){document.querySelectorAll('.gttToast[data-auto-dismiss="true"]').forEach(function(el){var config=initGoaster({dismissTimer:Number(el.dataset.dismissTimer)||void 0,removeDuration:Number(el.dataset.removeDuration)||void 0}),hasProgressBar=!!el.querySelector(".gttProgressBar");handleAutoDismiss(el,hasProgressBar,config.dismissTimer,config.removeDuration)})})})();
99

1010
/* [tempo] END */
1111
</script>

0 commit comments

Comments
 (0)