Skip to content
Extraits de code Groupes Projets
Valider e84e621a rédigé par placido's avatar placido
Parcourir les fichiers

fix: peaufiner les styles embarqués par défaut pour éviter les sautillements...

fix: peaufiner les styles embarqués par défaut pour éviter les sautillements visuels durant le `.loading`

On fait en sorte d'embarquer les styles CSS nécessaires au bon fonctionnement ajax.

`spipConfig.css.ajax` contient les règles en question.
En partie publique, on récupère ainsi automatiquement les styles d'un fichier `css/ajax.css` s'il existe, ou à défaut celle du privé `themes/spip/ajax.css`

Ainsi, une personne qui souhaite réaliser un thème complet en redéfinissant les effets d'animations (ref: spip#4641 ) peut désormais le faire assez simplement:
- soit en rédigeant les règles dans un fichier `css/ajax.css`
- soit en l'embarquant dans son fichier CSS global, et vidant la variable `spipConfig.css.ajax` avant le lancement de `ajaxbloc()` (cf initjs)

Dans l'espace privé `spipConfig.css.ajax` reste vide et ne fait rien pour ne pas doublonner les styles.

La feuille de style du privé `ajax.css` est completée avec les effets d'animations `animateAppend`, `animateLoading` et `positionner`.

On précise quelques variantes d'affichage pour `.image_loading` (qui reçoit le loader.svg).
Une variable `--loader-inset` décline différentes compositions (en `display:absolute` pour limiter les effets de sautillements)
suivant sa position dans le DOM, et la direction de langue de la page `html[dir="rtl"]`

