diff --git a/css/itflow_custom.css b/css/itflow_custom.css index 927992f21..ebcc66a62 100644 --- a/css/itflow_custom.css +++ b/css/itflow_custom.css @@ -496,12 +496,107 @@ a:focus { font-weight: 600; } -/* --- Toast fade --------------------------------------------------------- - Bootstrap's .fade is a .15s transition, which next to toastr's old 1s - fadeOut reads as a blink. Bootstrap uses the same .showing class for both - directions, so in and out cannot be timed separately without JS - .5s is - the compromise. Change the duration here to taste. */ -#itflowFlashToast.toast.fade, +/* --- Flash toast --------------------------------------------------------- + Restores the four things toastr did that a plain Bootstrap toast does not: + sits top-right rather than centred over what you are reading, carries an + icon for its type, counts its own timer down, and dismisses on a click + anywhere instead of only on the small x. + + It also appears on the FIRST PAINT. includes/inc_alert_feedback.php renders + the element with .show already set and every state change here is an + animation, so nothing waits on bootstrap.Toast - which used to mean waiting + for DOMContentLoaded, and therefore for every parser-blocking script in the + document. The inline script in that file only handles the click and removes + the node; neither gates the appearance. + + Durations are toastr's own: 300ms in, 5s on screen, 1s out. */ +.itflow-toast { + position: relative; + overflow: hidden; + cursor: pointer; + animation: + itflow-toast-in 300ms ease-out both, + itflow-toast-out 1s ease-in 5s both; +} + +/* Hovering holds it open, so a long message cannot disappear mid-sentence. + The progress bar needs its own rule - animation-play-state does not + cascade to a child's separate animation. */ +.itflow-toast:hover, +.itflow-toast:hover .itflow-toast-progress { + animation-play-state: paused; +} + +/* currentColor, so the bar inherits whichever contrast text-bg-* already + picked - white on success and danger, dark on warning and info - rather + than needing a rule per type. */ +.itflow-toast-progress { + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 3px; + background: currentColor; + opacity: .4; + transform-origin: left; + animation: itflow-toast-progress 5s linear both; +} + +@keyframes itflow-toast-in { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* visibility flips on the last one percent rather than at the end, because a + discrete property switches at the MIDPOINT of its keyframe interval - put + it only on the 100% stop and the toast would vanish halfway through its own + fade. This is the fallback that matters if the inline remover never runs: + without it an invisible toast keeps sitting over the user menu, eating + clicks. */ +@keyframes itflow-toast-out { + 0% { + opacity: 1; + visibility: visible; + } + 99% { + opacity: 0; + visibility: visible; + } + 100% { + opacity: 0; + visibility: hidden; + } +} + +@keyframes itflow-toast-progress { + from { + transform: scaleX(1); + } + to { + transform: scaleX(0); + } +} + +/* Reduced motion drops the slide and the moving bar but keeps the timed + dismissal - that is a timeout, not an animation the user is watching. */ +@media (prefers-reduced-motion: reduce) { + .itflow-toast { + animation: itflow-toast-out 1s linear 5s both; + } + .itflow-toast-progress { + animation: none; + transform: scaleX(0); + } +} + +/* The JS-raised toasts from itflowToast() still go through bootstrap.Toast, + so they keep the slower fade. */ .itflow-toast-js .toast.fade { transition: opacity .5s linear; } diff --git a/includes/inc_alert_feedback.php b/includes/inc_alert_feedback.php index 53df7810f..7b496ca1f 100644 --- a/includes/inc_alert_feedback.php +++ b/includes/inc_alert_feedback.php @@ -3,9 +3,14 @@ /** * Flash alert rendering. * - * Renders the session flash message as a Bootstrap 5 toast. Bootstrap's Toast - * component ships in bootstrap.bundle.min.js, which is already loaded, so this - * replaces toastr (and its jQuery dependency) without adding a library. + * Renders the session flash message as a toast, replacing toastr and its jQuery + * dependency without adding a library. It borrows Bootstrap's .toast styling + * but NOT its Toast component: the element ships with .show already on it and + * the whole appear/linger/fade cycle is a CSS animation, so the alert is on + * screen at first paint. Driving it with bootstrap.Toast meant waiting for + * DOMContentLoaded, which does not fire until every parser-blocking script has + * run - so it used to arrive well after the page had settled. See the + * .itflow-toast block in css/itflow_custom.css. * * SECURITY: the message is rendered into HTML here, not interpolated into a * JavaScript string literal as it was previously. 429 of the ~653 flashAlert() @@ -24,48 +29,79 @@ if (!empty($_SESSION['alert_message'])) { // functions/sanitize.php. flashAlert() is called with more type names than // Bootstrap has classes ('danger', 'alert' and a typo'd 'errpr' among // them), and an unmapped value used to resolve to nothing at all. - $alert_style = 'text-bg-' . alertStyleClass($alert_type); + $alert_style_class = alertStyleClass($alert_type); + $alert_style = 'text-bg-' . $alert_style_class; // text-bg-info and text-bg-warning are the two Bootstrap pairs with dark text - $alert_dark_text = in_array(alertStyleClass($alert_type), ['warning', 'info'], true); + $alert_dark_text = in_array($alert_style_class, ['warning', 'info'], true); + + // Font Awesome 5 names - the vendored build is 5.15.4, so no fa-circle-* + // aliases. Keyed on alertStyleClass()' output rather than the raw type, so + // 'error', 'errpr' and 'danger' all land on the same icon the same way they + // already land on the same colour. + $alert_icons = [ + 'success' => 'fa-check-circle', + 'info' => 'fa-info-circle', + 'warning' => 'fa-exclamation-triangle', + 'danger' => 'fa-times-circle', + 'secondary' => 'fa-bell', + ]; + $alert_icon = $alert_icons[$alert_style_class] ?? 'fa-bell'; // Escaping lives in one place now - see alertMessageHtml() in functions/sanitize.php $alert_safe_message = alertMessageHtml($_SESSION['alert_message']); ?> -
+ +
diff --git a/js/app.js b/js/app.js index afc86fbcc..92459ee76 100644 --- a/js/app.js +++ b/js/app.js @@ -804,7 +804,7 @@ function itflowToast(message, type) { var container = document.querySelector('.toast-container.itflow-toast-js'); if (!container) { container = document.createElement('div'); - container.className = 'toast-container itflow-toast-js position-fixed top-0 start-50 translate-middle-x p-3'; + container.className = 'toast-container itflow-toast-js position-fixed top-0 end-0 p-3'; container.style.zIndex = '1090'; document.body.appendChild(container); }