1jQuery(function(){ 2 saisies_fieldset_pliable(); 3 saisies_fieldset_onglet(); 4 saisies_multi_novalidate(); 5 SaisiesDateJourMoisAnnee.init(); 6 onAjaxLoad(saisies_fieldset_pliable); 7 onAjaxLoad(saisies_fieldset_onglet); 8 onAjaxLoad(saisies_multi_novalidate); 9 onAjaxLoad(saisies_date_jour_mois_annee); 10}); 11 12/** 13 * Rend certains fieldsets pliables 14 * 15 * Il s'agit des fieldsets portant les classes "fieldset.pliable" 16 * Non cumulable avec les fieldsets en onglets. 17 */ 18function saisies_fieldset_pliable(){ 19 // On cherche les groupes de champs pliables 20 jQuery('.fieldset.pliable') 21 .each(function(){ 22 var fieldset = jQuery(this); 23 var groupe = jQuery(this).find('> .editer-groupe'); 24 var legend = jQuery(this).find('> legend'); 25 26 // On éviter de plier un fieldset qui contient des erreurs lors de 27 // l'initialisation. 28 if (fieldset.find('.erreur').length > 0) { 29 fieldset.removeClass('plie'); 30 } 31 32 // S'il est déjà plié on cache le contenu 33 if (fieldset.is('.plie')) 34 groupe.hide(); 35 36 // Ensuite on ajoute une action sur le titre 37 legend 38 .unbind('click') 39 .click( 40 function(){ 41 fieldset.toggleClass('plie'); 42 if (groupe.is(':hidden')) 43 groupe.show(); 44 else 45 groupe.hide(); 46 } 47 ); 48 }); 49}; 50 51/** 52 * Tranforme certains fieldsets en onglets 53 * 54 * - Ceux portant les classes "fieldset.fieldset_onglet". 55 * - Accessible à l'exception de la navigation au clavier. 56 * - Les onglets sont persistants si les fieldsets possèdent un id ou un data-id. 57 * - Non cumulable avec les fieldsets pliables. 58 * 59 * Markup inspiré de https://van11y.net/fr/onglets-accessibles/ 60 */ 61function saisies_fieldset_onglet() { 62 63 // Classes utilisées 64 var classes = { 65 // conteneur général 66 wrapper_horizontal: 'saisies-onglets', 67 wrapper_vertical: 'saisies-onglets saisies-onglets-verticaux', 68 // Menu 69 tablist: 'saisies-menu-onglets', 70 tablist_items: 'saisies-menu-onglets__items', 71 tablist_item: 'saisies-menu-onglets__item', 72 tablist_link: 'saisies-menu-onglets__lien', 73 active: 'actif', 74 error: 'erreur', 75 scrollable: 'scrollable', 76 // contenus (les fieldsets) 77 tabscontents: 'saisies-contenus-onglets', 78 tabcontent: 'saisies-contenu-onglet afficher_si_sans_visuel', // en complément de .fieldset_onglet 79 } 80 var selecteur_fieldset = '.fieldset.fieldset_onglet:not(.pliable)'; 81 var storage = window.sessionStorage; 82 // Générer les onglets 83 var init = function() { 84 $.each(collections_fieldsets(), function(i, $fieldsets) { 85 // On peut avoir des onglets horizontaux dans les onglets verticaux, donc il faut reinitialiser à chaque niveau 86 classes.wrapper = classes.wrapper_horizontal; 87 // A-t-on ne serait-ce qu'un onglet vertical ? Si oui, on considère que tout est en vertical 88 $fieldsets.each(function() { 89 if ($(this).hasClass('fieldset_onglet_vertical')) { 90 classes.wrapper = classes.wrapper_vertical; 91 return false; 92 } 93 }); 94 95 var 96 $conteneur = $('<div class="'+classes.wrapper+'"></div>'), 97 $menu = $('<nav class="'+classes.tablist+'"><ul class="'+classes.tablist_items+'" role="tablist"></ul></nav>'), 98 $contenus = $('<div class="'+classes.tabscontents+'"></div>'), 99 ids_contenus = [], 100 $first_fieldset = $fieldsets.first(), 101 id_menu = null; 102 103 // On insère un conteneur général avant le premier fieldset de la série 104 // puis celui des contenus à l'intérieur. 105 $conteneur.append($contenus).insertBefore($first_fieldset); 106 107 // On parcourt la série de fieldsets pour préparer 108 // les entrées du menu, les interactions et les contenus 109 $fieldsets.each(function() { 110 111 var 112 $contenu = $(this), 113 id_persistant = $contenu.attr('id') || $contenu.attr('data-id'), 114 afficher_si = $contenu.attr('data-afficher_si') || 'true', 115 id_contenu = id_persistant || randomId(), 116 id_onglet = 'onglet-' + id_contenu; 117 118 // On ajoute les attributs nécessaire : id, classe, role et aria 119 // puis on le cache d'office et on le déplace dans le conteneur. 120 121 $contenu 122 .attr('id', id_contenu)
123 .addClass(classes.tabcontent) 124 .attr('role', 'tabpanel') 125 .attr('aria-labelledby', id_onglet) 126 .attr('data-saisies-onglet', true) // pour s'assurer de ne pas passer plusieurs fois 127 .hide().attr('hidden', '') 128 .appendTo($contenus); 129 130 // On récupère le titre et on le cache 131 var titre = $contenu.find('legend').first().hide().text(); 132 133 // On crée l'onglet avec son interaction 134 var $onglet = $('<li class="'+classes.tablist_item+'"><a class="'+classes.tablist_link+'" href="#'+id_contenu+'" id="'+id_onglet+'" aria-controls="'+id_contenu+'" role="tab" aria-selected="false" tabindex="-1">'+titre+'</a></li>'); 135 var autoriser_changement = false; 136 $onglet 137 .attr('data-afficher_si', afficher_si) 138 .click(function() { 139 var sibling_active = $(this).siblings().has('.' + classes.active); 140 sibling_active.each(function() { 141 var onglet_reference = $(this).find('a').attr('href'); 142 autoriser_changement = container_reportValidity(onglet_reference); 143 }); 144 if (!sibling_active.length) { 145 autoriser_changement = true; 146 } 147 if (autoriser_changement) { 148 activer_onglet($(this).find('.'+classes.tablist_link)); 149 } 150 return false; 151 }); 152 153 // Lorsqu'on masque l'onglet avec afficher_si, désactiver l'onglet, puis se rendre si possible au premier onglet 154 $onglet.on('afficher_si_masque_pre', function() { 155 $this_onglet = $(this); 156 $lien = $this_onglet.children('a'); 157 // Si c'est onglet actif, on ferme l'onglet et on cherche le premier onglet dispo 158 if ($lien.attr('aria-selected') == 'true') { 159 $nouvel_onglet = $this_onglet.siblings().not('.afficher_si_masque').first().children('a') 160 desactiver_onglet($lien);//On désactive dans tous les cas l'onglet courant, car cela se trouve il n'y aura pas d'autres onglets à activer 161 activer_onglet($nouvel_onglet); 162 } 163 }); 164 165 166 // Lorsqu'on rend visible l'onglet après un afficher_si, si c'est le seul, y aller directement 167 $onglet.on('afficher_si_visible_post', function() { 168 $voisins = $(this).siblings().not('.afficher_si_masque'); 169 if (!$voisins.length) { 170 activer_onglet($onglet.children('a')); 171 } 172 }); 173 174 175 // On note l'id persistant 176 if (id_persistant) { 177 ids_contenus.push(id_persistant); 178 } 179 180 // S'il y a des erreurs dans cette partie du contenu, on met une classe "erreur" à l'onglet aussi 181 if ($contenu.find('.editer.erreur').length) {
182 $onglet.children('a').addClass(classes.error); 183 } 184 // On ajoute l'onglet au menu 185 $menu.find('.'+classes.tablist_items).append($onglet); 186 }); 187 188 // On insère le menu dans le DOM. 189 // Si *tous* les fieldsets on un id persistant, on peut s'en servir pour celui du menu, 190 // ce qui permet la navigation persistante. 191 // l'id du menu sera utilisé comme clé dans la session, on le simplifie avec un hash. 192 if (ids_contenus.length === $fieldsets.length) { 193 id_menu = 'onglets-'+hashCode(ids_contenus.join('')); 194 $menu.attr('data-id', id_menu); 195 } 196 $menu.prependTo($conteneur); 197 198 // Indiquer si le menu doit être scrollable 199 if ($menu[0].scrollWidth > $menu[0].clientWidth) { 200 $menu.addClass(classes.scrollable); 201 } 202 203 // On active l'onglet par défaut, par ordre de priorité : 204 // - le premier avec une erreur au sein de son groupe d'onglets 205 // - celui en session s'il existe 206 // - le 1er trouvé 207 var $onglet_defaut; 208 if ($('.' + classes.tablist_link + '.' + classes.error).length > 0) { 209 $onglet_defaut = $menu.find('.' + classes.tablist_link + '.' + classes.error).first(); 210 } else if (storage.getItem(id_menu) !== null && $('#'+escapeId(storage.getItem(id_menu))).length > 0) { 211 $onglet_defaut = $('#'+escapeId(storage.getItem(id_menu))); 212 } else { 213 $onglet_defaut = $menu.find('.'+classes.tablist_link).first(); 214 } 215 activer_onglet($onglet_defaut, 0, false); 216 217 }); 218 219 // Si un problème de validation sur un champ, basculer vers le fieldset qui contient l'élèment invalide 220 // Note : on initialise cela en dehors de l'initialisation des onglets 221 // Car un même champ peut être dans plusieurs onglets (onglets imbriqués) 222 // Et on ne veut pas de double écouteur 223 $('.' + classes.tabscontents + ' [name]').on('invalid', function() { 224 // On fait cela lot d'onglets par lot d'onglets 225 $(this).parents('form').find('.' + classes.tabscontents).each(function() { 226 var id_fieldset = '#' + $(this).find('> fieldset:invalid').first().attr('id'); 227 var $onglet = $('a[href="' + id_fieldset + '"]'); 228 activer_onglet($onglet); 229 }); 230 }); 231 } 232 233 // Retourne un tableau de collections de fieldsets 234 // par log de fieldset coté à cote 235 var collections_fieldsets = function() { 236 var collections = []; 237 $(selecteur_fieldset).each( function() { 238 $fieldsets_niveau = $(this).add($(this).nextUntil(':not('+selecteur_fieldset+')')), 239 parsed = $(this).data('saisies-onglet-parsed') || false; 240 if (!parsed) { 241 collections.push($fieldsets_niveau); 242 $fieldsets_niveau.each( function() { 243 $(this).data('saisies-onglet-parsed', true); 244 }); 245 } 246 }); 247 return collections; 248 } 249 250 // Activer un onglet 251 // en commencant par désactiver son voisin 252 // @param object $onglet Ãlément <a> 253 var activer_onglet = function( $onglet, duree = 150, persistant = true ) { 254 $onglet_actuel = $onglet.parent().siblings().has('.' + classes.active).find('.' + classes.tablist_link); 255 desactiver_onglet($onglet_actuel); 256 if ($onglet.length) { 257 var $contenu = $(escapeId($onglet.attr('href')));
258 $onglet.addClass(classes.active).attr('aria-selected', true).removeAttr('tabindex'); 259 $contenu.fadeIn(duree).removeAttr('hidden'); 260 // Mettre en session si on a ce qu'il faut 261 var id_menu = $onglet.parents('.'+classes.tablist).attr('data-id') || null; 262 if (persistant && id_menu) { 263 storage.setItem(id_menu, $onglet.attr('id')); 264 } 265 } 266 } 267 268 // Désactiver un onglet 269 // @param object $onglet Ãlément <a> 270 var desactiver_onglet = function( $onglet, duree = 150 ) { 271 if ($onglet.length) { 272 var $contenu = $(escapeId($onglet.attr('href'))); 273 $onglet.removeClass(classes.active).attr('aria-selected', false).attr('tabindex', -1); 274 $contenu.hide().attr('hidden', ''); 275 } 276 } 277 278 // Ãchapper les ids pour ne pas faire couiner jQuery 279 var escapeId = function ( id ) { 280 id = (id || '').replace(/[^\d\w_\-\#]/gi, '\\$&'); 281 return id; 282 } 283 284 // Retourne un identifiant aléatoire 285 // https://stackoverflow.com/a/8084248 286 var randomId = function (taille = 8) { 287 var random = (Math.random() + 1).toString(36); 288 return random.substring(random.length - taille); 289 } 290 291 // Hash simple et rapide 292 // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0 293 var hashCode = function(s) { 294 for (var i = 0, h = 0; i < s.length; i++) 295 h = Math.imul(31, h) + s.charCodeAt(i) | 0; 296 return Math.abs(h); 297 } 298 299 /* 300 * Vérifier la validité de l'ensemble des champs contenu dans element 301 * @param string selector : un critère de selection 302 * @return bool 303 **/ 304 var container_reportValidity = function(selector) { 305 var retour = true; 306 $(selector).find('[name]').each(function() { 307 let champ = $(this).get(0); 308 309 // Si c'est bien un champ de formulaire, pouvant être vérifié en JS 310 if (typeof champ.reportValidity === 'function') { 311 retour = retour && champ.reportValidity(); //Un seul `false` et tout est `false` 312 } 313 }); 314 315 return retour; 316 } 317 318 // C'est parti 319 init(); 320} 321 322function saisies_date_jour_mois_annee_changer_date(me, datetime) { 323 var champ = jQuery(me); 324 var li = champ.closest('.editer'); 325 var jour = jQuery.trim(li.find('.date_jour').val()); 326 var mois = jQuery.trim(li.find('.date_mois').val()); 327 var annee = jQuery.trim(li.find('.date_annee').val()); 328 var date = jQuery.trim(li.find('.datetime').val()); 329 330 while(jour.length < 2) {jour = '0' + jour;} 331 while(mois.length < 2) {mois = '0' + mois;} 332 while(annee.length < 4) {annee = '0' + annee;} 333 334 if (datetime == 'oui') { 335 heure = date.substring(10); 336 if (!heure || !(heure.length == 9)) { 337 heure = ' 00:00:00'; 338 } 339 date = annee + '-' + mois + '-' + jour + heure; 340 } 341 else { 342 date = annee + '-' + mois + '-' + jour; 343 } 344 li.find('.datetime').attr('value', date); 345} 346 347/** 348 * Gestion des saisies date_jour_mois_annee 349 */ 350class SaisiesDateJourMoisAnnee { 351 constructor(container) { 352 this.container = container; 353 this.dateWrapper = container.querySelector('[data-composant="date"]'); 354 this.dateInput = container.querySelector('[data-composant-input="date"]'); 355 356 this.jourInput = container.querySelector('[data-composant-input="jour"]'); 357 this.moisInput = container.querySelector('[data-composant-input="mois"]'); 358 this.anneeInput = container.querySelector('[data-composant-input="annee"]'); 359 360 this.jourWrapper = container.querySelector('[data-composant="jour"]'); 361 this.moisWrapper = container.querySelector('[data-composant="mois"]'); 362 this.anneeWrapper = container.querySelector('[data-composant="annee"]'); 363 364 this.resetBtn = container.querySelector('.btn_reset'); 365 this.format = this.dateInput.dataset.format || 'datetime'; 366 } 367 368 // Initialiser toutes les saisies 369 static init() { 370 document.querySelectorAll('.saisie_date_jour_mois_annee .composants-date.mode-jour-mois-annee').forEach(container => { 371 if (container.hasAttribute('data-saisies-initialized')) return; 372 373 const instance = new SaisiesDateJourMoisAnnee(container); 374 instance.setup(); 375 instance.sync(); 376 }); 377 } 378 379 // Setup d'une saisie : cacher l'input de date et afficher les autres 380 setup() { 381 this.container.setAttribute('data-saisies-initialized', 'true'); 382 383 // Cacher l'input de date 384 this.dateInput.type = 'hidden'; 385 this.dateWrapper.classList.add('saisies-visually-hidden'); 386 387 // Afficher les autres inputs et le bouton reset 388 [this.jourInput, this.moisInput, this.anneeInput].forEach((input) => { 389 const wrapper = input.closest('[data-composant]'); 390 wrapper.removeAttribute('hidden'); 391 wrapper.style.removeProperty('display'); 392 if (input.dataset.disabled !== 'true') { // certains sont volontairement en disabled 393 input.removeAttribute('disabled'); 394 } 395 }); 396 this.resetBtn.style.removeProperty('display'); 397 this.resetBtn.removeAttribute('hidden'); 398 } 399 400 // Synchronisation d'une saisie : synchroniser la date avec les autres inputs 401 // et gestion du bouton reset 402 sync() { 403 const updateDateValue = () => { 404 const jour = this.jourInput.hasAttribute('disabled') ? '' : this.jourInput.value.trim(); 405 const mois = this.moisInput.hasAttribute('disabled') ? '' : this.moisInput.value.trim(); 406 const annee = this.anneeInput.hasAttribute('disabled') ? '' : this.anneeInput.value.trim(); 407 408 const allEmpty = !jour && !mois && !annee; 409 // const someEmpty = !jour || !mois || !annee; 410
411 if (allEmpty) { 412 this.dateInput.value = ''; 413 return; 414 } 415 416 // Normaliser 417 // Si des éléments sont en disabled, 418 // en fallback on s'aligne sur les valeurs minimales tolérées par PHP. 419 const j = jour ? jour.padStart(2, '0') : '01'; 420 const m = mois ? mois.padStart(2, '0') : '01'; 421 const a = annee ? annee.padStart(4, '0') : '0000'; 422 423 let value = `${a}-${m}-${j}`; 424 if (this.format === 'datetime') { 425 value += ' 00:00:00'; 426 } 427 428 this.dateInput.value = value; 429 this.dateInput.dispatchEvent(new Event('change', { bubbles: true })); 430 }; 431 432 [this.jourInput, this.moisInput, this.anneeInput].forEach(input => { 433 input.addEventListener('input', updateDateValue); 434 }); 435 436 // Clic sur le bouton reset 437 if (this.resetBtn) { 438 this.resetBtn.addEventListener('click', (e) => { 439 e.preventDefault(); 440 [this.jourInput, this.moisInput, this.anneeInput].forEach(input => { 441 if ( 442 !input.hasAttribute('readonly') 443 && !input.hasAttribute('disabled') 444 && input.dataset.readonly !== 'true' 445 && input.dataset.disabled !== 'true' 446 ) { 447 input.value = ''; 448 } 449 }); 450 updateDateValue(); 451 }); 452 } 453 454 // Synchro initiale s'il y a des valeurs par défaut 455 updateDateValue(); 456 } 457} 458// Fonction wrapper pour relancer lors de rechargements ajax 459function saisies_date_jour_mois_annee() { 460 SaisiesDateJourMoisAnnee.init(); 461} 462 463/** Ne pas valider lors des retours arrières sur multiétape **/ 464function saisies_multi_novalidate() { 465 $('[name^="_retour_etape"],[name="aller_a_etape"]').click(function() { 466 $(this).parents('form').attr('novalidate', 'novalidate'); 467 }); 468}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.