feat: `addCSS()` simplifie l'écriture de règle CSS dans une balise `<style>` du `<head>`.
parent f05c983b
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
<B_initjs> <B_initjs>
import { default as spip } from "config.js"; import { default as spip } from "config.js";
import { import {
addCSS,
animateAppend, animateAppend,
animateLoading, animateLoading,
animateRemove, animateRemove,
......
...@@ -6,5 +6,6 @@ function cibler_ajaxbloc(_root) { ...@@ -6,5 +6,6 @@ function cibler_ajaxbloc(_root) {
} }
ajaxbloc(root); ajaxbloc(root);
} }
addCSS(spip.css.ajax,'ajax');
cibler_ajaxbloc(); cibler_ajaxbloc();
onAjaxLoad(cibler_ajaxbloc); onAjaxLoad(cibler_ajaxbloc);
\ No newline at end of file
...@@ -10,5 +10,6 @@ function cibler_ajaxform(_root) { ...@@ -10,5 +10,6 @@ function cibler_ajaxform(_root) {
} }
} }
} }
addCSS(spip.css.ajax,'ajax');
cibler_ajaxform(); cibler_ajaxform();
onAjaxLoad(cibler_ajaxform); onAjaxLoad(cibler_ajaxform);
\ No newline at end of file
import { default as spip } from "config.js"; import { default as spip } from "config.js";
import { animateLoading , endLoading } from "./anim.js"; import { animateLoading, endLoading } from "./anim.js";
import { import {
on_ajax_failed, on_ajax_failed,
on_ajax_loaded, on_ajax_loaded,
...@@ -96,38 +96,36 @@ export function formulaire_dyn_ajax(formParent) { ...@@ -96,38 +96,36 @@ export function formulaire_dyn_ajax(formParent) {
/** /**
* Normalise le markup d'un retour de formulaire en excluant les div.ajax redondantes * Normalise le markup d'un retour de formulaire en excluant les div.ajax redondantes
* *
* @param {HTMLElement} formParent <div.spip_formulaire> * @param {HTMLElement} formParent <div.spip_formulaire>
*/ */
function formulaire_fix_markup_post_submit(formParent) { function formulaire_fix_markup_post_submit(formParent) {
if (!formParent) return; if (!formParent) return;
/* // retrait du marqueur qui a deja rempli son role
// retrait hack obsolete
const br = formParent.querySelector("br.bugajaxie"); const br = formParent.querySelector("br.bugajaxie");
if (br) { if (br) {
br.parentNode.removeChild(br); br.parentNode.removeChild(br);
} }
*/
// Si div.ajax est à l'intérieur du formulaire // Si div.ajax est à l'intérieur du formulaire
// Il faut désemboiter sur 2 niveaux // Il faut désemboiter sur 2 niveaux
// div.ajax > .spip-formulaire > form ===> form // div.ajax > .spip-formulaire > form ===> form
if (formParent.firstElementChild?.className === 'ajax') { if (formParent.firstElementChild?.className === "ajax") {
formParent.outerHTML = formParent.firstElementChild.innerHTML formParent.outerHTML = formParent.firstElementChild.innerHTML;
} }
// Si parent/enfant ont les mêmes classes ou si la class ajax est redondante // Si parent/enfant ont les mêmes classes ou si la class ajax est redondante
// on désemboite sur 1 niveau // on désemboite sur 1 niveau
// .spip_formulaire.ajax > spip_formulaire.ajax > form ===> .spip_formulaire.ajax > form // .spip_formulaire.ajax > spip_formulaire.ajax > form ===> .spip_formulaire.ajax > form
if ( if (
formParent?.className === formParent?.firstElementChild.className formParent?.className === formParent?.firstElementChild.className ||
|| (formParent.classList.contains("ajax") && formParent?.firstElementChild.classList.contains("ajax")) (formParent.classList.contains("ajax") &&
formParent?.firstElementChild.classList.contains("ajax"))
) { ) {
formParent.firstElementChild.outerHTML = formParent.firstElementChild.innerHTML; formParent.firstElementChild.outerHTML =
formParent.firstElementChild.innerHTML;
} }
} }
/** /**
* Action standard de soumission d'un formulaire ajaxé (hors bouton_action) * Action standard de soumission d'un formulaire ajaxé (hors bouton_action)
* *
...@@ -137,7 +135,7 @@ export async function formulaire_on_submit(event) { ...@@ -137,7 +135,7 @@ export async function formulaire_on_submit(event) {
event.preventDefault(); event.preventDefault();
const formElem = event.target; const formElem = event.target;
const formParent = formElem.closest(".formulaire_spip"); const formParent = formElem.closest(".formulaire_spip");
const formContainer = formParent.closest('.ajax-form-container'); const formContainer = formParent.closest(".ajax-form-container");
const submitter = event.submitter; const submitter = event.submitter;
const scrollwhensubmit = !formElem.classList.contains("noscroll"); const scrollwhensubmit = !formElem.classList.contains("noscroll");
const scrollwhensubmit_button = !submitter.classList.contains("noscroll"); const scrollwhensubmit_button = !submitter.classList.contains("noscroll");
...@@ -146,7 +144,8 @@ export async function formulaire_on_submit(event) { ...@@ -146,7 +144,8 @@ export async function formulaire_on_submit(event) {
if (submitter.type === "submit" && submitter.name) { if (submitter.type === "submit" && submitter.name) {
params.append(submitter.name, submitter.value); params.append(submitter.name, submitter.value);
} }
submitter.classList.add('--clicked');
log(["formulaire_on_submit", formElem, event, params]); log(["formulaire_on_submit", formElem, event, params]);
animateLoading(formContainer); animateLoading(formContainer);
...@@ -198,7 +197,7 @@ export async function formulaire_on_submit(event) { ...@@ -198,7 +197,7 @@ export async function formulaire_on_submit(event) {
setInnerHTML(formParent, data); setInnerHTML(formParent, data);
formulaire_fix_markup_post_submit(formParent); formulaire_fix_markup_post_submit(formParent);
// chercher une ancre de redirection // chercher une ancre de redirection
const a = formParent.querySelector("a"); const a = formParent.querySelector("a");
...@@ -217,7 +216,7 @@ export async function formulaire_on_submit(event) { ...@@ -217,7 +216,7 @@ export async function formulaire_on_submit(event) {
// mais le relancer car l'image loading a pu disparaitre // mais le relancer car l'image loading a pu disparaitre
animateLoading(formContainer); animateLoading(formContainer);
} else { } else {
endLoading(formContainer,true); endLoading(formContainer, true);
if (scrollwhensubmit && scrollwhensubmit_button) { if (scrollwhensubmit && scrollwhensubmit_button) {
positionner(formParent, true); positionner(formParent, true);
} }
...@@ -234,8 +233,8 @@ export async function formulaire_on_submit(event) { ...@@ -234,8 +233,8 @@ export async function formulaire_on_submit(event) {
} }
/** /**
* Preparer les boutons d'action qui sont techniquement des form minimaux * Preparer les boutons d'action qui sont techniquement
* mais se comportent comme des liens * des form minimaux mais se comportent comme des liens
* *
* @param {HTMLElement} btn_action <form> * @param {HTMLElement} btn_action <form>
* @param {HTMLElement} blocfrag <div.ajaxbloc/> le plus proche * @param {HTMLElement} blocfrag <div.ajaxbloc/> le plus proche
...@@ -244,7 +243,7 @@ export async function formulaire_on_submit(event) { ...@@ -244,7 +243,7 @@ export async function formulaire_on_submit(event) {
export async function formulaire_bouton_action_post( export async function formulaire_bouton_action_post(
btn_action, btn_action,
blocfrag, blocfrag,
ajax_env ajax_env,
) { ) {
if (!btn_action || !ajax_env || !blocfrag) { if (!btn_action || !ajax_env || !blocfrag) {
return; return;
...@@ -272,12 +271,13 @@ export async function formulaire_bouton_action_post( ...@@ -272,12 +271,13 @@ export async function formulaire_bouton_action_post(
btn_action.addEventListener("submit", async (e) => { btn_action.addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
submitter.classList.add('--clicked');
animateLoading(btn_action); animateLoading(btn_action);
const confirm = await new Promise((resolve, reject) => { const confirm = await new Promise((resolve, reject) => {
resolve(formulaire_bouton_action_confirm(submitter)); resolve(formulaire_bouton_action_confirm(submitter));
}); });
if (!confirm) { if (!confirm) {
endLoading(btn_action,true); endLoading(btn_action, true);
return; //action interrompue, dans le <Dialog> ou la fonction préliminaire return; //action interrompue, dans le <Dialog> ou la fonction préliminaire
} }
const action = btn_action.getAttribute("action"); const action = btn_action.getAttribute("action");
......
...@@ -34,7 +34,8 @@ export function positionner(target, force = false, setfocus = true) { ...@@ -34,7 +34,8 @@ export function positionner(target, force = false, setfocus = true) {
} }
/** /**
* animation du bloc cible pour faire patienter * Animation du bloc cible pour faire patienter
* Cherche ou rajoute un loader .image_loading
* *
* @param object target * @param object target
*/ */
...@@ -42,18 +43,19 @@ export function animateLoading(target) { ...@@ -42,18 +43,19 @@ export function animateLoading(target) {
if (!target) { if (!target) {
return; return;
} }
anim_ecrire_style();
target.setAttribute("aria-busy", "true"); target.setAttribute("aria-busy", "true");
target.classList.add("loading"); target.classList.add("loading");
const ajax_image_searching = spip.ajax_image_searching || ""; const ajax_image_searching = spip.ajax_image_searching || "";
const i = target.querySelector(".image_loading"); let i = target.querySelector(".image_loading");
if (i) { if (i) {
i.innerHTML = ajax_image_searching; i.innerHTML = ajax_image_searching;
} else { } else {
const span = document.createElement("span"); i = document.createElement("span");
span.setAttribute("class", "image_loading"); i.setAttribute("class", "image_loading");
span.innerHTML = ajax_image_searching; i.innerHTML = ajax_image_searching;
target.insertBefore(span, target.firstChild); const where =
target.querySelector('[type="submit"].--clicked')?.parentNode ?? target;
where.prepend(i);
} }
return target; return target;
} }
...@@ -84,7 +86,7 @@ export function endLoading(target, hard = false) { ...@@ -84,7 +86,7 @@ export function endLoading(target, hard = false) {
* Animation d'un item que l'on supprime * Animation d'un item que l'on supprime
* *
* @param object target * @param object target
* @param {false|function} callback : (false masque l'élément au lieu d ele supprimer) * @param {false|function} callback : (false masque l'élément au lieu de le supprimer)
* @returns {null|Promise<boolean>} * @returns {null|Promise<boolean>}
*/ */
export function animateRemove(target, callback) { export function animateRemove(target, callback) {
...@@ -92,7 +94,6 @@ export function animateRemove(target, callback) { ...@@ -92,7 +94,6 @@ export function animateRemove(target, callback) {
return; return;
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
anim_ecrire_style();
target.classList.add(...["remove", "--animateRemove"]); target.classList.add(...["remove", "--animateRemove"]);
setTimeout(() => { setTimeout(() => {
switch (typeof callback) { switch (typeof callback) {
...@@ -100,11 +101,11 @@ export function animateRemove(target, callback) { ...@@ -100,11 +101,11 @@ export function animateRemove(target, callback) {
callback.apply(target); callback.apply(target);
break; break;
case false: case false:
target.style.display = 'none'; target.style.display = "none";
target.classList.remove("--animateRemove"); target.classList.remove("--animateRemove");
break; break;
default: default:
target.parentNode.removeChild(target) target.parentNode.removeChild(target);
} }
resolve(true); resolve(true);
}, 950); }, 950);
...@@ -123,9 +124,8 @@ export function animateAppend(target, callback) { ...@@ -123,9 +124,8 @@ export function animateAppend(target, callback) {
return; return;
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
anim_ecrire_style();
target.classList.add(...["append", "--animateAppend"]); target.classList.add(...["append", "--animateAppend"]);
if (callback!==false) { if (callback !== false) {
positionner(target, false); positionner(target, false);
} }
setTimeout(() => { setTimeout(() => {
...@@ -137,33 +137,12 @@ export function animateAppend(target, callback) { ...@@ -137,33 +137,12 @@ export function animateAppend(target, callback) {
break; break;
default: default:
target.classList.remove("--animateAppend"); target.classList.remove("--animateAppend");
setTimeout( () => {target.classList.remove("append")},5000) setTimeout(() => {
target.classList.remove("append");
}, 5000);
} }
resolve(true); resolve(true);
}, 1500); }, 1500);
}); });
} }
/**
* Écrire (une fois) dans le head les règles CSS d'animations
*/
function anim_ecrire_style() {
if (document.head && !document.getElementById("anim_dyn_style")) {
const style = document.createElement("style");
style.setAttribute("id", "anim_dyn_style");
//positionner
style.innerHTML = `[name="ajax_ancre"] { position:absolute; visibility:hidden; scroll-margin: 1em;}`;
style.innerHTML += `.reponse_formulaire { scroll-margin: 1em;}`;
//animateLoading
style.innerHTML += `[aria-busy="true"] > *, [aria-busy].loading > * { ${spip.css.animateLoading} }`;
//animateRemove
style.innerHTML += `
.--animateRemove{pointer-events:none;animation-name:animateRemove;animation-delay:0;animation-duration:1s;animation-fill-mode:both;overflow:hidden}@keyframes animateRemove{from{opacity:1;max-height:100vh;}to{opacity:0;max-height:0;}}
`;
//animateAppend
style.innerHTML += `
.--animateAppend{scroll-margin: 1em;animation-name:animateAppend;animation-delay:0;animation-duration:1.5s;animation-fill-mode:both;overflow:hidden}@keyframes animateAppend{from{opacity:0;max-height:0}to{opacity:1;max-height:100vh}}
`;
document.head.appendChild(style);
}
}
/**
* Écrire une balise <style> dans <head> avec du contenu
*
* @param {String} payload contenu brut CSS à écrire
* @param {String} identifiant sert à personnaliser un [id] sur la balise style
* @param {Boolean} (false) si true, écrase la balise déjà existante
*/
export function addCSS(payload, identifiant = "", force = false) {
const id = identifiant ? `dyn_css_${identifiant}` : "dyn_css";
const old = document.getElementById(id);
if (payload && document.head && (!old || force)) {
if (force) {
old.parentNode.removeChild(old);
}
const style = document.createElement("style");
style.setAttribute("id", id);
style.innerHTML = payload;
document.head.appendChild(style);
}
}
...@@ -4,11 +4,11 @@ export { ...@@ -4,11 +4,11 @@ export {
ajaxbloc, ajaxbloc,
followLink, followLink,
onAjaxLoad, onAjaxLoad,
triggerAjaxLoad triggerAjaxLoad,
} from "./ajaxbloc.js"; } from "./ajaxbloc.js";
export { export {
formulaire_dyn_ajax, formulaire_dyn_ajax,
formulaire_set_container formulaire_set_container,
} from "./ajaxform.js"; } from "./ajaxform.js";
export { export {
animateAppend, animateAppend,
...@@ -20,7 +20,8 @@ export { ...@@ -20,7 +20,8 @@ export {
export { export {
formulaire_activer_verif_auto, formulaire_activer_verif_auto,
formulaire_actualiser_erreurs, formulaire_actualiser_erreurs,
formulaire_verifier formulaire_verifier,
} from "./cvt_verifier.js"; } from "./cvt_verifier.js";
export { log } from "./log.js"; export { log } from "./log.js";
export { parametre_url } from "./url.js"; export { parametre_url } from "./url.js";
\ No newline at end of file export { addCSS } from "./css.js";
import { default as spip } from "config.js"; import { default as spip } from "config.js";
import { addCSS } from "./css.js";
// deux fonctions pour rendre l'ajax compatible Jaws // deux fonctions pour rendre l'ajax compatible Jaws
spip.virtualbuffer_id = "virtualbufferupdate"; spip.virtualbuffer_id = "virtualbufferupdate";
...@@ -7,9 +8,10 @@ export function initReaderBuffer() { ...@@ -7,9 +8,10 @@ export function initReaderBuffer() {
if (document.getElementById(spip.virtualbuffer_id)) { if (document.getElementById(spip.virtualbuffer_id)) {
return; return;
} }
const style = document.createElement("style"); addCSS(
style.innerHTML = `#${spip.virtualbuffer_id} {position:absolute;left:-9999em;}`; `#${spip.virtualbuffer_id} {position:absolute;left:-9999em;}`,
document.head.appendChild(style); "virtualbuffer",
);
const input = document.createElement("input"); const input = document.createElement("input");
input.setAttribute("id", spip.virtualbuffer_id); input.setAttribute("id", spip.virtualbuffer_id);
......
[(#HTTP_HEADER{Content-type:application/javascript[; charset=(#CHARSET)]})] [(#HTTP_HEADER{Content-type:application/javascript[; charset=(#CHARSET)]})]
export default { export default {
espace_prive: [(#REM|test_espace_prive|?{1,0})], espace_prive: [(#REM|test_espace_prive|?{1,0}|set{prive,1})],
load_handlers: [], load_handlers: [],
preloaded_urls: [], preloaded_urls: [],
ajaxbloc_selecteur:'.pagination a, a.ajax', ajaxbloc_selecteur:'.pagination a, a.ajax',
...@@ -11,6 +11,6 @@ export default { ...@@ -11,6 +11,6 @@ export default {
error_on_ajaxform: '<:erreur_technique_ajaxform|html2unicode|addslashes|unicode_to_javascript:>' error_on_ajaxform: '<:erreur_technique_ajaxform|html2unicode|addslashes|unicode_to_javascript:>'
}, },
css: { css: {
animateLoading: 'opacity:.5;transition: opacity linear 150ms; pointer-events: none; cursor:wait;' ajax: [(#GET{prive}|non|?{[@import url('(#CHEMIN{css/ajax.css}|sinon{#CHEMIN{themes/spip/ajax.css}}|url_absolue|protocole_implicite)');],''}|json_encode)],
}, }
}; };
...@@ -4,21 +4,119 @@ ...@@ -4,21 +4,119 @@
*/ */
div.ajaxbloc, div.ajaxbloc,
div.ajax, div.ajax,
div.ajax-form-container { div.ajax-form-container,
[type="submit"] {
position: relative; position: relative;
} }
/**
* Apparence et effets durant le chargement .loading
*/
:where([aria-busy="true"], [aria-busy].loading) > * {
opacity:.5;
pointer-events: none;
transition:
opacity linear .25s,
height .2s linear .2s;
}
[aria-busy="true"]:hover {
cursor:wait
}
/**
* Animations
*/
.--animateAppend {
scroll-margin:1em;
animation-name:animateAppend;
animation-delay:0;
animation-duration:1.5s;
animation-fill-mode:both;
overflow:hidden
}
@keyframes animateAppend{
from {
opacity:0;
max-height:0
} to {
opacity:1;
max-height:100vh
}
}
.--animateRemove {
pointer-events:none;
animation-name:animateRemove;
animation-delay:0;
animation-duration:1s;
animation-fill-mode:both;
overflow:hidden
}
@keyframes animateRemove {
from {
opacity:1;
max-height:100vh;
}
to {
opacity:0;
max-height:0;
}
}
/**
* positionner()
*/
[name="ajax_ancre"] { position:absolute; visibility:hidden; scroll-margin: 1em}
.reponse_formulaire { scroll-margin: 1em}
/* Picto de chargement */ /**
* <span.image_loading> est l'élément parent du loader (svg)
*
* --loader-inset définit la position (absolue) adéquate
* en fonction contexte (élément parent et direction de la langue)
*/
:root {
--loader-inset: .75em .75em auto auto;
}
[dir="rtl"] {
--loader-inset: .75em auto auto .75em;
}
.boutons, [type="submit"] {
--loader-inset: 0;
}
.image_loading { .image_loading {
float: inset-inline-end; padding: .25em;
display: flex;
}
.image_loading > * {
place-content: center;
justify-self: center;
transition : opacity linear .2s
} }
div.ajaxbloc > .image_loading, :where(
div.ajax > .image_loading, div.ajaxbloc,
.formulaire_spip > .image_loading, div.ajax-form-container,
div.ajax-form-container > .image_loading { div.ajax,
.formulaire_spip,
.loading,
.boutons,
[type="submit"]
) > .image_loading {
position: absolute; /* éviter les sautillements */
inset: var(--loader-inset);
}
/* A l'intérieur du bouton : centré et au dessus pour éviter les changements de taille */
[type="submit"] > .image_loading {
display: flex;
justify-content: center;
align-items: center;
}
/* A l'intérieur du la barre .boutons, centré verticalement, côté opposé au bouton principal */
.boutons > .image_loading > * {
position: absolute; position: absolute;
right: 0; inset: calc(-10px + 50%) auto auto .75em
float: none; }
[dir="rtl"] .boutons > .image_loading > * {
inset: calc(-10px + 50%) .75em auto auto
} }
/* Bug IE/Win lol */ /* Bug IE/Win lol */
......
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Veuillez vous inscrire ou vous pour commenter