Source of file DatatablesExtension.php

Size: 9,197 Bytes - Last Modified: 2023-11-16T22:56:02+01:00

/home/websites/teicee/packagist/site/phpdoc/conf/../vendor/teicee/twig-bundle/src/Extension/DatatablesExtension.php

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
<?php
namespace TIC\TwigBundle\Extension;

use TIC\TwigBundle\Base\TICTwigExtension as BaseExtension;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\HttpFoundation\UrlHelper;
use Twig\TwigFunction;

/**
 * Filtres et fonctions twig pour générer des listes avec DataTables.js
 * https://symfony.com/doc/current/templating/twig_extension.html
 */
class DatatablesExtension extends BaseExtension
{

	public function getFunctions(): array
	{
		return [
			new TwigFunction('dtFilters',    [$this, 'dtFiltersFunction'],      ['is_safe' => ['html']] ),
			new TwigFunction('dtGenerate',   [$this, 'dtGenerateFunction'],     ['is_safe' => ['html']] ),
		];
	}

	public function __construct(UrlHelper $urlHelper, TranslatorInterface $translator = null)
	{
		$this->urlHelper  = $urlHelper;
		$this->translator = $translator;
	}


	/**
	 * Génération du code HTML du conteneur des filtres de recherches (créés ensuite dynamiquement en JS).
	 *
	 * Classes possibles pour activer les filtres (à définir dans les headers) :
	 *   sel-filter, lsel-filter, csel-filter, isel-filter,
	 *   ltxt-filter, ctxt-filter, bool-filter,
	 *   min-filter, max-filter
	 *
	 * @param   string  $id
	 * @param   bool    $noreset
	 * @return  string
	 */
	public function dtFiltersFunction(string $id = null, bool $noreset = false): string
	{
		if (empty($id)) $id = "itemlist"; // TODO: html attr escape
		
		$buttons = '';
		if (empty($noreset)) {
			// réinitialisation (suppression) des filtres
			$html = '<button id="%s-filters-reset" class="btn btn-secondary btn-sm dt-filters-reset" title="%s" type="button">%s</button>';
			$buttons.= \sprintf($html, $id, $this->trans('btn.unfilter', true), $this->getIcon('bi-eraser-fill'));
			/*
			$('#{{ id }}-filters-reset').click( function(){
				$('#{{ id }}-filters select option:selected').prop("selected", false).closest('select').change();
				$('#{{ id }}-filters input[type="text"]').val("").change();
				$('#{{ id }}_filter input[type="search"]').val("").trigger('search');
			});
			*/
		}
		
		$html = '<div id="%s-filters" class="form-group d-none dt-filters"><label>%s</label>&nbsp;:%s</div>';
		return \sprintf($html, $id, \htmlspecialchars($this->trans('widget.datatable.filters')), $buttons);
	}


	/**
	 * Génération du code JavaScript pour initialiser DataTables sur une table HTML.
	 *
	 * Liste des options disponibles :
	 *   - id         (default 'itemlist') : Valeur de l'attribut 'id' de l'objet HTML à transformer
	 *   - orders     (default 1)     : Numéro(s) des colonnes pour le tri (sens inversé si négatif) ex: "-1,2"
	 *   - persist    (default false) :
	 *   - filters    (default false) :
	 *   - search     (default false) : Si faux mais option "filters" vrai, vrai forcé mais avec input masqué
	 *   - paging     (default false) :
	 *   - info       (default false) :
	 *   - buttons    (default false) : Si true affichage de la zone avec la liste définie par défaut, si faux liste vide
	 *   - responsive (default false) :
	 *   - select     (default false) : ["info"=>false] sinon ["style"=>'multi'] ou ["style"=>'os',"selector"=>'td:first-child']
	 *   - ajax       (default false) : URL
	 *   # phPrefix   (default '– ')  : Préfixe du libellé (placeholder) pour génération des filtres (ex '• ')
	 *   # selOpts    (default [])    : Spécification des choix à utiliser pour les filtres select
	 *   # defaults   (default [])    : Valeurs de filtre par défaut pour les colonnes (dans l'ordre)
	 *   # fnPost     (default false) : Nom d'une fonction JS à exécuter en fin d'initialisation
	 *   - options    (default [])    : Hachage d'autres options JS à passer dans la configuration de DataTables
	 *
	 * @param   array   $options    Liste d'options pour paramétrer la construction
	 * @return  string              Code JS d'initialisation de DataTables sur l'objet HTML spécifié
	 */
	public function dtGenerateFunction(array $options = []): string
	{
		// application des options par défaut de la fonction Twig
		$options+= array(
			'id'         => "itemlist",
			'orders'     => "1",          // deviendra : [ [0,"asc"] ]
			'persist'    => false,
			'filters'    => false,
			'search'     => false,
			'paging'     => false,
			'info'       => false,
			'buttons'    => false,
			'responsive' => false,
			'select'     => false,
			'ajax'       => false,
#			'phPrefix'   => "– ",
#			'selOpts'    => [],
#			'defaults'   => [],
#			'fnPost'     => false,
			'options'    => [],
		);
		
		// liste des boutons : array ou string avec virgules, true => défaut, false => aucun (liste vide)
		if (\is_string($options['buttons'])) $options['buttons'] = \explode(",", \str_replace(' ', '', $options['buttons']));
		if (empty($options['buttons'])) $options['buttons'] = [];

		// génération des options pour le constructeur JS de DataTable
		$jsopts = array(
			'order'       => $this->makeOrders( $options['orders'] ),
			'applyFilter' => (bool)$options['filters'],
			'stateSave'   => (bool)$options['persist'],
			'searching'   => (bool)$options['filters'] || (bool)$options['search'],
			'paging'      => (bool)$options['paging'],
			'info'        => (bool)$options['info'],
			'buttons'     => $options['buttons'],
			'select'      => empty($options['select']) ? ["info" => false] : $options['select'],
			'responsive'  => (bool)$options['responsive'],
			'dom'         => $this->makeLayout($options['search'], $options['paging'], ! empty($options['buttons'])),
			'language'    => [ "url" => $this->findLanguageURL() ],
		);
		
		if ($options['ajax']) $jsopts+= array(
			'ajax'        => [ "type" => "POST", "url" => $options['ajax'] ],
			'serverSide'  => true,
			'processing'  => true,  // utilise "r" dans le layout "dom" pour indiquer le traitement en cours
		);
		foreach ($options['options'] as $key => $val) $jsopts[ $key ] = $val;
		
#		dump($options, $jsopts);
		return \sprintf("var dt_%s = $('#%s').DataTable(%s);", $options['id'], $options['id'], \json_encode($jsopts));
	}


