diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php new file mode 100644 index 0000000..b44c0f4 --- /dev/null +++ b/src/View/Helper/BootstrapHelper.php @@ -0,0 +1,298 @@ +Bootstrap->Tabs([ + * 'pills' => true, + * 'card' => true, + * 'data' => [ + * 'navs' => [ + * 'tab1', + * ['text' => 'tab2', 'active' => true], + * ['html' => 'tab3', 'disabled' => true], + * ], + * 'content' => [ + * 'body1', + * 'body2', + * '~body3~' + * ] + * ] + * ]); + */ + +namespace App\View\Helper; + +use Cake\View\Helper; +use Cake\Utility\Security; +use InvalidArgumentException; + +class BootstrapHelper extends Helper +{ + public function tabs($options) + { + $bsTabs = new BootstrapTabs($options); + return $bsTabs->tabs(); + } +} + +class BootstrapTabs extends Helper +{ + private $defaultOptions = [ + 'nav-fill' => false, + 'nav-justify' => false, + 'pills' => false, + 'vertical' => false, + 'vertical-size' => 3, + 'card' => false, + 'nav-class' => [], + 'nav-item-class' => [], + 'content-class' => [], + 'data' => [ + 'navs' => [], + 'content' => [], + ], + ]; + + private $allowedOptionValues = [ + 'nav-justify' => [false, 'center', 'end'], + ]; + + private $options = null; + private $bsClasses = null; + + function __construct($options) { + $this->processOptions($options); + } + + public function tabs() + { + return $this->genTabs(); + } + + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->data = $this->options['data']; + $this->checkOptionValidity(); + $this->bsClasses = [ + 'nav' => [], + 'nav-item' => $this->options['nav-item-class'], + + ]; + + if (!empty($this->options['nav-justify'])) { + $this->bsClasses['nav'][] = 'justify-content-' . $this->options['nav-justify']; + } + + if ($this->options['pills']) { + $this->bsClasses['nav'][] = 'nav-pills'; + if ($this->options['vertical']) { + $this->bsClasses['nav'][] = 'flex-column'; + } + if ($this->options['card']) { + $this->bsClasses['nav'][] = 'card-header-pills'; + } + } else { + $this->bsClasses['nav'][] = 'nav-tabs'; + if ($this->options['card']) { + $this->bsClasses['nav'][] = 'card-header-tabs'; + } + } + + if ($this->options['nav-fill']) { + $this->bsClasses['nav'][] = 'nav-fill'; + } + if ($this->options['nav-justify']) { + $this->bsClasses['nav'][] = 'nav-justify'; + } + + $activeTab = 0; + foreach ($this->data['navs'] as $i => $nav) { + if (!is_array($nav)) { + $this->data['navs'][$i] = ['text' => $nav]; + } + if (!isset($this->data['navs'][$i]['id'])) { + $this->data['navs'][$i]['id'] = 't-' . Security::randomString(8); + } + if (!empty($nav['active'])) { + $activeTab = $i; + } + } + $this->data['navs'][$activeTab]['active'] = true; + + $this->options['vertical-size'] = $this->options['vertical-size'] < 0 || $this->options['vertical-size'] > 11 ? 3 : $this->options['vertical-size']; + } + + private function checkOptionValidity() + { + foreach ($this->allowedOptionValues as $option => $values) { + if (!isset($this->options[$option])) { + throw new InvalidArgumentException(__('Option `{0}` should have a value', $option)); + } + if (!in_array($this->options[$option], $values)) { + throw new InvalidArgumentException(__('Option `{0}` is not a valid option for `{1}`. Accepted values: {2}', json_encode($this->options[$option]), $option, json_encode($values))); + } + } + if (empty($this->data['navs'])) { + throw new InvalidArgumentException(__('No navigation data provided')); + } + if ($this->options['card'] && $this->options['vertical']) { + throw new InvalidArgumentException(__('`card` option can only be used on horizontal mode')); + } + } + + private function genTabs() + { + $html = ''; + if ($this->options['vertical']) { + $html .= $this->genVerticalTabs(); + } else { + $html .= $this->genHorizontalTabs(); + } + return $html; + } + + private function genHorizontalTabs() + { + $html = ''; + if ($this->options['card']) { + $html .= $this->genNode('div', ['class' => ['card']]); + $html .= $this->genNode('div', ['class' => ['card-header']]); + } + $html .= $this->genNav(); + if ($this->options['card']) { + $html .= ''; + $html .= $this->genNode('div', ['class' => ['card-body']]); + } + $html .= $this->genContent(); + if ($this->options['card']) { + $html .= ''; + } + return $html; + } + + private function genVerticalTabs() + { + $html = sprintf('
', $this->genHTMLParams(['class' => 'row'])); + $html .= sprintf('
', $this->genHTMLParams(['class' => 'col-' . $this->options['vertical-size']])); + $html .= $this->genNav(); + $html .= '
'; + $html .= sprintf('
', $this->genHTMLParams(['class' => 'col-' . (12 - $this->options['vertical-size'])])); + $html .= $this->genContent(); + $html .= '
'; + $html .= '
'; + return $html; + } + + private function genNav() + { + $ulParams = [ + 'class' => array_merge(['nav'], $this->bsClasses['nav'], $this->options['nav-class']), + 'role' => 'tablist', + ]; + $html = sprintf(''; + return $html; + } + + private function genNavItem($navItem) + { + $liParams = [ + 'class' => array_merge(['nav-item'], $this->bsClasses['nav-item'], $this->options['nav-item-class']), + 'role' => 'presentation', + ]; + $aParams = [ + 'class' => array_merge( + ['nav-link'], + [!empty($navItem['active']) ? 'active' : ''], + [!empty($navItem['disabled']) ? 'disabled' : ''] + ), + 'data-toggle' => $this->options['pills'] ? 'pill' : 'tab', + 'id' => $navItem['id'] . '-tab', + 'href' => '#' . $navItem['id'], + 'aria-controls' => $navItem['id'], + 'aria-selected' => !empty($navItem['active']), + 'role' => 'tab', + ]; + $html = sprintf('
  • ', $this->genHTMLParams($liParams)); + $html .= sprintf('', $this->genHTMLParams($aParams)); + if (!empty($navItem['html'])) { + $html .= $navItem['html']; + } else { + $html .= h($navItem['text']); + } + $html .= '
  • '; + return $html; + } + + private function genContent() + { + $divParams = [ + 'class' => array_merge(['tab-content'], $this->options['content-class']), + ]; + $html = sprintf('
    ', $this->genHTMLParams($divParams)); + foreach ($this->data['content'] as $i => $content) { + $navItem = $this->data['navs'][$i]; + $html .= $this->genContentItem($navItem, $content); + } + $html .= ''; + return $html; + } + + private function genContentItem($navItem, $content) + { + $divParams = [ + 'class' => array_merge(['tab-pane', 'fade'], [!empty($navItem['active']) ? 'show active' : '']), + 'role' => 'tabpanel', + 'id' => $navItem['id'], + 'aria-labelledby' => $navItem['id'] . '-tab' + ]; + $html = sprintf('
    ', $this->genHTMLParams($divParams)); + $html .= $content; + $html .= '
    '; + return $html; + } + + private function genNode($node, $params) + { + return sprintf('<%s %s>', $node, $this->genHTMLParams($params)); + } + + private function genHTMLParams($params) + { + $html = ''; + foreach ($params as $k => $v) { + $html .= $this->genHTMLParam($k, $v) . ' '; + } + return $html; + } + + private function genHTMLParam($paramName, $values) + { + if (!is_array($values)) { + $values = [$values]; + } + return sprintf('%s="%s"', $paramName, implode(' ', $values)); + } +} +