Source of file BootstrapExtension.php
Size: 21,018 Bytes - Last Modified: 2023-11-16T22:56:02+01:00
/home/websites/teicee/packagist/site/phpdoc/conf/../vendor/teicee/twig-bundle/src/Extension/BootstrapExtension.php
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 | <?phpnamespace TIC\TwigBundle\Extension; use TIC\TwigBundle\Base\TICTwigExtension as BaseExtension; use TIC\CoreBundle\Util\ConvertHelper; use TIC\CoreBundle\Util\StringHelper; use Symfony\Contracts\Translation\TranslatorInterface; use Twig\TwigFilter; use Twig\TwigFunction; /** * Filtres et fonctions twig pour générer des éléments avec Bootstrap 5. * https://symfony.com/doc/current/templating/twig_extension.html */class BootstrapExtension extends BaseExtension {public function getFilters(): array { return [ new TwigFilter('bsState', [$this, 'bsStateFilter'], ['is_safe' => ['html']] ), new TwigFilter('bsBoolean', [$this, 'bsBooleanFilter'], ['is_safe' => ['html']] ), new TwigFilter('bsEnabled', [$this, 'bsEnabledFilter'], ['is_safe' => ['html']] ), new TwigFilter('bsToggle', [$this, 'bsToggleFilter'], ['is_safe' => ['html']] ), new TwigFilter('bsLabel', [$this, 'bsLabelFilter'], ['is_safe' => ['html']] ), new TwigFilter('bsBadge', [$this, 'bsBadgeFilter'], ['is_safe' => ['html']] ), new TwigFilter('bsCount', [$this, 'bsCountFilter'], ['is_safe' => ['html']] ), new TwigFilter('bsButton', [$this, 'bsButtonFilter'], ['is_safe' => ['html']] ), new TwigFilter('bsAction', [$this, 'bsActionFilter'], ['is_safe' => ['html']] ), new TwigFilter('bsAlert', [$this, 'bsAlertFilter'], ['is_safe' => ['html']] ), # new TwigFilter('bsThumb', [$this, 'bsThumbFilter'], ['is_safe' => ['html']] ),]; } public function getFunctions(): array { return [ new TwigFunction('card_tabs_head', [$this, 'cardTabsHeadFunction'], ['is_safe' => ['html']] ), new TwigFunction('form_tabs_head', null, ['node_class' => 'Symfony\Bridge\Twig\Node\RenderBlockNode', 'is_safe' => ['html']] ), new TwigFunction('form_tabs_body', null, ['node_class' => 'Symfony\Bridge\Twig\Node\RenderBlockNode', 'is_safe' => ['html']] ), ]; } public function __construct(TranslatorInterface $translator = null) { $this->translator = $translator; } /** * Retourne le nom d'un icone correspondant à un état bootstrap (par exemple pour bsAlert). */protected function getStateIcon(string $state): ?string { switch ($state) { case 'primary' : return 'bi-chat-fill'; case 'secondary' : return 'bi-question-circle-fill'; case 'info' : return 'bi-info-fill'; case 'success' : return 'bi-check-square-fill'; case 'warning' : return 'bi-exclamation-circle-fill'; case 'danger' : return 'bi-exclamation-triangle-fill'; case 'dark' : return 'bi-exclamation-square-fill'; case 'light' : return 'bi-chat-left-fill'; } return null; } /** * Détermine un contexte d'état Bootstrap d'après la valeur donnée. * * @param mixed $value Variable à analyser (null, bool, string, numeric, DateTime) * @return string Classe Bootstrap (primary, secondary, info, success, warning, danger, light, dark) */public function bsStateFilter($value): string { if (\is_object($value)) switch (true) { case ($value instanceof \DateTime): $value = $value->format('Ymd') - \date('Ymd'); // numeric (past/now/future) break; case \method_exists($value, '__toString'): $value = (string)$value; break; default: return 'light'; } if (\is_string($value)) switch ($value) { case 'primary': case 'secondary': case 'success': case 'danger': case 'warning': case 'info': case 'light': case 'dark': return $value; case 'ext.boolean.true': $value = true; break; case 'ext.boolean.false': $value = false; break; case 'ext.boolean.null': $value = null; break; // TODO: voir pour conversion en numérique si ctype_digit() ? } if (\is_null($value)) return 'dark'; if (\is_bool($value)) return ($value) ? 'success' : 'danger'; if (\is_numeric($value)) { if ($value > 0) return 'success'; if ($value < 0) return 'danger'; return 'warning'; } return 'info'; } /** * Affichage d'une valeur booléenne (vrai/faux) dans un label Bootstrap (avec traduction et état automatique). * * @param mixed $value Variable à interpréter pour déterminer le booléen correspondant * @param string $default Libellé par défaut à afficher si aucune correspondance booléenne trouvée * @return string Code HTML représentant un label Bootstrap (gros badge carré) */public function bsBooleanFilter($value, string $default='-'): string { $text = self::$strings['null']; try { $bool = $this->getBool($value); if ($bool !== null) $text = self::$strings[($bool) ? 'true' : 'false' ]; } catch (\UnexpectedValueException $e) { $text = $default; } return $this->bsLabelFilter($text, true, true); // state auto et trans activé } /** * Affichage d'une valeur booléenne (actif/inactif) dans un label Bootstrap (avec traduction et état automatique). * * @param mixed $value Variable à interpréter pour déterminer le booléen correspondant * @param string $tok_true Libellé (token de traduction) à afficher si la variable vaut true * @param string $tok_false Libellé (token de traduction) à afficher si la variable vaut false * @return string Code HTML représentant un label Bootstrap (gros badge carré) */public function bsEnabledFilter($value, string $tok_true='ext.state.enabled', string $tok_false='ext.state.disabled'): string { $state = $this->bsStateFilter($this->getBool($value)); switch ($state) { case 'success' : $label = $tok_true; break; case 'danger' : $label = $tok_false; break; default : $label = $value; } return $this->bsLabelFilter($label, $state, true); } /** * Affichage d'une valeur booléenne (actif/inactif) dans un label Bootstrap cliquable (pour changer l'état). * * @param mixed $value Variable à interpréter pour déterminer le booléen correspondant * @param string $route_on Route de l'action du lien si la variable vaut faux (pour activer) * @param string $route_off Route de l'action du lien si la variable vaut vrai (pour désactiver) * @return string Code HTML représentant un label Bootstrap (gros badge carré) */public function bsToggleFilter(mixed $value, string $route_on, string $route_off = null): string { if ($route_off === null) $route_off = $route_on; $vbool = $this->getBool($value); $route = ($vbool) ? $route_off : $route_on; $title = ($vbool) ? "widget.toggle.off" : "widget.toggle.on"; return sprintf('<a href="%s" title="%s">%s</a>', $route, $this->trans($title), $this->bsEnabledFilter($value) ); } /** * Affichage de la valeur dans un label Bootstrap (traduction activable) avec état automatique si non spécifié. * * @param string $value Texte à afficher dans le bloc label * @param mixed $state Classe Bootstrap ('primary', 'info', 'success', 'warning', 'danger'...) * ou boolean true pour détermination automatique à partir du texte (bsStateFilter) * ou sinon (empty) utilisation de la classe par défaut ('dark') * @param bool $trans Recherche d'une traduction pour le token $text (si c'est une chaine) * @param array $attrs Liste d'attributs HTML (en clé/valeur) à ajouter * @return string Code HTML représentant un label Bootstrap (gros badge carré) */public function bsLabelFilter($value, $state=null, bool $trans=false, array $attrs=[]): string { if ($state === true) $state = $this->bsStateFilter($value); return \sprintf('<span class="badge bg-%s"%s>%s</span>', empty($state) ? 'dark' : $state, $this->htmlAttr($attrs), $this->getText($value, $trans, true) ); } /** * Affichage de la valeur dans un badge Bootstrap (traduction activable). * * @param mixed $value Valeur à afficher dans le bloc badge (calcul du nombre d'éléments pour un objet|array) * @param mixed $state Classe Bootstrap ('primary', 'info', 'success', 'warning', 'danger'...) * ou boolean true pour détermination automatique à partir du texte (bsStateFilter) * ou sinon (empty) utilisation de la classe par défaut ('dark') * @param bool $trans Recherche d'une traduction pour le token $value (si c'est une chaine) * @param string $default Valeur par défaut à traiter si $value est null (aucun badge si $default est aussi null) * @return string Code HTML représentant un badge Bootstrap (petit badge arrondi) */public function bsBadgeFilter($value, $state=null, bool $trans=false, ?string $default=''): string { if ($value === null) { if ($default === null) return ''; $value = $default; } if (! \is_scalar($value)) $value = \count($value); if ($state === true) $state = $this->bsStateFilter($value); return \sprintf('<span class="badge rounded-pill bg-%s">%s</span>', empty($state) ? 'dark' : $state, $this->getText($value, $trans, true) ); } /** * Affichage d'un nombre dans un badge Bootstrap (conversion numérique automatique). * * @param mixed $value Valeur à traiter pour obtenir une quantité (calcul du nombre d'éléments pour un objet|array) * @param mixed $state Classe Bootstrap ('primary', 'info', 'success', 'warning', 'danger'...) * ou boolean true pour détermination automatique à partir du nombre (bsStateFilter) * ou sinon (empty) utilisation de la classe par défaut ('dark') * @param string $title Texte facultatif pour ajouter un attribut HTML title (affichage au survol) * @param string $default Code HTML par défaut à retourner si le nombre est 0 (en place du badge) * @return string Code HTML représentant un badge Bootstrap (petit badge arrondi) */public function bsCountFilter($value, $state=null, ?string $title=null, ?string $default=null) { $value = ConvertHelper::counter($value); if ($value === null) return $default; if ($state === true) $state = $this->bsStateFilter($value); return \sprintf('<span class="tic-counter badge rounded-pill bg-%s"%s>%s</span>', empty($state) ? 'dark' : $state, empty($title) ? '' : ' title="'.\htmlspecialchars($title).'"', \number_format($value, 0, ',', ' ') ); } /** * Génération d'un bouton (icone et/ou texte) avec les styles Bootstrap. * * @param string $target URL (pour tag A) ou TYPE (pour tag BUTTON avec 'submit'|'reset'|'button') ou #ID ou .CLASS (tag button) * @param string $icon Nom d'un icone (FontAwesome), par ex: 'ok', 'download', 'envelope', 'ban-circle', 'remove', 'edit', 'list' * @param mixed $label Texte à afficher dans le bouton à côté de l'icone (utilisation de $href si true) * @param string $context Classe Bootstrap pour le style du bouton : 'primary', 'secondary', 'info', 'success', 'warning', 'danger', 'link' * (par défaut 'primary' pour un bouton de type 'submit', sinon 'default') * @param string $size Classe Bootstrap pour indiquer la taille du bouton : 'lg', 'md' (défaut), 'sm', 'xs' * @param array $attrs Liste d'attributs HTML supplémentaires (ex 'class', 'title', 'confirm', 'disabled') * @param bool $trans Si faux, désactivation de la traduction du libellé du bouton (et éventuel message de confirmation) * @return string Code HTML représentant un bouton Bootstrap * * Exemples : * {{ 'submit'|bsButton('save', 'btn.save') }} * {{ path('app_item_show', {'id':item.id})|bsButton('show', 'btn.show') }} * {{ path('app_item_delete', {'id':item.id})|bsButton('remove', 'btn.delete', 'danger', '', {'confirm':true}) }} */public function bsButtonFilter(string $target=null, string $icon=null, $label=null, string $context=null, string $size=null, array $attrs=[], bool $trans=true): string { // gestion de l'option 'confirm' $confirm = false; if (\array_key_exists('confirm', $attrs)) { $confirm = $attrs['confirm']; unset($attrs['confirm']); if (\is_string($confirm) && \strlen($confirm)) $attrs['data-confirm'] = ($trans) ? $this->trans($confirm) : $confirm; } // gestion de l'option 'modal' if (\array_key_exists('modal', $attrs)) { if (! isset($attrs['disabled']) || ! $attrs['disabled']) { $attrs['data-bs-toggle'] = "modal"; $attrs['data-bs-target'] = "#" . $attrs['modal']; } unset($attrs['modal']); } // différents types de boutons selon $target if (\preg_match('/^(submit|reset|button)?$/', $target)) { $tag = 'button'; if ($target !== '') { $attrs['type'] = $target; if ($label === true ) { $label = 'btn.' . $target; $trans = true; } } } elseif (\preg_match('/^([#\.])(.*)$/', $target, $match)) { $tag = 'button'; $attrs['type'] = 'button'; if ($match[1] == '#') $attrs['id'] = $match[2]; else $attrs['class'] = $match[2] . ' ' . $attrs['class']; if ($label === true) { $label = 'btn.' . $match[2]; $trans = true; } } elseif (isset($attrs['disabled']) && $attrs['disabled']) { // si disabled : <button> forcé avec href déporté dans 'data-href' // sans quoi un <a> avec la classe 'disabled' de bootstrap bloque tout event avec 'pointers-event:none', y compris le changement du cursor ! $tag = 'button'; $attrs['type'] = 'button'; $attrs['data-href'] = $target; if ($label === true) { $label = $target; $trans = false; } } elseif ($confirm) { // si click à protéger par une demande de confirmation, href déporté dans 'data-href' // l'URL sera remise uniquement après confirmation positive (évite les risques de court-circuit notamment par un middle-clic) $tag = 'a'; $attrs['role'] = 'button'; $attrs['href'] = '#need_confirmation'; // alternative: 'javascript:null' $attrs['data-href'] = $target; } else { $tag = 'a'; $attrs['role'] = 'button'; $attrs['href'] = $target; if ($label === true) { $label = $target; $trans = false; } if (isset($attrs['disabled'])) unset($attrs['disabled']); } // détermination des classes CSS if (! \strlen($context)) $context = ($target === 'submit') ? "primary" : "secondary"; $class = "btn btn-" . $context; if (! empty($size) && ($size != 'md')) $class.= " btn-" . $size; if (! empty($confirm)) $class.= " btn-confirm"; if (isset($attrs['disabled']) && $attrs['disabled']) $class.= " disabled"; if (\array_key_exists('class', $attrs)) $class.= " " .$attrs['class']; // génération du contenu du bouton (icone & texte) if ($label === null) $label = ''; if ($trans) $label = $this->trans($label); if ($icon) { $content = $this->getIcon($icon); if (\strlen($label)) { $content.= " $label"; $class.= " icon-left"; } } else { $content = $label; } // envoi du tag du bouton $attrs['class'] = $class; return \sprintf('<%1$s %2$s>%3$s</%1$s>', $tag, $this->htmlAttr($attrs), $content); } /** * Génération d'un bouton affichant uniquement un icone (avec libellé en title) avec les styles Bootstrap. * * @param string $target URL (pour tag A) ou TYPE (pour tag BUTTON avec 'submit'|'reset'|'button') ou #ID ou .CLASS (tag button) * @param string $icon Nom d'un icone (FontAwesome), par ex: 'ok', 'download', 'envelope', 'ban-circle', 'remove', 'edit', 'list' * @param string $title Texte à afficher uniquement au survol du bouton (utilisation de $href si true) * @param string $context Classe Bootstrap pour le style du bouton : 'default', 'primary' (défaut), 'info', 'success', 'warning', 'danger', 'link' * @param string $size Classe Bootstrap pour indiquer la taille du bouton : 'lg', 'md', 'sm' (défaut), 'xs' * @param array $attrs Liste d'attributs HTML supplémentaires (ex 'class', 'title', 'confirm', 'disabled') * @param bool $trans Si faux, désactivation de la traduction du libellé du bouton (et éventuel message de confirmation) * @return string Code HTML représentant un bouton Bootstrap */public function bsActionFilter(string $target=null, string $icon=null, string $title=null, string $context=null, string $size=null, array $attrs=[], bool $trans=true): string { if ($title !== null) $attrs['title'] = ($trans) ? $this->trans($title) : $title; if (empty($context)) $context = 'primary'; if (empty($size)) $size = 'sm'; return $this->bsButtonFilter($target, $icon, null, $context, $size, $attrs, $trans); } /** * Affichage d'un texte dans une boite d'alerte Bootstrap. * * @param string $text Texte à afficher dans la boite d'alerte * @param string $state Classe Bootstrap : primary, secondary, info (défaut), success, warning, danger, dark, light * @param mixed $icon Nom d'un icone (ou true/null par défaut pour automatique à partir du $state) * @param bool $html Désactivation de l'échappement du texte s'il doit déjà contenir du HTML * @param bool $close Ajout d'un bouton permettant de fermer la boite d'alerte * @param bool $trans Recherche d'une traduction pour le token $text (si c'est une chaine) * @return string Code HTML représentant un message d'alerte Bootstrap */public function bsAlertFilter($text, ?string $state = null, $icon = null, bool $html = false, bool $close = false, bool $trans = false): string { $class = 'alert-' . (empty($state) ? 'info' : $state); $head = $body = $foot = ''; if ($icon === null || $icon === true) $icon = $this->getStateIcon($state); if ($icon) { $class.= ' d-flex align-items-center'; $head = $this->getIcon($icon) . '<div>'; $foot = '</div>'; } if ($close) { $class.= ' alert-dismissible fade show'; $foot.= '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>'; } $body = ($trans) ? $this->trans($text) : $text; if (! $html) $body = \htmlspecialchars($body); return \sprintf('<div class="alert %s" role="alert">%s</div>', $class, $head, $body, $foot); } /** * Formattage d'une liste de liens en barre de navigation Bootstrap (pour header de card). * * @param array $navs Liste de références pour les différents onglets/boutons de la barre * @param string $type Style des liens : 'tabs' (onglets), 'pills' (boutons) ou 'vtabs' (boutons verticaux) * @param string $label Préfixe des tokens de traductions pour les libellés des références (défaut 'tabs.group.') * @param array $extra Liste de contenus HTML (indexés par les références) à ajouter après le libellé * @return string Code HTML d'une liste de liens de navigation Bootstrap (onglets) */public function cardTabsHeadFunction(array $navs, string $type = 'tabs', ?string $label = 'tabs.group.', array $extra = []): string { if (empty($navs)) return ''; if ($type == 'vtabs') { $container_tag = 'div'; $container_mode = 'vertical'; $container_class = 'nav nav-pills flex-column me-3'; } else { $container_tag = 'ul'; $container_mode = 'horizontal'; $container_class = 'nav nav-' . $type . ' card-header-tabs'; } $line = '<button role="tab" data-bs-toggle="'.(($type=='tabs')?'tab':'pill').'" data-bs-target="#%1$s"'; $line.= ' class="nav-link%4$s" aria-controls="%2$s" type="button">%2$s%3$s</button>'; if ($container_tag == 'ul') $line = '<li role="presentation" class="nav-item">' . $line . '</li>' . "\n"; $html = \sprintf('<%s role="tablist" class="%s" aria-orientation="%s">', $container_tag, $container_class, $container_mode); $active = ' active'; foreach ($navs as $nav) { if ($nav === '_') $nav = "misc"; $ref = StringHelper::slugify($nav); $html.= \sprintf($line, \htmlspecialchars($ref . '_pane'), \htmlspecialchars(empty($label) ? $nav : $this->trans($label . $ref)), isset($extra[$ref]) ? $extra[$ref] : '', $active ); $active = ""; } $html.= '</' . $container_tag . '>'; $html.= <<<END <script>if (typeof ticStarter == "object") ticStarter.add( function(){ // activation de l'onglet correspondant si un hash est passé dans l'URL var hash = document.location.hash.replace('_tab', '_pane'); if (hash) jQuery('.nav [data-bs-toggle][data-bs-target="' + hash + '"]').first().tab('show'); // mise à jour du hash dans l'URL sur changement d'onglet (ancre altérée pour éviter le déplacement) var res = jQuery('.nav [data-bs-toggle][data-bs-target]').on('shown.bs.tab', function(e){ document.location.hash = e.target.data('bs-target').replace('_pane', '_tab'); });});</script>END; return $html; } } |