1: <?php
2: namespace TIC\TwigBundle\Extension;
3:
4: use TIC\TwigBundle\Base\TICTwigExtension as BaseExtension;
5: use Symfony\Contracts\Translation\TranslatorInterface;
6: use Symfony\Component\HttpFoundation\UrlHelper;
7: use Twig\TwigFunction;
8:
9: /**
10: * Filtres et fonctions twig pour générer des listes avec DataTables.js
11: * https://symfony.com/doc/current/templating/twig_extension.html
12: */
13: class DatatablesExtension extends BaseExtension
14: {
15:
16: public function getFunctions(): array
17: {
18: return [
19: new TwigFunction('dtFilters', [$this, 'dtFiltersFunction'], ['is_safe' => ['html']] ),
20: new TwigFunction('dtGenerate', [$this, 'dtGenerateFunction'], ['is_safe' => ['html']] ),
21: ];
22: }
23:
24: public function __construct(UrlHelper $urlHelper, TranslatorInterface $translator = null)
25: {
26: $this->urlHelper = $urlHelper;
27: $this->translator = $translator;
28: }
29:
30:
31: /**
32: * Génération du code HTML du conteneur des filtres de recherches (créés ensuite dynamiquement en JS).
33: *
34: * Classes possibles pour activer les filtres (à définir dans les headers) :
35: * sel-filter, lsel-filter, csel-filter, isel-filter,
36: * ltxt-filter, ctxt-filter, bool-filter,
37: * min-filter, max-filter
38: *
39: * @param string $id
40: * @param bool $noreset
41: * @return string
42: */
43: public function dtFiltersFunction(string $id = null, bool $noreset = false): string
44: {
45: if (empty($id)) $id = "itemlist"; // TODO: html attr escape
46:
47: $buttons = '';
48: if (empty($noreset)) {
49: // réinitialisation (suppression) des filtres
50: $html = '<button id="%s-filters-reset" class="btn btn-secondary btn-sm dt-filters-reset" title="%s" type="button">%s</button>';
51: $buttons.= \sprintf($html, $id, $this->trans('btn.unfilter', true), $this->getIcon('bi-eraser-fill'));
52: /*
53: $('#{{ id }}-filters-reset').click( function(){
54: $('#{{ id }}-filters select option:selected').prop("selected", false).closest('select').change();
55: $('#{{ id }}-filters input[type="text"]').val("").change();
56: $('#{{ id }}_filter input[type="search"]').val("").trigger('search');
57: });
58: */
59: }
60:
61: $html = '<div id="%s-filters" class="form-group d-none dt-filters"><label>%s</label>&nbsp;:%s</div>';
62: return \sprintf($html, $id, \htmlspecialchars($this->trans('widget.datatable.filters')), $buttons);
63: }
64:
65:
66: /**
67: * Génération du code JavaScript pour initialiser DataTables sur une table HTML.
68: *
69: * Liste des options disponibles :
70: * - id (default 'itemlist') : Valeur de l'attribut 'id' de l'objet HTML à transformer
71: * - orders (default 1) : Numéro(s) des colonnes pour le tri (sens inversé si négatif) ex: "-1,2"
72: * - persist (default false) :
73: * - filters (default false) :
74: * - search (default false) : Si faux mais option "filters" vrai, vrai forcé mais avec input masqué
75: * - paging (default false) :
76: * - info (default false) :
77: * - buttons (default false) : Si true affichage de la zone avec la liste définie par défaut, si faux liste vide
78: * - responsive (default false) :
79: * - select (default false) : ["info"=>false] sinon ["style"=>'multi'] ou ["style"=>'os',"selector"=>'td:first-child']
80: * - ajax (default false) : URL
81: * # phPrefix (default '– ') : Préfixe du libellé (placeholder) pour génération des filtres (ex '• ')
82: * # selOpts (default []) : Spécification des choix à utiliser pour les filtres select
83: * # defaults (default []) : Valeurs de filtre par défaut pour les colonnes (dans l'ordre)
84: * # fnPost (default false) : Nom d'une fonction JS à exécuter en fin d'initialisation
85: * - options (default []) : Hachage d'autres options JS à passer dans la configuration de DataTables
86: *
87: * @param array $options Liste d'options pour paramétrer la construction
88: * @return string Code JS d'initialisation de DataTables sur l'objet HTML spécifié
89: */
90: public function dtGenerateFunction(array $options = []): string
91: {
92: // application des options par défaut de la fonction Twig
93: $options+= array(
94: 'id' => "itemlist",
95: 'orders' => "1", // deviendra : [ [0,"asc"] ]
96: 'persist' => false,
97: 'filters' => false,
98: 'search' => false,
99: 'paging' => false,
100: 'info' => false,
101: 'buttons' => false,
102: 'responsive' => false,
103: 'select' => false,
104: 'ajax' => false,
105: # 'phPrefix' => "– ",
106: # 'selOpts' => [],
107: # 'defaults' => [],
108: # 'fnPost' => false,
109: 'options' => [],
110: );
111:
112: // liste des boutons : array ou string avec virgules, true => défaut, false => aucun (liste vide)
113: if (\is_string($options['buttons'])) $options['buttons'] = \explode(",", \str_replace(' ', '', $options['buttons']));
114: if (empty($options['buttons'])) $options['buttons'] = [];
115:
116: // génération des options pour le constructeur JS de DataTable
117: $jsopts = array(
118: 'order' => $this->makeOrders( $options['orders'] ),
119: 'applyFilter' => (bool)$options['filters'],
120: 'stateSave' => (bool)$options['persist'],
121: 'searching' => (bool)$options['filters'] || (bool)$options['search'],
122: 'paging' => (bool)$options['paging'],
123: 'info' => (bool)$options['info'],
124: 'buttons' => $options['buttons'],
125: 'select' => empty($options['select']) ? ["info" => false] : $options['select'],
126: 'responsive' => (bool)$options['responsive'],
127: 'dom' => $this->makeLayout($options['search'], $options['paging'], ! empty($options['buttons'])),
128: 'language' => [ "url" => $this->findLanguageURL() ],
129: );
130:
131: if ($options['ajax']) $jsopts+= array(
132: 'ajax' => [ "type" => "POST", "url" => $options['ajax'] ],
133: 'serverSide' => true,
134: 'processing' => true, // utilise "r" dans le layout "dom" pour indiquer le traitement en cours
135: );
136: foreach ($options['options'] as $key => $val) $jsopts[ $key ] = $val;
137:
138: # dump($options, $jsopts);
139: return \sprintf("var dt_%s = $('#%s').DataTable(%s);", $options['id'], $options['id'], \json_encode($jsopts));
140: }
141:
142:
143: /**
144: * Construction de l'option "orders" de DataTable à partir d'un (ou plusieurs) entier(s).
145: *
146: * Exemples :
147: * "-1" => [ [0,"desc"] ]
148: * [-1,2] => [ [0,"desc"], [1,"asc"] ]
149: * "1,-3" => [ [0,"asc"], [2,"desc"] ]
150: *
151: * @param mixed $orders Numéro(s) des colonnes, négatif pour tri inverse (liste ou chaine avec virgule)
152: * @return array Liste de critères de tri (couples index de colonne et sens)
153: */
154: protected function makeOrders(mixed $orders = 0): array
155: {
156: $option = [];
157: if (! \is_array($orders)) $orders = \explode(',', $orders);
158: foreach ($orders as $order) {
159: if (! ($order = \intval($order))) continue;
160: $option[] = [ \abs($order) - 1, ($order < 0) ? "desc" : "asc" ];
161: }
162: return $option;
163: }
164:
165: /**
166: * Construction de l'option "dom" de DataTable selon les fonctionnalités à afficher.
167: *
168: * "i" informations (nb affichés/total)
169: * "l" [paging] sélecteur nb d'éléments par page
170: * "p" [paging] navigation sur les numéros de pages
171: * "f" [search] filtre de recherche global
172: * "B" [button] boutons d'action (exports, print...)
173: *
174: * @param bool $search Option de recherche globale activée ?
175: * @param bool $paging Option de pagination de la liste activée ?
176: * @return string Chaine indiquant les blocs et classes de mise en page
177: */
178: protected function makeLayout(bool $search = false, bool $paging = false, bool $buttons = false): string
179: {
180: $top = "'row tic-dt-top d-print-none'";
181: $lst = "'row tic-dt-lst'<'col-sm-12'tr>";
182: $end = "'row tic-dt-end'";
183:
184: if ($paging) {
185: if ($search) {
186: if ($buttons) $top.= "<'col-md-6 col-lg-4'l>" . "<'col-md-6 col-lg-4 order-lg-last'f>" . "<'col-lg-4 text-center'B>";
187: else $top.= "<'col-md-6'l>" . "<'col-md-6'f>";
188: } else {
189: if ($buttons) $top.= "<'col-md-6'l>" . "<'col-md-6'B>";
190: else $top.= "<'col-sm-12'l>";
191: }
192: } else {
193: if ($search) {
194: if ($buttons) $top.= "<'col-md-6'B>" . "<'col-md-6'f>";
195: else $top.= "<'col-sm-12'f>";
196: } else {
197: if ($buttons) $top.= "<'col-sm-12'B>";
198: else $top.= "";
199: }
200: }
201:
202: $end.= empty($paging) ? "<'col-sm-12'i>" : "<'col-sm-5'i><'col-sm-7'p>";
203:
204: return \sprintf('<%s><%s><%s>', $top, $lst, $end);
205: }
206:
207: /**
208: * Retourne l'URL du fichier JSON des traductions à charger en AJAX.
209: *
210: * @param string $locale Locale des traductions souhaitées (courante par défaut)
211: * @return string URL absolue vers le fichier JSON de traductions de Datatables
212: */
213: protected function findLanguageURL(?string $locale = null): string
214: {
215: $url = $this->urlHelper->getAbsoluteUrl("/bundles/tictwig/datatables/i18n/");
216:
217: if ($locale === null) $locale = \Locale::getDefault();
218: $locale = \substr(\strtolower($locale), 0, 2);
219:
220: switch ($locale) {
221: case 'fr' : $file = "fr_fr.json"; break;
222: case 'de' : $file = "de_de.json"; break;
223: case 'it' : $file = "it_it.json"; break;
224: case 'es' : $file = "es_es.json"; break;
225: case 'en' : $file = "en-en.json"; break;
226: default : $file = "en-en.json";
227: }
228: return $url . $file;
229: }
230:
231: }
232: