1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Hyde\Framework\Features\Navigation; |
6
|
|
|
|
7
|
|
|
use Hyde\Hyde; |
8
|
|
|
use Hyde\Facades\Config; |
9
|
|
|
use Hyde\Support\Models\Route; |
10
|
|
|
use Hyde\Pages\DocumentationPage; |
11
|
|
|
use Illuminate\Support\Collection; |
12
|
|
|
use Hyde\Foundation\Facades\Routes; |
13
|
|
|
use Hyde\Foundation\Kernel\RouteCollection; |
14
|
|
|
use Hyde\Framework\Exceptions\InvalidConfigurationException; |
15
|
|
|
|
16
|
|
|
use function filled; |
17
|
|
|
use function assert; |
18
|
|
|
use function collect; |
19
|
|
|
use function in_array; |
20
|
|
|
use function strtolower; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @experimental This class may change significantly before its release. |
24
|
|
|
*/ |
25
|
|
|
class NavigationMenuGenerator |
26
|
|
|
{ |
27
|
|
|
/** @var \Illuminate\Support\Collection<string, \Hyde\Framework\Features\Navigation\NavigationItem|\Hyde\Framework\Features\Navigation\NavigationGroup> */ |
28
|
|
|
protected Collection $items; |
29
|
|
|
|
30
|
|
|
/** @var \Hyde\Foundation\Kernel\RouteCollection<string, \Hyde\Support\Models\Route> */ |
31
|
|
|
protected RouteCollection $routes; |
32
|
|
|
|
33
|
|
|
/** @var class-string<\Hyde\Framework\Features\Navigation\NavigationMenu> */ |
|
|
|
|
34
|
|
|
protected string $menuType; |
35
|
|
|
|
36
|
|
|
protected bool $generatesSidebar; |
37
|
|
|
protected bool $usesGroups; |
38
|
|
|
|
39
|
|
|
/** @param class-string<\Hyde\Framework\Features\Navigation\NavigationMenu> $menuType */ |
|
|
|
|
40
|
|
|
protected function __construct(string $menuType) |
41
|
|
|
{ |
42
|
|
|
assert(in_array($menuType, [MainNavigationMenu::class, DocumentationSidebar::class])); |
43
|
|
|
|
44
|
|
|
$this->menuType = $menuType; |
45
|
|
|
|
46
|
|
|
$this->items = new Collection(); |
47
|
|
|
|
48
|
|
|
$this->generatesSidebar = $menuType === DocumentationSidebar::class; |
49
|
|
|
|
50
|
|
|
$this->routes = $this->generatesSidebar |
51
|
|
|
? Routes::getRoutes(DocumentationPage::class) |
|
|
|
|
52
|
|
|
: Routes::all(); |
53
|
|
|
|
54
|
|
|
$this->usesGroups = $this->usesGroups(); |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
/** @param class-string<\Hyde\Framework\Features\Navigation\NavigationMenu> $menuType */ |
|
|
|
|
58
|
|
|
public static function handle(string $menuType): MainNavigationMenu|DocumentationSidebar |
59
|
|
|
{ |
60
|
|
|
$menu = new static($menuType); |
61
|
|
|
|
62
|
|
|
$menu->generate(); |
63
|
|
|
|
64
|
|
|
return new $menuType($menu->items); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
protected function generate(): void |
68
|
|
|
{ |
69
|
|
|
$this->routes->each(function (Route $route): void { |
70
|
|
|
if ($this->canAddRoute($route)) { |
71
|
|
|
if ($this->canGroupRoute($route)) { |
72
|
|
|
$this->addRouteToGroup($route); |
73
|
|
|
} else { |
74
|
|
|
$this->items->put($route->getRouteKey(), NavigationItem::create($route)); |
|
|
|
|
75
|
|
|
} |
76
|
|
|
} |
77
|
|
|
}); |
78
|
|
|
|
79
|
|
|
if ($this->generatesSidebar) { |
80
|
|
|
// If there are no pages other than the index page, we add it to the sidebar so that it's not empty |
81
|
|
|
if ($this->items->count() === 0 && DocumentationPage::home() !== null) { |
82
|
|
|
$this->items->push(NavigationItem::create(DocumentationPage::home())); |
|
|
|
|
83
|
|
|
} |
84
|
|
|
} else { |
85
|
|
|
collect(Config::getArray('hyde.navigation.custom', []))->each(function (array $data): void { |
|
|
|
|
86
|
|
|
/** @var array{destination: string, label: ?string, priority: ?int, attributes: array<string, scalar>} $data */ |
87
|
|
|
$message = 'Invalid navigation item configuration detected the configuration file. Please double check the syntax.'; |
88
|
|
|
$item = InvalidConfigurationException::try(fn () => NavigationItem::create(...$data), $message); |
|
|
|
|
89
|
|
|
|
90
|
|
|
// Since these were added explicitly by the user, we can assume they should always be shown |
91
|
|
|
$this->items->push($item); |
92
|
|
|
}); |
93
|
|
|
} |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
protected function usesGroups(): bool |
97
|
|
|
{ |
98
|
|
|
if ($this->generatesSidebar) { |
99
|
|
|
// In order to know if we should use groups in the sidebar, we need to loop through the pages and see if they have a group set. |
100
|
|
|
// This automatically enables the sidebar grouping for all pages if at least one group is set. |
101
|
|
|
|
102
|
|
|
return $this->routes->first(fn (Route $route): bool => filled($route->getPage()->navigationMenuGroup())) !== null; |
103
|
|
|
} else { |
104
|
|
|
return Config::getString('hyde.navigation.subdirectory_display', 'hidden') === 'dropdown'; |
105
|
|
|
} |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
protected function canAddRoute(Route $route): bool |
109
|
|
|
{ |
110
|
|
|
if (! $route->getPage()->showInNavigation()) { |
111
|
|
|
return false; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
if ($this->generatesSidebar) { |
115
|
|
|
// Since the index page is linked in the header, we don't want it in the sidebar |
116
|
|
|
return ! $route->is(DocumentationPage::homeRouteName()); |
117
|
|
|
} else { |
118
|
|
|
// While we for the most part can rely on the navigation visibility state provided by the navigation data factory, |
119
|
|
|
// we need to make an exception for documentation pages, which generally have a visible state, as the data is |
120
|
|
|
// also used in the sidebar. But we only want the documentation index page to be in the main navigation. |
121
|
|
|
return ! $route->getPage() instanceof DocumentationPage || $route->is(DocumentationPage::homeRouteName()); |
122
|
|
|
} |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
protected function canGroupRoute(Route $route): bool |
126
|
|
|
{ |
127
|
|
|
if (! $this->generatesSidebar) { |
128
|
|
|
return $route->getPage()->navigationMenuGroup() !== null; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
if (! $this->usesGroups) { |
132
|
|
|
return false; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
return true; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
protected function addRouteToGroup(Route $route): void |
139
|
|
|
{ |
140
|
|
|
$item = NavigationItem::create($route); |
141
|
|
|
|
142
|
|
|
$groupKey = $item->getPage()->navigationMenuGroup(); |
143
|
|
|
$groupName = $this->generatesSidebar ? ($groupKey ?? 'Other') : $groupKey; |
144
|
|
|
|
145
|
|
|
$groupItem = $this->getOrCreateGroupItem($groupName); |
|
|
|
|
146
|
|
|
|
147
|
|
|
$groupItem->add($item); |
148
|
|
|
|
149
|
|
|
if (! $this->items->has($groupItem->getGroupKey())) { |
150
|
|
|
$this->items->put($groupItem->getGroupKey(), $groupItem); |
|
|
|
|
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
protected function getOrCreateGroupItem(string $groupName): NavigationGroup |
155
|
|
|
{ |
156
|
|
|
$groupKey = NavigationGroup::normalizeGroupKey($groupName); |
157
|
|
|
$group = $this->items->get($groupKey); |
158
|
|
|
|
159
|
|
|
if ($group instanceof NavigationGroup) { |
160
|
|
|
return $group; |
161
|
|
|
} elseif ($group instanceof NavigationItem) { |
162
|
|
|
// We are trying to add children to an existing navigation menu item, |
163
|
|
|
// so here we create a new instance to replace the base one, this |
164
|
|
|
// does mean we lose the destination as we can't link to them. |
165
|
|
|
|
166
|
|
|
$item = new NavigationGroup($group->getLabel(), [], $group->getPriority()); |
167
|
|
|
|
168
|
|
|
$this->items->put($groupKey, $item); |
|
|
|
|
169
|
|
|
|
170
|
|
|
return $item; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
return $this->createGroupItem($groupKey, $groupName); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
protected function createGroupItem(string $groupKey, string $groupName): NavigationGroup |
177
|
|
|
{ |
178
|
|
|
$label = $this->searchForGroupLabelInConfig($groupKey) ?? $groupName; |
179
|
|
|
|
180
|
|
|
$priority = $this->searchForGroupPriorityInConfig($groupKey); |
181
|
|
|
|
182
|
|
|
return NavigationGroup::create($this->normalizeGroupLabel($label), [], $priority ?? NavigationMenu::LAST); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
protected function normalizeGroupLabel(string $label): string |
186
|
|
|
{ |
187
|
|
|
// If there is no label, and the group is a slug, we can make a title from it |
188
|
|
|
if ($label === strtolower($label)) { |
189
|
|
|
return Hyde::makeTitle($label); |
|
|
|
|
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
return $label; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
protected function searchForGroupLabelInConfig(string $groupKey): ?string |
196
|
|
|
{ |
197
|
|
|
// TODO: Normalize this: sidebar_group_labels -> docs.sidebar.labels |
198
|
|
|
return $this->getConfigArray($this->generatesSidebar ? 'docs.sidebar_group_labels' : 'hyde.navigation.labels')[$groupKey] ?? null; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
protected function searchForGroupPriorityInConfig(string $groupKey): ?int |
202
|
|
|
{ |
203
|
|
|
return $this->getConfigArray($this->generatesSidebar ? 'docs.sidebar.order' : 'hyde.navigation.order')[$groupKey] ?? null; |
|
|
|
|
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** @return array<string|int, string|int> */ |
207
|
|
|
protected function getConfigArray(string $key): array |
208
|
|
|
{ |
209
|
|
|
/** @var array<string|int, string|int> $array */ |
210
|
|
|
$array = Config::getArray($key, []); |
211
|
|
|
|
212
|
|
|
return $array; |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
|