	/**
	 * Construction de l'option "orders" de DataTable à partir d'un (ou plusieurs) entier(s).
	 *
	 * Exemples :
	 *   "-1"    => [ [0,"desc"] ]
	 *   [-1,2]  => [ [0,"desc"], [1,"asc"] ]
	 *   "1,-3"  => [ [0,"asc"], [2,"desc"] ]
	 *
	 * @param  mixed    $orders     Numéro(s) des colonnes, négatif pour tri inverse (liste ou chaine avec virgule)
	 * @return array                Liste de critères de tri (couples index de colonne et sens)
	 */
	protected function makeOrders(mixed $orders = 0): array
	{
		$option = [];
		if (! \is_array($orders)) $orders = \explode(',', $orders);
		foreach ($orders as $order) {
			if (! ($order = \intval($order))) continue;
			$option[] = [ \abs($order) - 1, ($order < 0) ? "desc" : "asc" ];
		}
		return $option;
	}

	/**
	 * Construction de l'option "dom" de DataTable selon les fonctionnalités à afficher.
	 * 
	 *   "i"              informations (nb affichés/total)
	 *   "l"    [paging]  sélecteur nb d'éléments par page
	 *   "p"    [paging]  navigation sur les numéros de pages
	 *   "f"    [search]  filtre de recherche global
	 *   "B"    [button]  boutons d'action (exports, print...)
	 *
	 * @param  bool     $search     Option de recherche globale activée ?
	 * @param  bool     $paging     Option de pagination de la liste activée ?
	 * @return string               Chaine indiquant les blocs et classes de mise en page
	 */
	protected function makeLayout(bool $search = false, bool $paging = false, bool $buttons = false): string
	{
		$top = "'row tic-dt-top d-print-none'";
		$lst = "'row tic-dt-lst'<'col-sm-12'tr>";
		$end = "'row tic-dt-end'";
		
		if ($paging) {
			if ($search) {
				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>";
				else           $top.= "<'col-md-6'l>" . "<'col-md-6'f>";
			} else {
				if ($buttons)  $top.= "<'col-md-6'l>" . "<'col-md-6'B>";
				else           $top.= "<'col-sm-12'l>";
			}
		} else {
			if ($search) {
				if ($buttons)  $top.= "<'col-md-6'B>" . "<'col-md-6'f>";
				else           $top.= "<'col-sm-12'f>";
			} else {
				if ($buttons)  $top.= "<'col-sm-12'B>";
				else           $top.= "";
			}
		}
		
		$end.= empty($paging) ? "<'col-sm-12'i>" : "<'col-sm-5'i><'col-sm-7'p>";
		
		return \sprintf('<%s><%s><%s>', $top, $lst, $end);
	}

	/**
	 * Retourne l'URL du fichier JSON des traductions à charger en AJAX.
	 *
	 * @param  string   $locale     Locale des traductions souhaitées (courante par défaut)
	 * @return string               URL absolue vers le fichier JSON de traductions de Datatables
	 */
	protected function findLanguageURL(?string $locale = null): string
	{
		$url = $this->urlHelper->getAbsoluteUrl("/bundles/tictwig/datatables/i18n/");
		
		if ($locale === null) $locale = \Locale::getDefault();
		$locale = \substr(\strtolower($locale), 0, 2);
		
		switch ($locale) {
			case 'fr' : $file = "fr_fr.json"; break;
			case 'de' : $file = "de_de.json"; break;
			case 'it' : $file = "it_it.json"; break;
			case 'es' : $file = "es_es.json"; break;
			case 'en' : $file = "en-en.json"; break;
			default   : $file = "en-en.json";
		}
		return $url . $file;
	}

}