Browse Source

On sort enfin la v2 de formidable_tablesorter, on va écrire la doc

pull/17/head
Maïeul 2 years ago
parent
commit
0197f466b7
  1. 39
      action/formidable_ts_export.php
  2. 34
      css/formidable_tablesorter.css
  3. 110
      css/formidable_ts.css
  4. 34
      css/formidable_ts_prive_bloc_entete.css
  5. 17
      css/formidable_ts_prive_layout_page.css
  6. 3
      formidable_ts.json.html
  7. 62
      formidable_ts_administrations.php
  8. 23
      formidable_ts_pipelines.php
  9. 23
      inclure/formidable_ts_boutons.html
  10. 27
      inclure/formidable_ts_entete.html
  11. 10
      inclure/formidable_ts_filtres.html
  12. 6
      inclure/formidable_ts_preambule.html
  13. 339
      javascript/formidable_ts.js
  14. 1
      javascript/formidable_ts.json.html
  15. 475
      javascript/formidable_ts.json_fonctions.php
  16. 32
      js/formidable_tablesorter.js
  17. 32
      lang/formidable_ts_fr.php
  18. 2
      lang/paquet-formidable_ts_fr.php
  19. 37
      modeles/formidable_ts.html
  20. 32
      modeles/formidable_ts_fonctions.php
  21. 15
      paquet.xml
  22. 63
      prive/squelettes/contenu/formidable_tablesorter.html
  23. 26
      prive/squelettes/contenu/formidable_tablesorter_fonctions.php
  24. 7
      prive/squelettes/contenu/formidable_ts.html
  25. 2
      prive/squelettes/hierarchie/formidable_ts.html
  26. 13
      prive/squelettes/inclure/formidable_tablesorter_boutons.html
  27. 11
      prive/squelettes/inclure/formidable_tablesorter_entete.html
  28. 0
      prive/squelettes/top/formidable_ts.html

39
action/formidable_ts_export.php

@ -0,0 +1,39 @@
<?php
if (!defined("_ECRIRE_INC_VERSION")) return;
require_once find_in_path('lib/Spout/Autoloader/autoload.php');
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
use Box\Spout\Writer\Common\Creator\Style\StyleBuilder;
use Box\Spout\Common\Entity\Style\Color;
function action_formidable_ts_export() {
$data = json_decode(_request('data'));
$type_export = _request('type_export');
$filename = _request('filename');
$style_entete = (new StyleBuilder())
->setFontBold()
->build();
if ($type_export == 'csv') {
$writer = WriterEntityFactory::createCSVWriter();
} elseif ($type_export == 'ods') {
$writer = WriterEntityFactory::createODSWriter();
} else {
$writer = WriterEntityFactory::createXLSXWriter();
}
$writer->openToBrowser("$filename.$type_export");
//
$i = 0;
foreach ($data as $row => $content) {
$i++;
if ($i == 1 and $type_export != 'csv') {
$writer->addRow(WriterEntityFactory::createRowFromArray($content, $style_entete));
} else {
$writer->addRow(WriterEntityFactory::createRowFromArray($content));
}
}
$writer->close();
}

34
css/formidable_tablesorter.css

@ -1,34 +0,0 @@
.formidable_tablesorter #conteneur, .formidable_tablesorter #contenu{
width:100%;
}
.formidable_tablesorter #navigation {
display:none;
}
.formidable_tablesorter .boutons{
text-align:left;
}
.boutons > * {
display:inline-block;
}
.formidable_tablesorter .boutons label {
box-sizing:border-box;
padding:4px;
}
.tablesorter-ignoreRow th {
border:1px black solid;
text-align:center;
font-size:1.25em;
padding-top:1em;
padding-bottom:1em;
}
#columnSelector label{
display:block;
}
#colSelect1:checked ~ #columnSelector {
display: block;
}
#columnSelector {
display: none;
}

110
css/formidable_ts.css

@ -0,0 +1,110 @@
/** Réglages de la table,
* éviter les debordement,
* inspiré de https://mottie.github.io/tablesorter/docs/example-widget-resizable.html
**/
.formidable_ts-wrapper {
max-width:100%;
width:100%;
overflow-x:scroll;
}
.formidable_ts-wrapper.loading {
cursor:wait;
}
.formidable_ts-wrapper.loading > * {
opacity:0.3;
}
.formidable_ts-wrapper.loading .tablesorter-sticky-wrapper {
opacity:0;
}
.formidable_ts-wrapper.loading::before, .formidable_ts-wrapper.loading .tablesorter-sticky-wrapper::before{
content: "Chargement";
text-align: center;
font-size: 150%;
display: block;
opacity:1;
color:red;
}
.formidable_ts-wrapper.puce_statut {
overflow-x:visible;
}
.formidable_ts-wrapper table {
width: auto;
}
.formidable_ts-wrapper .tablesorter td {
overflow: hidden;
text-overflow: ellipsis;
min-width: 10px;
}
.formidable_ts-wrapper td.puce_statut {
overflow:visible;
}
.formidable_ts-wrapper .tablesorter th {
overflow: hidden;
text-overflow: ellipsis;
min-width: 2em;
}
.tablesorter .fieldset_label {
font-size:0.8em;
font-style:italic;
font-weight:normal;
}
.formidable_ts caption {
text-align:left;
}
.formidable_ts .tablesorter-filter {
width:100%;
min-width:1em;
}
.tablesorter-header .header-title {
cursor:pointer;
}
.tablesorter-header .header-title {
display:inline;
}
.tablesorter-header .header-title::after {
float:right
}
.tablesorter-headerUnSorted .header-title::after {
content: "⬆";
font-family:Noto Color Emoji;
}
.tablesorter-headerAsc .header-title::after {
content: "⬆";
font-family:Noto Color Emoji;
}
.tablesorter-headerDesc .header-title::after {
content: "⬇";
font-family:Noto Color Emoji;
}
.tablesorter-filter-row {
background:white;
}
.tablesorter .move-arrows {
display: flex;
justify-content: space-between;
}
.tablesorter .move-arrows.rightOnly{
justify-content:end;
}
.tablesorter tr > *{
border:1px black solid;
}
.tablesorter-sticky-wrapper caption {
margin: 0;
}
.tablesorter .pager {
font-weight:normal;
}
.tablesorter .pager .disabled, .tablesorter .move-arrows .disabled {
display:none;
}
.tablesorter .pager a.next, .tablesorter .pager a.first, .tablesorter .pager a.last, .tablesorter .pager a.prev {
font-family:Noto Color Emoji;
}
.tablesorter input {
font-size:100%;
}
.filtered {
display:none;
}

