mirror of
https://github.com/itflow-org/itflow
synced 2026-09-22 06:31:15 +00:00
restore nice toast alerts
This commit is contained in:
@@ -496,12 +496,107 @@ a:focus {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Toast fade ---------------------------------------------------------
|
/* --- Flash toast ---------------------------------------------------------
|
||||||
Bootstrap's .fade is a .15s transition, which next to toastr's old 1s
|
Restores the four things toastr did that a plain Bootstrap toast does not:
|
||||||
fadeOut reads as a blink. Bootstrap uses the same .showing class for both
|
sits top-right rather than centred over what you are reading, carries an
|
||||||
directions, so in and out cannot be timed separately without JS - .5s is
|
icon for its type, counts its own timer down, and dismisses on a click
|
||||||
the compromise. Change the duration here to taste. */
|
anywhere instead of only on the small x.
|
||||||
#itflowFlashToast.toast.fade,
|
|
||||||
|
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 {
|
.itflow-toast-js .toast.fade {
|
||||||
transition: opacity .5s linear;
|
transition: opacity .5s linear;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,14 @@
|
|||||||
/**
|
/**
|
||||||
* Flash alert rendering.
|
* Flash alert rendering.
|
||||||
*
|
*
|
||||||
* Renders the session flash message as a Bootstrap 5 toast. Bootstrap's Toast
|
* Renders the session flash message as a toast, replacing toastr and its jQuery
|
||||||
* component ships in bootstrap.bundle.min.js, which is already loaded, so this
|
* dependency without adding a library. It borrows Bootstrap's .toast styling
|
||||||
* replaces toastr (and its jQuery dependency) without adding a library.
|
* 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
|
* SECURITY: the message is rendered into HTML here, not interpolated into a
|
||||||
* JavaScript string literal as it was previously. 429 of the ~653 flashAlert()
|
* 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
|
// functions/sanitize.php. flashAlert() is called with more type names than
|
||||||
// Bootstrap has classes ('danger', 'alert' and a typo'd 'errpr' among
|
// Bootstrap has classes ('danger', 'alert' and a typo'd 'errpr' among
|
||||||
// them), and an unmapped value used to resolve to nothing at all.
|
// 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
|
// 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
|
// Escaping lives in one place now - see alertMessageHtml() in functions/sanitize.php
|
||||||
$alert_safe_message = alertMessageHtml($_SESSION['alert_message']);
|
$alert_safe_message = alertMessageHtml($_SESSION['alert_message']);
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="toast-container position-fixed top-0 start-50 translate-middle-x p-3" style="z-index: 1090;">
|
<?php /* `show` is on the element from the server, and `fade` is gone: the
|
||||||
|
appearance is a CSS animation now, so the alert is on screen at
|
||||||
|
first paint instead of waiting for bootstrap.Toast. The old version
|
||||||
|
called it on DOMContentLoaded, which does not fire until every
|
||||||
|
parser-blocking script in the document has run - most of a megabyte
|
||||||
|
on a page with tinymce - so the alert turned up after you had
|
||||||
|
already started reading. toastr never had that problem because it
|
||||||
|
loaded in the header. */ ?>
|
||||||
|
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1090;">
|
||||||
<div id="itflowFlashToast"
|
<div id="itflowFlashToast"
|
||||||
class="toast fade align-items-center <?= $alert_style ?> border-0"
|
class="toast show itflow-toast align-items-center <?= $alert_style ?> border-0"
|
||||||
role="alert" aria-live="assertive" aria-atomic="true">
|
role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
<div class="d-flex">
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-fw <?= $alert_icon ?> fa-lg ms-3"></i>
|
||||||
<div class="toast-body"><?= $alert_safe_message ?></div>
|
<div class="toast-body"><?= $alert_safe_message ?></div>
|
||||||
|
<?php /* No data-bs-dismiss - the click handler below covers the
|
||||||
|
whole toast, and this button sits inside it. */ ?>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn-close <?= $alert_dark_text ? '' : 'btn-close-white' ?> me-2 m-auto"
|
class="btn-close <?= $alert_dark_text ? '' : 'btn-close-white' ?> me-2 m-auto"
|
||||||
data-bs-dismiss="toast" aria-label="Close"></button>
|
aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="itflow-toast-progress"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
// This include runs near the top of the page but bootstrap.bundle
|
// Deliberately depends on nothing. It runs inline, right after the
|
||||||
// loads in the footer, so the toast has to wait for scripts to
|
// markup above, and does not wait for DOMContentLoaded - showing
|
||||||
// finish. toastr did not need this - it was loaded in the header.
|
// and hiding is CSS, so if this never ran the alert would still
|
||||||
function showFlashToast() {
|
// appear and still go away on its own.
|
||||||
var el = document.getElementById('itflowFlashToast');
|
var el = document.getElementById('itflowFlashToast');
|
||||||
if (el && window.bootstrap && bootstrap.Toast) {
|
if (!el) {
|
||||||
bootstrap.Toast.getOrCreateInstance(el, {
|
return;
|
||||||
animation: true,
|
}
|
||||||
autohide: true,
|
|
||||||
delay: 5000
|
// Click anywhere on it to dismiss, the way toastr did. The close
|
||||||
}).show();
|
// button is inside the toast, so this covers it too.
|
||||||
|
el.addEventListener('click', function () {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove on the fade rather than a fixed timer, so hovering to
|
||||||
|
// finish reading a long message keeps the node alive as well as
|
||||||
|
// visible.
|
||||||
|
el.addEventListener('animationend', function (event) {
|
||||||
|
if (event.animationName === 'itflow-toast-out') {
|
||||||
|
el.remove();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', showFlashToast);
|
|
||||||
} else {
|
|
||||||
showFlashToast();
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -804,7 +804,7 @@ function itflowToast(message, type) {
|
|||||||
var container = document.querySelector('.toast-container.itflow-toast-js');
|
var container = document.querySelector('.toast-container.itflow-toast-js');
|
||||||
if (!container) {
|
if (!container) {
|
||||||
container = document.createElement('div');
|
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';
|
container.style.zIndex = '1090';
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user