Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions apps/_scaffold/static/css/no.css
Original file line number Diff line number Diff line change
Expand Up @@ -540,3 +540,52 @@ ul.tags-list li {
ul.tags-list li[data-selected=true] {
opacity: 1.0;
}


flash-alerts {
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
/* Evita que los clics pasen a través del contenedor si está vacío */
pointer-events: none;
}
/* Permite hacer clic en los toasts hijos */
flash-alerts > * {
pointer-events: auto;
}

/* 2. Lógica de animación genérica.
Esto funcionará para CUALQUIER elemento que coloques dentro de flash-alerts. */

/* Estado inicial (oculto) de cualquier notificación */
flash-alerts > * {
opacity: 0;
transform: translateX(100%);
/* Define la transición que se aplicará cuando cambien las propiedades */
transition: all 0.4s ease-in-out;
}

/* Estado final (visible) cuando se añade la clase .toast-show */
flash-alerts > *.toast-show {
opacity: 1;
transform: translateX(0);
}

/* 3. (Opcional) Estilo para el botón de cierre genérico, por si no usas Bulma. */
.toast-close-btn {
float: right;
cursor: pointer;
font-size: 1.5rem;
font-weight: bold;
line-height: 1;
margin-left: 15px;
opacity: 0.5;
}
.toast-close-btn:hover {
opacity: 1;
}
139 changes: 80 additions & 59 deletions apps/_scaffold/static/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,69 +534,90 @@ Q.flash = function (detail) {
// Displays flash messages
Q.handle_flash = function () {
const elem = Q("flash-alerts")[0];
/** @type {(arg0: HTMLElement) => (event: CustomEvent) => void} */
let make_handler;
if ("bootstrap" in window) {
make_handler = function (elem) {
return function (event) {
let node = document.createElement("div");
const color = event.detail.class || "info";
node.innerHTML = `<div
class="toast fade ${color} border-${color} bg-${color}-subtle"
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<div class="toast-header">
<strong class="me-auto">${
event.detail.title || "Alert"
}</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class=" toast-body">
${event.detail.message}
</div>
</div>`;
// @ts-ignore
node = node.firstElementChild;
elem.appendChild(node);
bootstrap.Toast.getOrCreateInstance(node).show();
node.addEventListener("hidden.bs.toast", () => {
node.parentNode.removeChild(node);
});
};
};
} else {
const make_delete_handler = function (node) {
return function () {
node.parentNode.removeChild(node);
};
};

make_handler = function (elem) {
return function (event) {
let node = document.createElement("div");
node.innerHTML = `<div role="alert"><span class="close"></span>${event.detail.message}</div>`;
// @ts-ignore
node = Q('[role="alert"]', node)[0];
node.classList.add(event.detail.class || "info");
elem.appendChild(node);
Q('[role="alert"] .close', node)[0].onclick =
make_delete_handler(node);
};
};
// Si no hay un elemento <flash-alerts> en la página, no hacemos nada.
if (!elem) {
return;
}

if (elem) {
elem.addEventListener("flash", make_handler(elem), false);
/**
*
* @param {FlashDetails} detail
*/
Q.flash = function (detail) {
elem.dispatchEvent(new CustomEvent("flash", { detail: detail }));
const make_custom_toast_handler = function (container) {
return function (event) {
const detail = event.detail;

// Validar que tengamos los datos mínimos
if (!detail.message) {
detail.message = ""
}
if ( !detail.class) {
detail.class = "solid-flash"
}

// 1. Crear el elemento principal y aplicar TODAS las clases del backend.
const toast = document.createElement("div");
toast.className = detail.class; // Clave: Asigna el string completo de clases.

// 2. Crear el botón de cierre basado en una convención (Bulma o genérico)
let closeButton;
if (detail.class.includes('notification')) {
// Si es una notificación de Bulma, usa su estructura.
closeButton = document.createElement('button');
closeButton.className = 'delete';
} else {
// Fallback para un sistema genérico o personalizado.
closeButton = document.createElement('span');
closeButton.className = 'toast-close-btn'; // Una clase genérica para darle estilo
closeButton.innerHTML = '&times;'; // El símbolo 'x'
}

// 3. Ensamblar el toast
toast.appendChild(closeButton);
// Añadir el mensaje como texto para evitar problemas de XSS.
// El espacio inicial es para separarlo del botón.
toast.appendChild(document.createTextNode(" " + detail.message));

// 4. Añadir el toast al contenedor
container.appendChild(toast);

// 5. Lógica para cerrar y eliminar el toast
const closeToast = () => {
// Quitamos la clase de estado para iniciar la animación de salida
toast.classList.remove('toast-show');

// Cuando la transición termine, eliminamos el elemento del DOM
toast.addEventListener('transitionend', () => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, { once: true });
};

// 6. Asignar eventos
closeButton.onclick = closeToast;
setTimeout(closeToast, 5000); // Auto-cierre

// 7. Iniciar la animación de entrada añadiendo la clase de estado
setTimeout(() => {
toast.classList.add('toast-show');
}, 10);
};
if (elem.dataset.alert) Q.flash(Q.eval(elem.dataset.alert));
};

elem.addEventListener("flash", make_custom_toast_handler(elem), false);

Q.flash = function (detail) {
elem.dispatchEvent(new CustomEvent("flash", { detail: detail }));
};

if (elem.dataset.alert) {
try {
const alertDataStr = elem.dataset.alert.replace(/'/g, '"');
const initialAlert = JSON.parse(alertDataStr);
if (initialAlert && initialAlert.message) {
Q.flash(initialAlert);
}
} catch (e) {
console.error("Failed to parse initial flash data:", elem.dataset.alert, e);
}
}
};

Expand Down
92 changes: 84 additions & 8 deletions docs/chapter-06.rst
Original file line number Diff line number Diff line change
Expand Up @@ -338,15 +338,88 @@ The Flash helper handles the server side of them. Here is an example:
@action('index')
@action.uses(flash)
def index():
flash.set("Hello World", _class="info", sanitize=True)
flash.set("Hello World", _class="solid-flash", sanitize=True)
return dict()

and in the template:

Example with Bulma CSS classes for notifications.

.. code:: python
@action('index2')
@action.uses(flash)
def index():
flash.set("Hello World", _class="notification is-success", sanitize=True)
return dict()


To make the flash messages smooth and visually appealing, we need some CSS. This piece of CSS is included in ``no.css`` when using the default template.

.. code:: html

flash-alerts {
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
/* Prevent clicks from passing through the container if it is empty */
pointer-events: none;
}
/* Allows clicking on child toasts */
flash-alerts > * {
pointer-events: auto;
}

/* 2. Generic animation logic.
This will work for ANY element you place inside flash-alerts. */

/* Initial state (hidden) of any notification */
flash-alerts > * {
opacity: 0;
transform: translateX(100%);
/* Defines the transition that will be applied when properties change */
transition: all 0.4s ease-in-out;
}

/* Final state (visible) when adding the .toast-show class */
flash-alerts > *.toast-show {
opacity: 1;
transform: translateX(0);
}

/* 3. (Optional) Style for the generic close button, in case you don't use Bulma. */
.toast-close-btn {
float: right;
cursor: pointer;
font-size: 1.5rem;
font-weight: bold;
line-height: 1;
margin-left: 15px;
opacity: 0.5;
}
.toast-close-btn:hover {
opacity: 1;

}
//Default backround
.solid-flash {
background-color: #d1d1d1; /* Light gray */
padding: 5px;
padding-left: 10px;
border-radius: 10px;
}


And in the template:
.. code:: html

<flash-alerts class="padded" data-alert="[[=globals().get('flash','')]]"></flash-alerts>



By setting the value of the message in the flash helper, a flash
variable is returned by the action and this triggers the JS in the
template to inject the message in the ``py4web-flash`` DIV which you
Expand All @@ -363,14 +436,17 @@ The client can also set/add flash messages by calling:

::

Q.flash({'message': 'hello world', 'class': 'info'});
Q.flash({'message': 'hello world', 'class': 'solid-flash'});


Remember to include ``utils.js`` in your ``layout.html``. This file is located in every new app in /static/js/utils.js, contains the logic to properly show and manage flash messages.

Py4web defaults to an alert class called ``solid-flash`` which is the default. Yet, there is nothing in py4web
that hardcodes those names. You can use your own class names, or use any class you have available.



py4web defaults to an alert class called ``info`` and most CSS
frameworks define classes for alerts called ``success``, ``error``,
``warning``, ``default``, and ``info``. Yet, there is nothing in py4web
that hardcodes those names. You can use your own class names.

You can see the basic usage of flash messages in the **examples** app.

The Session fixture
-------------------
Expand Down
Loading