| 1: | <?php |
| 2: | namespace TIC\TwigBundle\Extension; |
| 3: | |
| 4: | use TIC\TwigBundle\Base\TICTwigExtension as BaseExtension; |
| 5: | use TIC\CoreBundle\Util\ConvertHelper; |
| 6: | use TIC\CoreBundle\Util\StringHelper; |
| 7: | |
| 8: | use Symfony\Contracts\Translation\TranslatorInterface; |
| 9: | use Twig\TwigFilter; |
| 10: | use Twig\TwigFunction; |
| 11: | |
| 12: | |
| 13: | |
| 14: | |
| 15: | |
| 16: | class BootstrapExtension extends BaseExtension |
| 17: | { |
| 18: | |
| 19: | public function getFilters(): array |
| 20: | { |
| 21: | return [ |
| 22: | new TwigFilter('bsState', [$this, 'bsStateFilter'], ['is_safe' => ['html']] ), |
| 23: | new TwigFilter('bsBoolean', [$this, 'bsBooleanFilter'], ['is_safe' => ['html']] ), |
| 24: | new TwigFilter('bsEnabled', [$this, 'bsEnabledFilter'], ['is_safe' => ['html']] ), |
| 25: | new TwigFilter('bsToggle', [$this, 'bsToggleFilter'], ['is_safe' => ['html']] ), |
| 26: | new TwigFilter('bsLabel', [$this, 'bsLabelFilter'], ['is_safe' => ['html']] ), |
| 27: | new TwigFilter('bsBadge', [$this, 'bsBadgeFilter'], ['is_safe' => ['html']] ), |
| 28: | new TwigFilter('bsCount', [$this, 'bsCountFilter'], ['is_safe' => ['html']] ), |
| 29: | new TwigFilter('bsButton', [$this, 'bsButtonFilter'], ['is_safe' => ['html']] ), |
| 30: | new TwigFilter('bsAction', [$this, 'bsActionFilter'], ['is_safe' => ['html']] ), |
| 31: | new TwigFilter('bsAlert', [$this, 'bsAlertFilter'], ['is_safe' => ['html']] ), |
| 32: | |
| 33: | ]; |
| 34: | } |
| 35: | |
| 36: | public function getFunctions(): array |
| 37: | { |
| 38: | return [ |
| 39: | new TwigFunction('card_tabs_head', [$this, 'cardTabsHeadFunction'], ['is_safe' => ['html']] ), |
| 40: | new TwigFunction('form_tabs_head', null, ['node_class' => 'Symfony\Bridge\Twig\Node\RenderBlockNode', 'is_safe' => ['html']] ), |
| 41: | new TwigFunction('form_tabs_body', null, ['node_class' => 'Symfony\Bridge\Twig\Node\RenderBlockNode', 'is_safe' => ['html']] ), |
| 42: | ]; |
| 43: | } |
| 44: | |
| 45: | public function __construct(TranslatorInterface $translator = null) |
| 46: | { |
| 47: | $this->translator = $translator; |
| 48: | } |
| 49: | |
| 50: | |
| 51: | |
| 52: | |
| 53: | |
| 54: | protected function getStateIcon(string $state): ?string |
| 55: | { |
| 56: | switch ($state) { |
| 57: | case 'primary' : return 'bi-chat-fill'; |
| 58: | case 'secondary' : return 'bi-question-circle-fill'; |
| 59: | case 'info' : return 'bi-info-fill'; |
| 60: | case 'success' : return 'bi-check-square-fill'; |
| 61: | case 'warning' : return 'bi-exclamation-circle-fill'; |
| 62: | case 'danger' : return 'bi-exclamation-triangle-fill'; |
| 63: | case 'dark' : return 'bi-exclamation-square-fill'; |
| 64: | case 'light' : return 'bi-chat-left-fill'; |
| 65: | } |
| 66: | return null; |
| 67: | } |
| 68: | |
| 69: | |
| 70: | |
| 71: | |
| 72: | |
| 73: | |
| 74: | |
| 75: | |
| 76: | public function bsStateFilter($value): string |
| 77: | { |
| 78: | if (\is_object($value)) switch (true) { |
| 79: | case ($value instanceof \DateTime): |
| 80: | $value = $value->format('Ymd') - \date('Ymd'); |
| 81: | break; |
| 82: | case \method_exists($value, '__toString'): |
| 83: | $value = (string)$value; |
| 84: | break; |
| 85: | default: |
| 86: | return 'light'; |
| 87: | } |
| 88: | |
| 89: | if (\is_string($value)) switch ($value) { |
| 90: | case 'primary': |
| 91: | case 'secondary': |
| 92: | case 'success': |
| 93: | case 'danger': |
| 94: | case 'warning': |
| 95: | case 'info': |
| 96: | case 'light': |
| 97: | case 'dark': |
| 98: | return $value; |
| 99: | case 'ext.boolean.true': $value = true; break; |
| 100: | case 'ext.boolean.false': $value = false; break; |
| 101: | case 'ext.boolean.null': $value = null; break; |
| 102: | |
| 103: | } |
| 104: | |
| 105: | if (\is_null($value)) return 'dark'; |
| 106: | if (\is_bool($value)) return ($value) ? 'success' : 'danger'; |
| 107: | if (\is_numeric($value)) { |
| 108: | if ($value > 0) return 'success'; |
| 109: | if ($value < 0) return 'danger'; |
| 110: | return 'warning'; |
| 111: | } |
| 112: | return 'info'; |
| 113: | } |
| 114: | |
| 115: | |
| 116: | |
| 117: | |
| 118: | |
| 119: | |
| 120: | |
| 121: | |
| 122: | public function bsBooleanFilter($value, string $default='-'): string |
| 123: | { |
| 124: | $text = self::$strings['null']; |
| 125: | try { |
| 126: | $bool = $this->getBool($value); |
| 127: | if ($bool !== null) $text = self::$strings[($bool) ? 'true' : 'false' ]; |
| 128: | } catch (\UnexpectedValueException $e) { |
| 129: | $text = $default; |
| 130: | } |
| 131: | return $this->bsLabelFilter($text, true, true); |
| 132: | } |
| 133: | |
| 134: | |
| 135: | |
| 136: | |
| 137: | |
| 138: | |
| 139: | |
| 140: | |
| 141: | |
| 142: | public function bsEnabledFilter($value, string $tok_true='ext.state.enabled', string $tok_false='ext.state.disabled'): string |
| 143: | { |
| 144: | $state = $this->bsStateFilter($this->getBool($value)); |
| 145: | switch ($state) { |
| 146: | case 'success' : $label = $tok_true; break; |
| 147: | case 'danger' : $label = $tok_false; break; |
| 148: | default : $label = $value; |
| 149: | } |
| 150: | return $this->bsLabelFilter($label, $state, true); |
| 151: | } |
| 152: | |
| 153: | |
| 154: | |
| 155: | |
| 156: | |
| 157: | |
| 158: | |
| 159: | |
| 160: | |
| 161: | public function bsToggleFilter(mixed $value, string $route_on, string $route_off = null): string |
| 162: | { |
| 163: | if ($route_off === null) $route_off = $route_on; |
| 164: | |
| 165: | $vbool = $this->getBool($value); |
| 166: | $route = ($vbool) ? $route_off : $route_on; |
| 167: | $title = ($vbool) ? "widget.toggle.off" : "widget.toggle.on"; |
| 168: | |
| 169: | return sprintf('<a href="%s" title="%s">%s</a>', |
| 170: | $route, $this->trans($title), $this->bsEnabledFilter($value) |
| 171: | ); |
| 172: | } |
| 173: | |
| 174: | |
| 175: | |
| 176: | |
| 177: | |
| 178: | |
| 179: | |
| 180: | |
| 181: | |
| 182: | |
| 183: | |
| 184: | |
| 185: | public function bsLabelFilter($value, $state=null, bool $trans=false, array $attrs=[]): string |
| 186: | { |
| 187: | if ($state === true) $state = $this->bsStateFilter($value); |
| 188: | |
| 189: | return \sprintf('<span class="badge bg-%s"%s>%s</span>', |
| 190: | empty($state) ? 'dark' : $state, |
| 191: | $this->htmlAttr($attrs), |
| 192: | $this->getText($value, $trans, true) |
| 193: | ); |
| 194: | } |
| 195: | |
| 196: | |
| 197: | |
| 198: | |
| 199: | |
| 200: | |
| 201: | |
| 202: | |
| 203: | |
| 204: | |
| 205: | |
| 206: | |
| 207: | public function bsBadgeFilter($value, $state=null, bool $trans=false, ?string $default=''): string |
| 208: | { |
| 209: | if ($value === null) { if ($default === null) return ''; $value = $default; } |
| 210: | if (! \is_scalar($value)) $value = \count($value); |
| 211: | |
| 212: | if ($state === true) $state = $this->bsStateFilter($value); |
| 213: | |
| 214: | return \sprintf('<span class="badge rounded-pill bg-%s">%s</span>', |
| 215: | empty($state) ? 'dark' : $state, |
| 216: | $this->getText($value, $trans, true) |
| 217: | ); |
| 218: | } |
| 219: | |
| 220: | |
| 221: | |
| 222: | |
| 223: | |
| 224: | |
| 225: | |
| 226: | |
| 227: | |
| 228: | |
| 229: | |
| 230: | |
| 231: | public function bsCountFilter($value, $state=null, ?string $title=null, ?string $default=null) |
| 232: | { |
| 233: | $value = ConvertHelper::counter($value); |
| 234: | if ($value === null) return $default; |
| 235: | |
| 236: | if ($state === true) $state = $this->bsStateFilter($value); |
| 237: | |
| 238: | return \sprintf('<span class="tic-counter badge rounded-pill bg-%s"%s>%s</span>', |
| 239: | empty($state) ? 'dark' : $state, |
| 240: | empty($title) ? '' : ' title="'.\htmlspecialchars($title).'"', |
| 241: | \number_format($value, 0, ',', ' ') |
| 242: | ); |
| 243: | } |
| 244: | |
| 245: | |
| 246: | |
| 247: | |
| 248: | |
| 249: | |
| 250: | |
| 251: | |
| 252: | |
| 253: | |
| 254: | |
| 255: | |
| 256: | |
| 257: | |
| 258: | |
| 259: | |
| 260: | |
| 261: | |
| 262: | |
| 263: | public function bsButtonFilter(string $target=null, string $icon=null, $label=null, string $context=null, string $size=null, array $attrs=[], bool $trans=true): string |
| 264: | { |
| 265: | |
| 266: | $confirm = false; |
| 267: | if (\array_key_exists('confirm', $attrs)) { |
| 268: | $confirm = $attrs['confirm']; |
| 269: | unset($attrs['confirm']); |
| 270: | if (\is_string($confirm) && \strlen($confirm)) $attrs['data-confirm'] = ($trans) ? $this->trans($confirm) : $confirm; |
| 271: | } |
| 272: | |
| 273: | |
| 274: | if (\array_key_exists('modal', $attrs)) { |
| 275: | if (! isset($attrs['disabled']) || ! $attrs['disabled']) { |
| 276: | $attrs['data-bs-toggle'] = "modal"; |
| 277: | $attrs['data-bs-target'] = "#" . $attrs['modal']; |
| 278: | } |
| 279: | unset($attrs['modal']); |
| 280: | } |
| 281: | |
| 282: | |
| 283: | if (\preg_match('/^(submit|reset|button)?$/', $target)) { |
| 284: | $tag = 'button'; |
| 285: | if ($target !== '') { |
| 286: | $attrs['type'] = $target; |
| 287: | if ($label === true ) { $label = 'btn.' . $target; $trans = true; } |
| 288: | } |
| 289: | } |
| 290: | elseif (\preg_match('/^([#\.])(.*)$/', $target, $match)) { |
| 291: | $tag = 'button'; |
| 292: | $attrs['type'] = 'button'; |
| 293: | if ($match[1] == '#') $attrs['id'] = $match[2]; |
| 294: | else $attrs['class'] = $match[2] . ' ' . $attrs['class']; |
| 295: | if ($label === true) { $label = 'btn.' . $match[2]; $trans = true; } |
| 296: | } |
| 297: | elseif (isset($attrs['disabled']) && $attrs['disabled']) { |
| 298: | |
| 299: | |
| 300: | $tag = 'button'; |
| 301: | $attrs['type'] = 'button'; |
| 302: | $attrs['data-href'] = $target; |
| 303: | if ($label === true) { $label = $target; $trans = false; } |
| 304: | } |
| 305: | elseif ($confirm) { |
| 306: | |
| 307: | |
| 308: | $tag = 'a'; |
| 309: | $attrs['role'] = 'button'; |
| 310: | $attrs['href'] = '#need_confirmation'; |
| 311: | $attrs['data-href'] = $target; |
| 312: | } |
| 313: | else { |
| 314: | $tag = 'a'; |
| 315: | $attrs['role'] = 'button'; |
| 316: | $attrs['href'] = $target; |
| 317: | if ($label === true) { $label = $target; $trans = false; } |
| 318: | if (isset($attrs['disabled'])) unset($attrs['disabled']); |
| 319: | } |
| 320: | |
| 321: | |
| 322: | if (! \strlen($context)) $context = ($target === 'submit') ? "primary" : "secondary"; |
| 323: | $class = "btn btn-" . $context; |
| 324: | if (! empty($size) && ($size != 'md')) $class.= " btn-" . $size; |
| 325: | if (! empty($confirm)) $class.= " btn-confirm"; |
| 326: | if (isset($attrs['disabled']) && $attrs['disabled']) $class.= " disabled"; |
| 327: | if (\array_key_exists('class', $attrs)) $class.= " " .$attrs['class']; |
| 328: | |
| 329: | |
| 330: | if ($label === null) $label = ''; |
| 331: | if ($trans) $label = $this->trans($label); |
| 332: | if ($icon) { |
| 333: | $content = $this->getIcon($icon); |
| 334: | if (\strlen($label)) { $content.= " $label"; $class.= " icon-left"; } |
| 335: | } else { |
| 336: | $content = $label; |
| 337: | } |
| 338: | |
| 339: | |
| 340: | $attrs['class'] = $class; |
| 341: | return \sprintf('<%1$s %2$s>%3$s</%1$s>', $tag, $this->htmlAttr($attrs), $content); |
| 342: | } |
| 343: | |
| 344: | |
| 345: | |
| 346: | |
| 347: | |
| 348: | |
| 349: | |
| 350: | |
| 351: | |
| 352: | |
| 353: | |
| 354: | |
| 355: | |
| 356: | public function bsActionFilter(string $target=null, string $icon=null, string $title=null, string $context=null, string $size=null, array $attrs=[], bool $trans=true): string |
| 357: | { |
| 358: | if ($title !== null) $attrs['title'] = ($trans) ? $this->trans($title) : $title; |
| 359: | if (empty($context)) $context = 'primary'; |
| 360: | if (empty($size)) $size = 'sm'; |
| 361: | return $this->bsButtonFilter($target, $icon, null, $context, $size, $attrs, $trans); |
| 362: | } |
| 363: | |
| 364: | |
| 365: | |
| 366: | |
| 367: | |
| 368: | |
| 369: | |
| 370: | |
| 371: | |
| 372: | |
| 373: | |
| 374: | |
| 375: | public function bsAlertFilter($text, ?string $state = null, $icon = null, bool $html = false, bool $close = false, bool $trans = false): string |
| 376: | { |
| 377: | $class = 'alert-' . (empty($state) ? 'info' : $state); |
| 378: | $head = $body = $foot = ''; |
| 379: | |
| 380: | if ($icon === null || $icon === true) $icon = $this->getStateIcon($state); |
| 381: | if ($icon) { |
| 382: | $class.= ' d-flex align-items-center'; |
| 383: | $head = $this->getIcon($icon) . '<div>'; |
| 384: | $foot = '</div>'; |
| 385: | } |
| 386: | if ($close) { |
| 387: | $class.= ' alert-dismissible fade show'; |
| 388: | $foot.= '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>'; |
| 389: | } |
| 390: | $body = ($trans) ? $this->trans($text) : $text; |
| 391: | if (! $html) $body = \htmlspecialchars($body); |
| 392: | return \sprintf('<div class="alert %s" role="alert">%s</div>', $class, $head, $body, $foot); |
| 393: | } |
| 394: | |
| 395: | |
| 396: | |
| 397: | |
| 398: | |
| 399: | |
| 400: | |
| 401: | |
| 402: | |
| 403: | |
| 404: | |
| 405: | public function cardTabsHeadFunction(array $navs, string $type = 'tabs', ?string $label = 'tabs.group.', array $extra = []): string |
| 406: | { |
| 407: | if (empty($navs)) return ''; |
| 408: | |
| 409: | if ($type == 'vtabs') { |
| 410: | $container_tag = 'div'; |
| 411: | $container_mode = 'vertical'; |
| 412: | $container_class = 'nav nav-pills flex-column me-3'; |
| 413: | } else { |
| 414: | $container_tag = 'ul'; |
| 415: | $container_mode = 'horizontal'; |
| 416: | $container_class = 'nav nav-' . $type . ' card-header-tabs'; |
| 417: | } |
| 418: | |
| 419: | $line = '<button role="tab" data-bs-toggle="'.(($type=='tabs')?'tab':'pill').'" data-bs-target="#%1$s"'; |
| 420: | $line.= ' class="nav-link%4$s" aria-controls="%2$s" type="button">%2$s%3$s</button>'; |
| 421: | if ($container_tag == 'ul') $line = '<li role="presentation" class="nav-item">' . $line . '</li>' . "\n"; |
| 422: | |
| 423: | $html = \sprintf('<%s role="tablist" class="%s" aria-orientation="%s">', $container_tag, $container_class, $container_mode); |
| 424: | $active = ' active'; |
| 425: | foreach ($navs as $nav) { |
| 426: | if ($nav === '_') $nav = "misc"; |
| 427: | $ref = StringHelper::slugify($nav); |
| 428: | $html.= \sprintf($line, |
| 429: | \htmlspecialchars($ref . '_pane'), |
| 430: | \htmlspecialchars(empty($label) ? $nav : $this->trans($label . $ref)), |
| 431: | isset($extra[$ref]) ? $extra[$ref] : '', |
| 432: | $active |
| 433: | ); |
| 434: | $active = ""; |
| 435: | } |
| 436: | $html.= '</' . $container_tag . '>'; |
| 437: | |
| 438: | $html.= <<<END |
| 439: | <script> |
| 440: | if (typeof ticStarter == "object") ticStarter.add( function(){ |
| 441: | // activation de l'onglet correspondant si un hash est passé dans l'URL |
| 442: | var hash = document.location.hash.replace('_tab', '_pane'); |
| 443: | if (hash) jQuery('.nav [data-bs-toggle][data-bs-target="' + hash + '"]').first().tab('show'); |
| 444: | |
| 445: | // mise à jour du hash dans l'URL sur changement d'onglet (ancre altérée pour éviter le déplacement) |
| 446: | var res = jQuery('.nav [data-bs-toggle][data-bs-target]').on('shown.bs.tab', function(e){ |
| 447: | document.location.hash = e.target.data('bs-target').replace('_pane', '_tab'); |
| 448: | }); |
| 449: | }); |
| 450: | </script> |
| 451: | END; |
| 452: | return $html; |
| 453: | } |
| 454: | |
| 455: | } |
| 456: | |