Skip to content

Commit 5559232

Browse files
linawolfjaapio
authored andcommitted
[FEATURE] Provide directive to display tabs in bootstrap theme
1 parent 21dba42 commit 5559232

File tree

12 files changed

+395
-1
lines changed

12 files changed

+395
-1
lines changed

packages/guides-restructured-text/src/RestructuredText/Parser/Directive.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ public function addOption(DirectiveOption $value): void
5151
{
5252
$this->options[$value->getName()] = $value;
5353
}
54+
55+
public function hasOption(string $name): bool
56+
{
57+
return isset($this->options[$name]);
58+
}
5459

5560
public function getOption(string $name): DirectiveOption
5661
{

packages/guides-theme-bootstrap/resources/config/guides-theme-bootstrap.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,24 @@
22

33
declare(strict_types=1);
44

5+
use phpDocumentor\Guides\Bootstrap\Directives\TabDirective;
6+
use phpDocumentor\Guides\Bootstrap\Directives\TabsDirective;
7+
use phpDocumentor\Guides\RestructuredText\Directives\BaseDirective;
8+
use phpDocumentor\Guides\RestructuredText\Directives\SubDirective;
9+
use phpDocumentor\Guides\RestructuredText\Parser\Productions\DirectiveContentRule;
510
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
611

12+
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
13+
714
return static function (ContainerConfigurator $container): void {
815
$container->services()
916
->defaults()
1017
->autowire()
11-
->autoconfigure();
18+
->autoconfigure()
19+
->instanceof(SubDirective::class)
20+
->bind('$startingRule', service(DirectiveContentRule::class))
21+
->instanceof(BaseDirective::class)
22+
->tag('phpdoc.guides.directive')
23+
->set(TabDirective::class)
24+
->set(TabsDirective::class);
1225
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ renderNode(tab.value) }}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<div class="tabs-container {% if node.classes %} {{ node.classesString }}{% endif %}">
2+
<ul class="nav nav-tabs" id="{{ node.key }}" role="tablist">
3+
{% for tab in node.tabs -%}
4+
{% include "body/directive/tabs/tabs-button.html.twig" with {
5+
tab:tab
6+
} %}
7+
{%- endfor %}
8+
</ul>
9+
<div class="tab-content" id="{{ node.key }}-content">
10+
{% for tab in node.tabs -%}
11+
{% include "body/directive/tabs/tabs-body.html.twig" with {
12+
tab:tab
13+
} %}
14+
{%- endfor %}
15+
</div>
16+
</div>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div class="tab-pane fade {%- if tab.active %} show active{% endif -%} {%- if tab.classes %} {{ tab.classesString }}{% endif %}" id="{{ tab.key }}-tab-pane" role="tabpanel" aria-labelledby="{{ tab.key }}-tab" tabindex="0">
2+
{{ renderNode(tab.value) }}
3+
</div>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<li class="nav-item" role="presentation">
2+
<button class="nav-link {%- if tab.active %} active{% endif -%}" id="{{ tab.key }}-tab" data-bs-toggle="tab" data-bs-target="#{{ tab.key }}-tab-pane"
3+
type="button" role="tab" aria-controls="{{ tab.key }}-tab-pane"
4+
aria-selected="{%- if tab.active -%} true {%- else -%} false{% endif -%}">{{ renderNode(tab.content) }}</button>
5+
</li>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link https://phpdoc.org
12+
*/
13+
14+
namespace phpDocumentor\Guides\Bootstrap\Directives;
15+
16+
use phpDocumentor\Guides\Bootstrap\Nodes\TabNode;
17+
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
18+
use phpDocumentor\Guides\Nodes\Node;
19+
use phpDocumentor\Guides\RestructuredText\Directives\SubDirective;
20+
use phpDocumentor\Guides\RestructuredText\Nodes\CollectionNode;
21+
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
22+
23+
use function is_string;
24+
use function preg_replace;
25+
use function str_replace;
26+
use function strtolower;
27+
use function strval;
28+
29+
class TabDirective extends SubDirective
30+
{
31+
public function getName(): string
32+
{
33+
return 'tab';
34+
}
35+
36+
/** {@inheritDoc}
37+
*
38+
* @param Directive $directive
39+
*/
40+
protected function processSub(
41+
CollectionNode $collectionNode,
42+
Directive $directive,
43+
): Node|null {
44+
if (is_string($directive->getOption('key')->getValue())) {
45+
$key = strtolower($directive->getOption('key')->getValue());
46+
} else {
47+
$key = strtolower($directive->getData());
48+
}
49+
50+
$key = str_replace(' ', '-', $key);
51+
$key = strval(preg_replace('/[^a-zA-Z0-9\-_]/', '', $key));
52+
$active = $directive->hasOption('active');
53+
54+
return new TabNode(
55+
'tab',
56+
$directive->getData(),
57+
$directive->getDataNode() ?? new InlineCompoundNode(),
58+
$key,
59+
$active,
60+
$collectionNode->getChildren(),
61+
);
62+
}
63+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link https://phpdoc.org
12+
*/
13+
14+
namespace phpDocumentor\Guides\Bootstrap\Directives;
15+
16+
use phpDocumentor\Guides\Bootstrap\Nodes\TabNode;
17+
use phpDocumentor\Guides\Bootstrap\Nodes\TabsNode;
18+
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
19+
use phpDocumentor\Guides\Nodes\Node;
20+
use phpDocumentor\Guides\RestructuredText\Directives\SubDirective;
21+
use phpDocumentor\Guides\RestructuredText\Nodes\CollectionNode;
22+
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
23+
use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule;
24+
use Psr\Log\LoggerInterface;
25+
26+
use function is_string;
27+
use function preg_replace;
28+
use function rand;
29+
use function str_replace;
30+
use function strtolower;
31+
use function strval;
32+
33+
class TabsDirective extends SubDirective
34+
{
35+
/** @param Rule<CollectionNode> $startingRule */
36+
public function __construct(protected Rule $startingRule, private readonly LoggerInterface $logger)
37+
{
38+
parent::__construct($startingRule);
39+
}
40+
41+
public function getName(): string
42+
{
43+
return 'tabs';
44+
}
45+
46+
/** {@inheritDoc}
47+
*
48+
* @param Directive $directive
49+
*/
50+
protected function processSub(
51+
CollectionNode $collectionNode,
52+
Directive $directive,
53+
): Node|null {
54+
$tabs = [];
55+
$hasActive = false;
56+
foreach ($collectionNode->getChildren() as $child) {
57+
if ($child instanceof TabNode) {
58+
if ($child->isActive()) {
59+
if (!$hasActive) {
60+
$hasActive = true;
61+
} else {
62+
// There may only be one active child, first wins
63+
$child->setActive(false);
64+
}
65+
}
66+
67+
$tabs[] = $child;
68+
} else {
69+
$this->logger->warning('The "tabs" directive may only contain children of type "tab". The following node was found: ' . $child::class);
70+
}
71+
}
72+
73+
if (!$hasActive && isset($tabs[0])) {
74+
$tabs[0]->setActive(true);
75+
}
76+
77+
if (is_string($directive->getOption('key')->getValue())) {
78+
$key = strtolower($directive->getOption('key')->getValue());
79+
$key = str_replace(' ', '-', $key);
80+
$key = strval(preg_replace('/[^a-zA-Z0-9\-_]/', '', $key));
81+
} else {
82+
$key = 'tabs-' . rand(1, 1000);
83+
}
84+
85+
return new TabsNode(
86+
'tabs',
87+
$directive->getData(),
88+
$directive->getDataNode() ?? new InlineCompoundNode(),
89+
$key,
90+
$tabs,
91+
);
92+
}
93+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpDocumentor\Guides\Bootstrap\Nodes;
6+
7+
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
8+
use phpDocumentor\Guides\Nodes\Node;
9+
use phpDocumentor\Guides\RestructuredText\Nodes\GeneralDirectiveNode;
10+
11+
final class TabNode extends GeneralDirectiveNode
12+
{
13+
/** @param list<Node> $value */
14+
public function __construct(
15+
protected readonly string $name,
16+
protected readonly string $plainContent,
17+
protected readonly InlineCompoundNode $content,
18+
private readonly string $key,
19+
private bool $active,
20+
array $value = [],
21+
) {
22+
parent::__construct($name, $plainContent, $content, $value);
23+
}
24+
25+
public function getKey(): string
26+
{
27+
return $this->key;
28+
}
29+
30+
public function isActive(): bool
31+
{
32+
return $this->active;
33+
}
34+
35+
public function setActive(bool $active): void
36+
{
37+
$this->active = $active;
38+
}
39+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpDocumentor\Guides\Bootstrap\Nodes;
6+
7+
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
8+
use phpDocumentor\Guides\RestructuredText\Nodes\GeneralDirectiveNode;
9+
10+
final class TabsNode extends GeneralDirectiveNode
11+
{
12+
/** @param TabNode[] $tabs */
13+
public function __construct(
14+
protected readonly string $name,
15+
protected readonly string $plainContent,
16+
protected readonly InlineCompoundNode $content,
17+
private readonly string $key,
18+
private array $tabs,
19+
) {
20+
parent::__construct($name, $plainContent, $content, $tabs);
21+
}
22+
23+
/** @return TabNode[] */
24+
public function getTabs(): array
25+
{
26+
return $this->tabs;
27+
}
28+
29+
public function getKey(): string
30+
{
31+
return $this->key;
32+
}
33+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<!DOCTYPE html>
2+
<html class="no-js" lang="en">
3+
<head>
4+
<title>Document Title</title>
5+
<!-- Required meta tags -->
6+
<meta charset="utf-8">
7+
<meta name="viewport" content="width=device-width, initial-scale=1">
8+
9+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
10+
</head>
11+
<body>
12+
<header class="">
13+
</header>
14+
<main id="main-content">
15+
<div class="container">
16+
<div class="container">
17+
<div class="row">
18+
<div class="col-lg-3">
19+
</div>
20+
<div class="col-lg-9">
21+
22+
<nav aria-label="breadcrumb">
23+
<ol class="breadcrumb">
24+
<li class="breadcrumb-item"><a href="/index.html">Document Title</a></li>
25+
</ol>
26+
</nav>
27+
28+
<div class="section" id="document-title">
29+
<h1>Document Title</h1>
30+
31+
<div class="tabs-container ">
32+
<ul class="nav nav-tabs" id="my-tab" role="tablist">
33+
<li class="nav-item" role="presentation">
34+
<button class="nav-link active" id="tab-1-tab" data-bs-toggle="tab" data-bs-target="#tab-1-tab-pane"
35+
type="button" role="tab" aria-controls="tab-1-tab-pane"
36+
aria-selected="true">Tab 1</button>
37+
</li>
38+
<li class="nav-item" role="presentation">
39+
<button class="nav-link" id="tab-2-tab" data-bs-toggle="tab" data-bs-target="#tab-2-tab-pane"
40+
type="button" role="tab" aria-controls="tab-2-tab-pane"
41+
aria-selected="false">Tab 2</button>
42+
</li>
43+
</ul>
44+
<div class="tab-content" id="my-tab-content">
45+
<div class="tab-pane fade show active" id="tab-1-tab-pane" role="tabpanel" aria-labelledby="tab-1-tab" tabindex="0">
46+
<p>Lorem Ipsum Dolor 1</p>
47+
</div>
48+
<div class="tab-pane fade" id="tab-2-tab-pane" role="tabpanel" aria-labelledby="tab-2-tab" tabindex="0">
49+
<p>Lorem Ipsum Dolor 2</p>
50+
</div>
51+
</div>
52+
</div>
53+
54+
<div class="tabs-container ">
55+
<ul class="nav nav-tabs" id="another-tab" role="tablist">
56+
<li class="nav-item" role="presentation">
57+
<button class="nav-link" id="tab-3-tab" data-bs-toggle="tab" data-bs-target="#tab-3-tab-pane"
58+
type="button" role="tab" aria-controls="tab-3-tab-pane"
59+
aria-selected="false">Tab 3</button>
60+
</li>
61+
<li class="nav-item" role="presentation">
62+
<button class="nav-link active" id="lorem-tab" data-bs-toggle="tab" data-bs-target="#lorem-tab-pane"
63+
type="button" role="tab" aria-controls="lorem-tab-pane"
64+
aria-selected="true">Tab 4</button>
65+
</li>
66+
</ul>
67+
<div class="tab-content" id="another-tab-content">
68+
<div class="tab-pane fade" id="tab-3-tab-pane" role="tabpanel" aria-labelledby="tab-3-tab" tabindex="0">
69+
<p>Lorem Ipsum Dolor 3</p>
70+
</div>
71+
<div class="tab-pane fade show active" id="lorem-tab-pane" role="tabpanel" aria-labelledby="lorem-tab" tabindex="0">
72+
<p>Lorem Ipsum Dolor 4</p>
73+
</div>
74+
</div>
75+
</div>
76+
77+
</div>
78+
79+
</div>
80+
</div>
81+
</div>
82+
</div>
83+
</main>
84+
85+
<!-- Optional JavaScript; choose one of the two! -->
86+
87+
<!-- Option 1: Bootstrap Bundle with Popper -->
88+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
89+
90+
<!-- Option 2: Separate Popper and Bootstrap JS -->
91+
<!--
92+
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
93+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
94+
-->
95+
</body>
96+
</html>

0 commit comments

Comments
 (0)