34
css/formidable_ts_prive_bloc_entete.css

@ -0,0 +1,34 @@
/** Le bloc d'action et d'aide**/
.formidable_ts-preambule {
display:flex;
justify-content:space-around;
}
.formidable_ts-preambule > *{
width:45%;
max-width:1024px;
height:min-content;
}
.formidable_ts-preambule .aide {
padding:1ex;
}
.formidable_ts-preambule .aide p {
padding:0px;
margin:0px;
}
.formidable_ts-preambule .boutons{
text-align:left;
}
.formidable_ts-preambule .boutons label {
padding:4px;
}
.formidable_ts #columnSelectorButton{
display:inline-block;
}
.formidable_ts #columnSelector {
display: flex;
flex-wrap:wrap;
}
.formidable_ts #columnSelector label {
width:50%;
padding:0px;
}

17
css/formidable_ts_prive_layout_page.css

@ -0,0 +1,17 @@
/** Réglages généraux de la page **/
body {
x-overflow:hidden;
}
.formidable_ts .largeur {
width:auto;
margin-left:5em;
margin-right:5em;
}
.formidable_ts #conteneur, .formidable_ts #contenu{
width:100%;
}
.formidable_ts #navigation {
display:none;
}

3
formidable_ts.json.html

@ -0,0 +1,3 @@
#HTTP_HEADER{Content-type:application/json;charset=#CHARSET}
[(#AUTORISER{voirreponses,#ID_FORMULAIRE}|sinon_interdire_acces)]
<INCLURE{fond=javascript/formidable_ts.json,env} />

62
formidable_ts_administrations.php

@ -0,0 +1,62 @@
<?php
/**
* Fichier gérant l'installation et désinstallation du plugin
*
* @package SPIP\Formidable_ts\Installation
**/
// Sécurité
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
/**
* Installation/maj des config de formidable_ts
*
* @param string $nom_meta_base_version
* Nom de la meta informant de la version du schéma de données du plugin installé dans SPIP
* @param string $version_cible
* Version du schéma de données dans ce plugin (déclaré dans paquet.xml)
* @return void
*/
function formidable_ts_upgrade($nom_meta_base_version, $version_cible) {
// Création des tables
$maj = array();
$maj['create'] = array(
array('formidable_ts_configurer_crayons'),
);
$maj['1'] = array(
array('formidable_ts_configurer_crayons'),
);
include_spip('base/upgrade');
maj_plugin($nom_meta_base_version, $version_cible, $maj);
}
/**
* Configurer crayons pour pouvoir éditer côté privé formidable_ts
**/
function formidable_ts_configurer_crayons() {
include_spip('inc/config');
ecrire_config('crayons/espaceprive', 'on');
$exec_autorise = lire_config('crayons/exec_autorise');
if (!$exec_autorise) {
ecrire_config('crayons/exec_autorise', 'formidable_ts');
} elseif ($exec_autorise !== '*' and strpos($exec_autorise, 'formidable_ts') === false) {
ecrire_config('crayons/exec_autorise', $exec_autorise.',formidable_ts');
}
}
/**
* Désinstallation/suppression des config de formidable_ts
*
* @param string $nom_meta_base_version
* Nom de la meta informant de la version du schéma de données du plugin installé dans SPIP
* @return void
*/
function formidable_ts_vider_tables($nom_meta_base_version) {
// On efface la version enregistrée
effacer_meta($nom_meta_base_version);
}

23
formidable_ts_pipelines.php

@ -14,13 +14,34 @@ function formidable_ts_affiche_gauche($flux) {
include_spip('inc/presentation');
if (isset($args['id_formulaire'])) {
$id_formulaire = $args['id_formulaire'];
if (!sql_countsel('spip_formulaires_reponses', "id_formulaire=$id_formulaire")) {
return $flux;
}
} else {
$id_formulaire = sql_getfetsel('id_formulaire','spip_formulaires_reponses','id_formulaires_reponse='.$args['id_formulaires_reponse']);
}
$boite_fermer = boite_fermer();
$url = parametre_url(generer_url_ecrire('formidable_tablesorter'),'id_formulaire',$id_formulaire);
$url = parametre_url(generer_url_ecrire('formidable_ts'),'id_formulaire',$id_formulaire);
$url = parametre_url($url, 'statut', 'publie');
$lien = icone_horizontale(_T('formidable_ts:tableau_reponses'), $url, 'formulaire-reponses-24');
$flux['data'] = str_replace($boite_fermer,"$lien\n\r$boite_fermer", $flux['data']);
}
return $flux;
}
/**
* Pipeline permettant de régler data-sort-value
* @array $flux 'args' => array(
* 'type'=> 'extra'/'champ',
* 'valeur' => ,
* 'saisie' => ),
* 'data' => ce qu'on veut retourner dans l'attribut
* @return $flux
* Pour l'heure
* - les évènements => date brute
* - les cxextras date date => $date brut
* - les cextras int/float => $valeur brut
**/
function formidable_ts_formidable_ts_sort_value($flux) {
return $flux;
}

23
inclure/formidable_ts_boutons.html

@ -0,0 +1,23 @@
<div id="tablesorter_boutons" class="formulaire_spip">
<div class="boutons">
<div class="print">
<button class="print submit button"><:formidable_ts:imprimer:></button>
</div>
<div class="columnSelectorWrapper">
<input id="colSelect1" type="checkbox" class="invisible">
<label class="submit" id="columnSelectorButton" for="colSelect1"><:formidable_ts:colonnes:></label>
<div id="columnSelector"></div>
</div>
<div class="output">
<button class="submit button output" value="csv"><:formidable_ts:exporter_csv:></button>
<button class="submit button output" value="ods"><:formidable_ts:exporter_ods:></button>
<button class="submit button output" value="xlsx"><:formidable_ts:exporter_xlsx:></button>
</div>
<div class="filtres">
<button class="submit button resetFilter"><:formidable_ts:resetFilter:></button>
</div>
<div class="reset">
<button class="submit button resetAll"><:formidable_ts:resetAll:></button>
</div>
</div>
</div>

27
inclure/formidable_ts_entete.html

@ -0,0 +1,27 @@
<script type="text/javascript">
url_action_formidable_ts_export = "[(#URL_ACTION_AUTEUR{formidable_ts_export,generer=true}|replace{'&amp;',&})]";
filter_filterLabel = '<:formidable_ts:filtrer_colonne:>';
filter_placeholder = '<:formidable_ts:filtre:>';
type_export = 'csv'
filename = 'nom';
checkAll = '<:formidable_ts:checkall:>';
uncheckAll = '<:formidable_ts:uncheckall:>';
resetAllconfirm = "<:formidable_ts:resetallconfirm:>";
pager_ajaxUrl = '#URL_PAGE{formidable_ts.json}&page_ts={page}&size={size}&{sortList:sort}&id_formulaire=#ID_FORMULAIRE&statut=#ENV{statut}';
</script>
[<link rel="stylesheet" href="(#CHEMIN{css/tablesorter.pager.min.css}|timestamp)" />]
[<script type="text/javascript" src="(#CHEMIN{javascript/jquery.tablesorter.min.js}|timestamp)"></script>]
[<script type="text/javascript" src="(#CHEMIN{javascript/jquery.tablesorter.widgets.min.js}|timestamp)"></script>]
[<script type="text/javascript" src="(#CHEMIN{javascript/widget-columnSelector.min.js}|timestamp)"></script>]
[<script type="text/javascript" src="(#CHEMIN{javascript/widget-print.min.js}|timestamp)"></script>]
[<script type="text/javascript" src="(#CHEMIN{javascript/widget-output.min.js}|timestamp)"></script>]
[<script type="text/javascript" src="(#CHEMIN{javascript/widget-pager.min.js}|timestamp)"></script>]
[<link rel="stylesheet" href="(#CHEMIN{css/formidable_ts.css}|timestamp)" />]
[<script type="text/javascript" src="(#CHEMIN{javascript/formidable_ts.js}|timestamp)"></script>]
[(#ENV{css_prive_layout_page}|=={oui}|oui)[<link rel="stylesheet" href="(#CHEMIN{css/formidable_ts.css}|timestamp)" />]]
[(#ENV{css_prive_layout_page}|=={oui}|oui)[<link rel="stylesheet" href="(#CHEMIN{css/formidable_ts_prive_layout_page.css}|timestamp)" />]]
[(#ENV{css_prive_layout_page}|=={oui}|oui)[<link rel="stylesheet" href="(#CHEMIN{css/formidable_ts_prive_layout_page.css}|timestamp)" />]]
[(#ENV{css_prive_bloc_entete}|=={oui}|oui)[<link rel="stylesheet" href="(#CHEMIN{css/formidable_ts_prive_bloc_entete.css}|timestamp)" />]]

10
inclure/formidable_ts_filtres.html

@ -0,0 +1,10 @@
<div class='onglets_simple clearfix'>
<ul>
<li>[(#SELF|parametre_url{statut|id_formulaires_reponse,'XXX'}|replace{XXX,''}|lien_ou_expose{<:formidable:info_reponse_toutes:>,[(#ENV{statut}|non|et{#ENV{id_formulaires_reponse,''}|non})],ajax})]</li>
<BOUCLE_statuts(POUR) {tableau #VAL{spip_formulaires_reponses}|lister_tables_objets_sql|table_valeur{statut_textes_instituer}}>
<BOUCLE_rep_statut(FORMULAIRES_REPONSES){id_formulaire}{statut=#CLE} />
<li>[(#SELF|parametre_url{statut,#CLE}|lien_ou_expose{[(#VALEUR|_T)][ \((#TOTAL_BOUCLE|>{0}|?{#TOTAL_BOUCLE})\)],#ENV{statut}|=={#CLE},ajax})]</li>
<//B_rep_statut>
</BOUCLE_statuts>
</ul>
</div>

6
inclure/formidable_ts_preambule.html

@ -0,0 +1,6 @@
<div class="formidable_ts-preambule">
#INCLURE{fond=inclure/formidable_ts_boutons,total}
<div class="aide formulaire_spip">
<:formidable_ts:aide|propre:>
</div>
</div>

339
javascript/formidable_ts.js

@ -0,0 +1,339 @@
/** Appeler jquery table sorter
* et lier les actions
**/
formidable_ts = '';
$(function() {
/** Migration des vieux storages**/
if (!localStorage.getItem('formidable_ts_version')) {
$([
'columnSelector',
'columnSelector-auto',
'filters',
'pager',
'resizable',
'savesort',
'table-original-css-width',
'table-resized-width',
'order',
]).each(function (key, value) {
value = 'tablesorter-'+value;
storage = localStorage.getItem(value);
if (storage) {
newStorage = storage.replace('formidable_tablesorter', 'formidable_ts');
localStorage.setItem(value, newStorage);
}
});
localStorage.setItem('formidable_ts_version', 1);
}
/** Code principal **/
formidable_ts = $(".tablesorter");
formidable_ts.tablesorter({
selectorSort: '.header-title',
widgets: [
"pager",
"stickyHeaders",
"filter",
"print",
"columnSelector",
"output",
"resizable",
"savesort"
],
widgetOptions: {
columnSelector_container: $('#columnSelector'),
columnSelector_layout : '<label><input type="checkbox"><span>{name}</span></label>',
columnSelector_mediaquery: false,
columnSelector_updated : 'columnUpdate',
print_columns: 's',
print_rows: 'f',
print_extraCSS: 'table{font-size:10pt}',
filter_filterLabel: filter_filterLabel,
filter_placeholder: {search:filter_placeholder},
filter_saveFilters : true,
output_separator: 'array',
output_delivery: 'download',
output_formatContent : function( c, wo, data ) {
return data.content.replace('⬅ ➡', '');
},
stickyHeaders_xScroll : '.formidable_ts-wrapper',
output_callback: function(config, data, url) {
return call_formidable_ts_export(config, data, url);
},
pager_size: 100,//Nombre de lignes par page
// css class names that are added
pager_css: {
container : 'tablesorter-pager', // class added to make included pager.css file work
errorRow : 'tablesorter-errorRow', // error information row (don't include period at beginning); styled in theme file
disabled : 'disabled' // class added to arrows @ extremes (i.e. prev/first arrows "disabled" on first page)
},
pager_selectors: {
container : '.pager', // target the pager markup (wrapper)
first : '.first', // go to first page arrow
prev : '.prev', // previous page arrow
next : '.next', // next page arrow
last : '.last', // go to last page arrow
gotoPage : '.gotoPage', // go to page selector - select dropdown that sets the current page
pageDisplay : '.pagedisplay', // location of where the "output" is displayed
pageSize : '.pagesize' // page size selector - select dropdown that sets the "size" option
},
pager_updateArrows: true,
pager_savePages: true,
pager_ajaxUrl : pager_ajaxUrl,
pager_customAjaxUrl: function(table, url) {
if (moving_flag) {
return;
}
$('.formidable_ts-wrapper').addClass('loading');// Ceserait sans doute mieux ailleurs, mais bon
obj = table.config.widgetOptions.pager_ajaxObject;
if (order = $.tablesorter.storage(formidable_ts, 'tablesorter-order')) {
obj.data['order'] = order;
};
if (filters = $.tablesorter.storage(formidable_ts, 'tablesorter-filters')) {
obj.data['filter'] = filters;
};
return url; // required to return url, but url remains unmodified
},
pager_ajaxObject: {
type: 'POST', // default setting
dataType: 'json',
data: {},
},
resizable_addLastColumn: true
}
}).bind('pagerComplete', function() {
formidable_ts_saveOrder();
formidable_ts_setColumnSelector();
formidable_ts_set_move_arrows();
formidable_ts.trigger('updateHeaders');
$('.tablesorter-stickyHeader th .header-title').each(function() {
$(this).bind('click', function(){
dc = $(this).parents('th').attr('data-column');
$('th[data-column='+dc+'] .header-title',formidable_ts).trigger('click');
});
});
formidable_ts_set_move_arrows_css();
$('.formidable_ts-wrapper').removeClass('loading');
$('.puce_objet', formidable_ts).hover(function() {
$('.formidable_ts-wrapper, .formidable_ts-wrapper td').addClass('puce_statut');
},function() {
$('.formidable_ts-wrapper, .formidable_ts-wrapper td').removeClass('puce_statut');
});
}).bind('columnUpdate', function(){
formidable_ts_set_move_arrows_css();
});
$('.print').click(function() {
formidable_ts.trigger('printTable');
});
$('.output').click(function() {
filename = $('table.tablesorter').data('identifiant');
type_export = $(this).val();
formidable_ts.trigger('outputTable');
return false;
});
$('.resetFilter').click(function() {
formidable_ts.trigger('filterReset');
});
$('.resetAll').click(function() {
if (confirm(resetAllconfirm)) {
formidable_ts_restart();
}
});
formidable_ts_add_check_all_button();
});
/** Réglage du column selector **/
$(function() {
flag_cs = false;
$('#columnSelector').css('display','none');
$('#columnSelectorButton').click(function () {
if (flag_cs) {
flag_cs = false;
$('#columnSelector').hide(1200);
} else {
flag_cs = true;
$('#columnSelector').show(1200);
}
}
);
});
/**
* Après chargement des entetes en ajax, remplir correctement le column selector
**/
function formidable_ts_setColumnSelector() {
labels = $('#columnSelector label:not(#columnSelectorCheckAll) span');
title = $('.header-title', formidable_ts);
i = 0;
title.each(function() {
text = $(this).text();
labels.eq(i).text(text);
i++;
});
}
function formidable_ts_add_check_all_button() {
$('#columnSelector').prepend('<label id="columnSelectorCheckAll"><input type="checkbox" checked="checked" class="checked"><span>'+uncheckAll+'</span></label>');
$('#columnSelectorCheckAll').change(function() {
input = $('input', this);
span = $('span', this);
if ($('input', this).is(':checked')) {
input.attr('class', 'checked');
span.text(uncheckAll);
$('#columnSelector input[data-column]').prop('checked',true).trigger('change');
} else {
input.attr('class');
span.text(checkAll);
$('#columnSelector input[data-column]').prop('checked',false).trigger('change');
}
});
}
/** Fonctions d'export tableur **/
function call_formidable_ts_export(config, data, url) {
var form = $('<form></form>').attr('action', url_action_formidable_ts_export).attr('method', 'post');
form.append($("<input></input>").attr('type', 'hidden').attr('name', 'data').attr('value', data));
form.append($("<input></input>").attr('type', 'hidden').attr('name', 'type_export').attr('value', type_export));
form.append($("<input></input>").attr('type', 'hidden').attr('name', 'filename').attr('value', filename));
form.appendTo('body').submit().remove();
return false;
}
/** Tout ce qui concerne le reordonnancement**/
/**
* Sauver l'ordre initial
**/
function formidable_ts_saveOrder() {
order = [];
$('th div[data-col]',formidable_ts).each(function() {
order.push($(this).attr('data-col'));
});
$.tablesorter.storage(formidable_ts, 'tablesorter-order', order, {});
};
moving_flag = false;
/**
* Masquer la toute première flèche de gauche dans les colonens visibles, et la toute dernière de droite
**/
function formidable_ts_set_move_arrows_css() {
formidable_ts_sticky = $('#'+formidable_ts.attr('id')+'-sticky');
tables = [formidable_ts_sticky, formidable_ts];
for (table of tables) {
$('.move-arrows').removeClass('leftOnly').removeClass('rightOnly');
left = $('th:not(.filtered) .move-arrows .left', table);
left.removeClass('disabled');
left.first().addClass('disabled');
left.first().parent().addClass('rightOnly');
right = $('th:not(.filtered) .move-arrows .right', table);
right.removeClass('disabled');
right.last().addClass('disabled');
right.last().parent().addClass('leftOnly');
}
}
/**
* Régler les évènemens sur les flèches
**/
function formidable_ts_set_move_arrows() {
$('.move-arrows .left, .move-arrows .right').click(function(event) {
col = $(this).parent().attr('data-col');
th = $(this).parents('th');
tr = th.parent();
index = th.index();
if ($(this).hasClass('left')) {
prev = th.prevAll(':not(.filtered)').first();
index_inserting = prev.index();
move = 'left';
} else {
next = th.nextAll(':not(.filtered)').first();
index_inserting = next.index();
move = 'right';
}
// Ajuster le storage des paramètres de colonnes, sauf pour le tri qui se fait plus loins
$([
'columnSelector',
'filters',
'resizable',
'order'
]).each(function(key, storage) {
order = $.tablesorter.storage(formidable_ts, 'tablesorter-' + storage);
if (!order) {
return true;
}
col = order[index];
if (!order) {
return true;
}
array_move(order, index, index_inserting);
$.tablesorter.storage(formidable_ts, 'tablesorter-' + storage, order, {});
});
// Ajuster le storage des tri de colonne
min = Math.min(index, index_inserting);
max = Math.max(index, index_inserting);
sortList = $.tablesorter.storage(formidable_ts, 'tablesorter-savesort');
if (sortList) {
sortList=sortList['sortList'];
$(sortList).each(function(key, value) {
if (value[0] == index) {
value[0] = index_inserting;
} else if (min <= value[0] && value[0] <= max) {
if (move == 'right') {
value[0] = value[0] - 1;
} else {
value[0] = value[0] + 1;
}
}
sortList[key] = value;
});
}
moving_flag = true;
$.tablesorter.storage(formidable_ts, 'tablesorter-savesort',{'sortList':sortList}, {});
$.tablesorter.setFilters(formidable_ts, $.tablesorter.storage(formidable_ts, 'tablesorter-filters'), false );
formidable_ts.trigger('sorton', [sortList]);
columnSelector = $.tablesorter.storage(formidable_ts, 'tablesorter-columnSelector');
$(columnSelector).each( function(key, value) {
$('#columnSelector input[data-column='+key+']').prop('checked',value).trigger('change');
});
$('.formidable_ts-wrapper').addClass('loading');
moving_flag = false;
formidable_ts.trigger('pagerUpdate');
});
};
//https://stackoverflow.com/a/5306832/3206025
function array_move(arr, old_index, new_index) {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) {
arr.push(undefined);
}
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
};
/**
* Réinitialisation de tout, aille, aille, aille
**/
function formidable_ts_restart() {
$([
'columnSelector',
'columnSelector-auto',
'filters',
'pager',
'resizable',
'savesort',
'table-original-css-width',
'table-resized-width',
'order',
]).each(function(key, storage) {
$.tablesorter.storage(formidable_ts, 'tablesorter-'+storage, null);
});
location.reload();
}

1
javascript/formidable_ts.json.html

@ -0,0 +1 @@
[(#ENV**|unserialize|formidable_ts\formidable_ts_json)]

475
javascript/formidable_ts.json_fonctions.php

@ -0,0 +1,475 @@
<?php
namespace formidable_ts;
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
include_spip('inc/sql');
include_spip('inc/saisies');
include_spip('inc/cextras');
include_spip('saisies/afficher_si_php');
/*
* La classe principale
* qui cherche les données en base
* et retourne un tableau json si besoin
*/
class table {
private $size;
private $page;
private $id_formulaire;
private $filter;
private $sort;
private $rows;
private $headers;
private $champs;
private $champs_finaux;
private $cextras;
private $cextras_finaux;
private $statut;
private $totalRows;
private $columns;
/**
* @param array $env
* L'env du squelette
**/
public function __construct($env) {
$this->id_formulaire = sql_quote($env['id_formulaire'] ?? null);
$this->totalRows=0;
// Transformer les filtres en pseudo afficher_si
$this->filter = $env['filter'] ?? array();
if (!$this->filter) {// Peut être ''
$this->filter = array();
}
$this->filter = array_map(function($a) {
$a = strtolower($a);
if (!preg_match('#(=|>|<|MATCH)#', $a)) {
$a = " MATCH '/$a/'";
}
$a = rawurldecode($a);
return $a;
},
$this->filter
);
$this->sort = $env['sort'];
if (!$this->sort){
$this->sort = array();
}
$env['statut'] = $env['statut'] ?? null;
$this->statut = \sql_quote($env['statut'] ? $env['statut'] : '.*');
$champs = \unserialize(sql_getfetsel('saisies', 'spip_formulaires', "id_formulaire=$this->id_formulaire"));
$this->champs = $champs;
$this->champs_finaux = \saisies_lister_finales($this->champs);
if (\test_plugin_actif('cextras')) {
include_spip('cextras_pipelines');
$champs = \champs_extras_objet('spip_formulaires_reponses');
$champs = \champs_extras_autorisation('voir', 'fprmulaires_reponse', $champs, $flux['args']);
$this->cextras = $champs;
$this->cextras_finaux = \saisies_lister_finales($this->cextras);
}
if (!$this->cextras) {
$this->cextras = array();
}
$this->rows = array();
$this->headers = array();
$this->page = $env['page_ts'];
$this->size = $env['size'];
$this->setColumns($env['order'] ?? array());
}
/**
* Définir les colonnes et leur ordre
* @param array $order ordre des colonnes envoyé;
* @return array
**/
function setColumns($order = array()) {
$columns = array();
$columns[] = 'natif-statut';
$columns[] = 'natif-id_formulaires_reponse';
$columns[] = 'natif-date_envoi';
foreach ($this->cextras_finaux as $extra) {
if ($extra['saisie'] == 'explication') {
continue;
}
$columns[] = 'extra-'.$extra['options']['nom'];
}
foreach ($this->champs_finaux as $champ) {
if ($champ['saisie'] == 'explication') {
continue;
}
$columns[] = 'champ-'.$champ['options']['nom'];
}
// Ajouter les nouvelles colonnes à la fin, et soustraire celles qui n'existent plus
$diff = array_diff($columns, $order);
$this->columns = array_merge($order, $diff);
$this->columns = array_intersect($this->columns, $columns);
}
/**
* Peupler headers et rows
**/
public function setData() {
$this->setRows();
$this->setHeaders();
}
/**
* Peupler les headers à partir de la base SQL
**/
public function setHeaders() {
$headers = &$this->headers;
foreach ($this->columns as $column) {
list($type, $nom) = explode('-', $column);
if ($type === 'natif') {
if ($nom === 'statut') {
$headers[] = new header([
'table' => $this,
'type' => 'natif',
'nom' => 'statut',
'value' => '#'
]);
} elseif ($nom === 'id_formulaires_reponse') {
$headers[] = new header([
'table' => $this,
'type' => 'natif',
'nom' => 'id_formulaires_reponse',
'value' => _T('info_numero_abbreviation')
]);
} elseif ($nom === 'date_envoi') {
$headers[] = new header ([
'table' => $this,
'type' => 'natif',
'nom' => 'date_envoi',
'value' => _T('formidable:date_envoi')
]);
}
} else {
if ($type === 'extra') {
$saisies = $this->cextras;
} else {
$saisies = $this->champs;
}
$saisie = \saisies_chercher($saisies, $nom);
$chemin = \saisies_chercher($saisies, $nom, true);
if (count($chemin) > 1) {
$fieldset = $this->saisies[$chemin[0]];
$fieldset = $fieldset['options']['label'];
} else {
$fieldset = '';
}
$label = $saisie['options']['label'] ?? $saisie['options']['label_case'] ?? $saisie['options']['nom'];
if ($fieldset) {
$label .= " <span class='fieldset_label'>($fieldset)</span>";
}
$headers[] = new header ([
'table' => $this,
'type' => $type,
'nom' => $saisie['options']['nom'],
'value' => $label
]);
}
}
}
/**
* Peupler les lignes à partir de la base SQL
**/
private function setRows() {
$res_reponse = \sql_select('*',
'spip_formulaires_reponses',
array(
"id_formulaire= $this->id_formulaire",
"statut REGEXP $this->statut"
),
'',
'DATE DESC'
);
while ($row_reponse = \sql_fetch($res_reponse)) {
$id_formulaires_reponse = $row_reponse['id_formulaires_reponse'];
$row_ts = [];
$this->totalRows++;
foreach ($this->columns as $column) {
list($type, $nom) = explode('-', $column);
if ($type === 'natif') {
if ($nom === 'statut') {
$value = \liens_absolus(\appliquer_filtre($row_reponse['statut'], 'puce_statut', 'formulaires_reponse', $row_reponse['id_formulaires_reponse'], true));
$row_ts[] = new cell([
'table' => $this,
'id_formulaires_reponse' => $id_formulaires_reponse,
'nom' => 'statut',
'value' => $value,
'sort_value' => $row_reponse['statut'],
'filter_value' => $row_reponse['statut'],
'crayons' => false,
'type' => 'natif'
]);
} elseif ($nom === 'id_formulaires_reponse') {
$value = '<a href="'.\generer_url_ecrire('formulaires_reponse', 'id_formulaires_reponse='.$row_reponse['id_formulaires_reponse']).'">'.$row_reponse['id_formulaires_reponse'].'</a>';
$row_ts[] = new cell([
'table' => $this,
'id_formulaires_reponse' => $id_formulaires_reponse,
'nom' => 'id_formulaires_reponse',
'value' => $value,
'sort_value' => $row_reponse['id_formulaires_reponse'],
'filter_value' => $row_reponse['id_formulaires_reponse'],
'crayons' => false,
'type' => 'natif'
]);
} elseif ($nom === 'date_envoi') {
$value = \affdate_heure($row_reponse['date_envoi']);
$row_ts[] = new cell([
'table' => $this,
'id_formulaires_reponse' => $id_formulaires_reponse,
'nom' => 'date_envoi',
'value' => $value,
'sort_value' => \strtotime($row_reponse['date_envoi']),
'filter_value' => $value,
'crayons' => false,
'type' =>'natif'
]);
}
} else {
if ($type === 'extra') {
$champ = \saisies_chercher($this->cextras, $nom);
$crayons = $false;
$nom = $champ['options']['nom'];
if (test_plugin_actif('crayons')) {
$opt = array(
'saisie' => $champ,
'type' => 'formulaires_reponse',
'champ' => $nom,
'table' => table_objet_sql('formulaires_reponse'),
);
if (autoriser('modifierextra', 'formulaires_reponse', $id_formulaires_reponse, '', $opt)) {
$crayons = true;
}
}
if (isset($champ['options']['traitements'])) {
$value = \appliquer_traitement_champ($row_reponse[$nom], $nom, 'formulaires_reponse');
} else {
$value = implode(\calculer_balise_LISTER_VALEURS('formulaires_reponses', $nom, $row_reponse[$nom]), ', ');
}
$row_ts[] = new cell(
[
'table' => $this,
'id_formulaires_reponse' => $id_formulaires_reponse,
'nom' => $nom,
'value' => $value,
'sort_value' => sort_value($value, $champ, 'extra'),
'filter_value' => null,
'crayons' => $crayons,
'type' => 'extra'
]
);
} else { // Réponse de l'internaute
$value = \calculer_voir_reponse($id_formulaires_reponse, $this->id_formulaire, $nom, '', 'valeur_uniquement');
$row_ts[] = new cell(
[
'table' => $this,
'id_formulaires_reponse' => $id_formulaires_reponse,
'nom' => $nom,
'value' => $value,
'sort_value' => sort_value($value, $champ, 'champ'),
'filter_value' => null,
'crayons' => true,
'type' => 'champ'
]
);
}
}
}
// Vérifier si cela passe le filtres:
if ($this->checkFilter($row_ts)) {
$this->rows[] = $row_ts;
}
}
$this->sortRows();
}
/**
* Vérifier si une ligne passe les tests de filtre
* @param array $row
* @return bool
**/
private function checkFilter($row) {
foreach ($this->filter as $col=>$filter) {
$result = saisies_evaluer_afficher_si(
$filter,
array(),
array(),
$row[$col]->filter_value
);
if ($result == false) {
return false;
}
}
return true;
}
/**
* Tri les lignes selon les paramètres passée en option
**/
private function sortRows() {
usort($this->rows, function ($a, $b) {
// Trouver les cellules sur lesquelles trier
foreach ($this->sort as $column => $sort) {
$sort = intval($sort);
$a_sort = $a[$column]->sort_value;
$b_sort = $b[$column]->sort_value;
if ($a_sort == $b_sort) {
continue;
} elseif ($sort) {
return $a_sort < $b_sort;
} else {
return $a_sort > $b_sort;
}
}
return 0;// Si tous les tests échoue à comparaison, c'est que nos deux lignes sont identiques, en ce qui concerne les critères de tri
});
}
/**
* Retourne le json final
* @return string
**/
public function getJson() {
$json = array(
'filteredRows' => \count($this->rows),
'rows' => array_slice($this->rows, $this->page*$this->size, $this->size),
'headers' => $this->headers,
'total' => $this->totalRows,
);
return \json_encode($json);
}
/**
* Compte le nombre d'entetes
* @return int
**/
public function countHeaders() {
return count($this->headers);
}
/**
* Return la propriété, permet d'y accéder mais pas de la modifier
* @param string $prop
**/
public function __get($prop) {
return $this->$prop;
}
}
/**
* Classe représentant une cellule
* @var str|int $id_formulaire
* @var str|int $id_formulaires_reponse
* @var str|int $nom
* @var str $value valeur de la cellule,
* @var str $sort_value valeur de la cellule, pour le tri
* @var bool $crayons est-ce crayonnable?
* @var string $type natif|extra|champ
**/
class cell implements \JsonSerializable{
private $table;
private $id_formulaires_reponse;
private $nom;
private $value;
private $sort_value;
private $filter_value;
private $crayons;
private $type;
public function __construct($param = array()) {
$this->table = $param['table'] ?? false;
$this->id_formulaires_reponse = $param['id_formulaires_reponse'] ?? false;
$this->nom = $param['nom'];
$this->value = $param['value'];
$this->sort_value = $param['sort_value'] ?? \textebrut($this->value);
$this->filter_value = strtolower($param['filter_value'] ?? \textebrut($this->value));
$this->crayons= $param['crayons'] ?? false;
$this->type = $param['type'] ?? 'champ';
}
/**
* Returne la valeur string, avec le span englobant, pour les crayons
**/
public function jsonSerialize() {
if ($this->crayons) {
if ($this->type == 'extra') {
$class = classe_boucle_crayon('formulaires_reponse', $this->nom, $this->id_formulaires_reponse);
} elseif ($this->type == 'champ') {
$class = \calculer_voir_reponse($this->id_formulaires_reponse, $this->table->id_formulaire, $this->nom, '', 'edit');
}
$value = $this->value ? $this->value : '&nbsp;';
return "<div class='$class'>$value</div>";
} else {
return $this->value;
}
}
/**
* Return la propriété, permet d'y accéder mais pas de la modifier
* @param string $prop
**/
public function __get($prop) {
return $this->$prop;
}
}
/**
* Classe implémentant un entete
* C'est comme une cellule, mais ca donne en plus les flèches de déplacement + des datas attributs sur le nom
**/
class header extends cell {
/**
* Returne la valeur string, avec le span englobant, pour les crayons
**/
public function jsonSerialize() {
$data_col = "$this->type-$this->nom";
$arrows = "<div data-col='$data_col' class='move-arrows'><a class='left'>&#x2B05;</a> <a class='right'>&#x27A1;</a></div>";
return "$arrows\n\r<div class='header-title'>$this->value</div>";
}
}
/**
* Depuis le #ENV de l'ajax appelé détermine le json à retourner
* Un simple wrapper pour une classe en fait
* @param array $env
* @return string json
**/
function formidable_ts_json($env) {
$formidable_ts = new table($env);
$formidable_ts->setData();
return $formidable_ts->getJson();
}
/**
* Appelle le pipeline formidable_ts_sort_value
* Pour trouver le type de tri
* @param str|int $valeur valeur brute du champ de formulaire
* @param array $saisie decrit la saisie
* @param string $type='champ' ou bien 'extra'
* @return string valeur du data-sort-attribut
**/
function sort_value($valeur, $saisie, $type = 'champ') {
if ($saisie['saisie'] === 'evenements' and $valeur) {
$data = \sql_getfetsel('date_debut', 'spip_evenements', 'id_evenement='.$flux['args']['valeur']);
}
if ($type === 'extra') {
if (strpos($saisie['options']['sql'], 'INT') !== false
or
strpos($saisie['options']['sql'], 'FLOAT') !== false
or
strpos($saisie['options']['sql'], 'DATE') !== false
) {
$data = $valeur;
}
}
return pipeline ('formidable_ts_sort_value', array(
'args' => array(
'valeur' => $valeur,
'saisie' => $saisie,
'type' => $type
),
'data' => $data
));
}

32
js/formidable_tablesorter.js

@ -1,32 +0,0 @@
$(function() {
$(".tablesorter").tablesorter({
widgets: ["zebra","stickyHeaders", "filter","print", "columnSelector", "output", "resizable"],
widgetOptions: {
columnSelector_container : $('#columnSelector'),
print_columns: 's',
print_rows: 'f',
print_extraCSS: 'table{font-size:10pt}',
filter_saveFilters : true,
output_saveFileName : 'export.csv',
output_encoding : 'data:application/octet-stream;charset=utf8,',
output_wrapQuotes: 'true',
output_delivery: 'download',
resizable_addLastColumn: true
}
}).bind('filterEnd', function(event, config){
total = $(this).find('tbody tr').length;
filtres = $(this).find('tbody tr.filtered').length;
$('#total').text(total-filtres);
}
);
$('.print').click(function() {
$('.tablesorter').trigger('printTable');
});
$('.output').click(function() {
$('.tablesorter').trigger('outputTable');
return false;
});
$('.reset').click(function() {
$('.tablesorter').trigger('filterReset');
});
});

32
lang/formidable_ts_fr.php

@ -1,27 +1,49 @@
<?php
// This is a SPIP language file -- Ceci est un fichier langue de SPIP
// Fichier source, a modifier dans https://git.spip.net/spip-contrib-extensions/formidable_tablesorter.git
// Fichier source, a modifier dans https://git.spip.net/spip-contrib-extensions/formidable_ts.git
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
$GLOBALS[$GLOBALS['idx_lang']] = array(
// A
'aide' => '{{Tri des colonnes}}
Appuyer sur le titre d\'une colonne pour la trier. Utiliser la touche <code></code> pour utiliser les autres colonnes comme tri secondaire.
{{Filtre}}
Dans le champ de filtre, vous pouvez :
-* Saisir une série de caractère
-* Utiliser des comparateurs mathématiques : < ; <= ; > ; >= ; = ; ==
',
// C
'colonnes' => 'Choix des colonnes',
'checkall' => 'Tout cocher',
'uncheckall' => 'Tout décocher',
'cextra' => 'Champ extra',
// E
'exporter' => 'Exporter',
'exporter_ods' => 'Exporter au format ODS (LibreOffice)',
'exporter_csv' => 'Exporter au format CSV',
'exporter_xlsx' => 'Exporter au format XLSX (Excel)',
'filtre' => 'Filtre',
'filtrer_colonne' => 'Filtrer la colonne {{label}}',
// I
'imprimer' => 'Imprimer le tableau',
// N
'nb_lignes' => 'Nombre de lignes affichées :',
//
'data_pager_output_filtered' => 'De {startRow:input} à {endRow} sur {filteredRows} réponses ({totalRows} sans les filtres)',
'data_pager_output' => 'De {startRow:input} à {endRow} sur {filteredRows} réponses',
// R
'reset' => 'Réinitialiser les filtres',
'resetfilter' => 'Réinitialiser les filtres',
'resetall' => 'Réinitialiser tous les paramètres',
'resetallconfirm' => 'Voulez-vous vraiment réinitialiser tous les réglages d\'affichage du tableau ?',
// T
'tableau_reponses' => 'Tableau des réponses'
);

2
lang/paquet-formidable_ts_fr.php

@ -1,6 +1,6 @@
<?php
// This is a SPIP language file -- Ceci est un fichier langue de SPIP
// Fichier source, a modifier dans https://git.spip.net/spip-contrib-extensions/formidable_tablesorter.git
// Fichier source, a modifier dans https://git.spip.net/spip-contrib-extensions/formidable_ts.git
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}

37
modeles/formidable_ts.html

@ -0,0 +1,37 @@
[(#ENV{choix_statut}|=={oui}|?{
#SET{statut,#ENV{statut,'.*'}},
#SET{statut,publie}
})]
<BOUCLE_formulaire(FORMULAIRES){id_formulaire}{tout}{si #AUTORISER{voir, formulairesreponse, #ID_FORMULAIRE}|ou{#ENV{public}|=={oui}}}>
<INCLURE{fond=inclure/formidable_ts_entete, env} />
[(#ENV{choix_statut}|=={oui}|oui)
<INCLURE{fond=inclure/formidable_ts_filtres, env}/>
]
<B_reponses>
<INCLURE{fond=inclure/formidable_ts_preambule} />
<div class="formidable_ts-wrapper loading">
<table data-identifiant="#IDENTIFIANT" id="formidable_ts#ID_FORMULAIRE" class="tablesorter">
<caption>#TITRE</caption>
<thead>
<tr class="tablesorter-ignoreRow">
<th class="pager tablesorter-pager" colspan="[(#ID_FORMULAIRE|formidable_ts_nb_th)]">
<a class="first">&#x23EE;</a> <a class="prev">&#x23EA;</a>
<span class="pagedisplay" data-pager-output="<:formidable_ts:data_pager_output:>" data-pager-output-filtered="<:formidable_ts:data_pager_output_filtered:>"></span>
<a class="next">&#x23E9;</a> <a class="last">&#x23EF;</a>
</td>
</th>
<tr>
[(#ID_FORMULAIRE|formidable_ts_insert_th)]
</tr>
</thead>
</B_entete>
<tbody>
<BOUCLE_reponses(FORMULAIRES_REPONSES){id_formulaire}{statut==#ENV{statut,'.*'}}>
</BOUCLE_reponses>
</tbody>
</table>
</div>
</B_reponses>
</BOUCLE_formulaire>

32
modeles/formidable_ts_fonctions.php

@ -0,0 +1,32 @@
<?php
// Sécurité
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
include_spip('inc/saisies');
include_spip('inc/saisies_lister');
include_spip('javascript/formidable_ts.json_fonctions');
/**
* Indique le nombre de colonnes
* @param int|val $id_formulaire
* @return int
**/
function formidable_ts_nb_th($id_formulaire) {
$object = new formidable_ts\table(array('id_formulaire'=>$id_formulaire));
$object->setHeaders();
$nb = $object->countHeaders();
return $nb;
}
/**
* Calcule le nombre de th à mettre dans le html
* @param int|val $id_formulaire
* @return str
**/
function formidable_ts_insert_th($id_formulaire) {
$nb = formidable_ts_nb_th($id_formulaire);
return str_repeat('<th></th>', $nb);
}

15
paquet.xml

@ -1,10 +1,11 @@
<paquet
prefix="formidable_ts"
categorie="communication"
version="1.2.1"
version="2.0.0"
etat="stable"
compatibilite="[3.1.0;3.2.*]"
documentation="https://contrib.spip.net/5211"
schema="1"
>
<nom>Tablesorter pour Formidable</nom>
@ -12,9 +13,13 @@
<licence lien="http://www.gnu.org/licenses/gpl-3.0.html">GPL 3</licence>
<pipeline nom="affiche_gauche" inclure="formidable_ts_pipelines.php" />
<necessite nom="formidable" compatibilite="[3.46.4;[" />
<necessite nom="tablesorter" compatibilite="[2.0.0;[" />
<necessite nom="saisies" compatibilite="[3.29.0;[" />
<pipeline nom="formidable_ts_sort_value" action="" />
<necessite nom="formidable" compatibilite="[4.5.1;[" />
<necessite nom="spout" compatibilite="[1.2.2;[" />
<necessite nom="tablesorter" compatibilite="[2.1.0;[" />
<necessite nom="saisies" compatibilite="[3.41.0;[" />
<necessite nom="crayons" compatibilite="[2.0.9;[" />
<utilise nom="prive_fluide" compatibilite="[1.2.3;[" />
<utilise nom="cextras" compatibilite="[3.12.6;[" />
<utilise nom="cvtupload" compatibilite="[1.20.3;[" />
</paquet>

63
prive/squelettes/contenu/formidable_tablesorter.html

@ -1,63 +0,0 @@
[(#AUTORISER{voir, formulairesreponse, #ID_FORMULAIRE}|sinon_interdire_acces)]
#INCLURE{fond=prive/squelettes/inclure/formidable_tablesorter_entete}
<BOUCLE_formulaire(FORMULAIRES){id_formulaire}{tout}{si #AUTORISER{voir, formulairesreponse, #ID_FORMULAIRE}}>
<h1>#TITRE</h1>
#SET{saisies,#SAISIES|unserialize}
#SET{saisies_finales,#GET{saisies}|saisies_lister_finales}
#SET{cextras,#VAL{spip_formulaires_reponses}|appliquer_filtre{champs_extras_objet}|sinon{#LISTE}}
<B_reponses>
#INCLURE{fond=prive/squelettes/inclure/formidable_tablesorter_boutons,total=#TOTAL_BOUCLE}
<table id="formidable_tablesorter#ID_FORMULAIRE" class="tablesorter">
<B_entete>
<caption>#TITRE</caption>
<thead>
<tr class="tablesorter-ignoreRow">
<th colspan="2">
</th>
[(#GET{cextras}|count|oui)