1 <?php
  2 
  3 /**
  4  * Klasse zum Erstellen von Navigationen, v0.1.
  5  *
  6  * @package redaxo\structure
  7  */
  8 
  9 /*
 10  * Beispiel:
 11  *
 12  * UL, LI Navigation von der Rootebene aus,
 13  * 2 Ebenen durchgehen, Alle unternavis offen
 14  * und offline categorien nicht beachten
 15  *
 16  * Navigation:
 17  *
 18  * $nav = rex_navigation::factory();
 19  * $nav->setClasses(array('lev1', 'lev2', 'lev3'));
 20  * $nav->setLinkClasses(array('alev1', 'alev2', 'alev3'));
 21  * echo $nav->get(0,2,TRUE,TRUE);
 22  *
 23  * Sitemap:
 24  *
 25  * $nav = rex_navigation::factory();
 26  * $nav->show(0,-1,TRUE,TRUE);
 27  *
 28  * Breadcrump:
 29  *
 30  * $nav = rex_navigation::factory();
 31  * $nav->showBreadcrumb(true);
 32  */
 33 
 34 class rex_navigation
 35 {
 36     use rex_factory_trait;
 37 
 38     private $depth; // Wieviele Ebene tief, ab der Startebene
 39     private $open; // alles aufgeklappt, z.b. Sitemap
 40     private $path = [];
 41     private $classes = [];
 42     private $linkclasses = [];
 43     private $filter = [];
 44     private $callbacks = [];
 45 
 46     private $current_article_id = -1; // Aktueller Artikel
 47     private $current_category_id = -1; // Aktuelle Katgorie
 48 
 49     private function __construct()
 50     {
 51         // nichts zu tun
 52     }
 53 
 54     /**
 55      * @return static
 56      */
 57     public static function factory()
 58     {
 59         $class = self::getFactoryClass();
 60         return new $class();
 61     }
 62 
 63     /**
 64      * Generiert eine Navigation.
 65      *
 66      * @param int  $category_id     Id der Wurzelkategorie
 67      * @param int  $depth           Anzahl der Ebenen die angezeigt werden sollen
 68      * @param bool $open            True, wenn nur Elemente der aktiven Kategorie angezeigt werden sollen, sonst FALSE
 69      * @param bool $ignore_offlines FALSE, wenn offline Elemente angezeigt werden, sonst TRUE
 70      *
 71      * @return string
 72      */
 73     public function get($category_id = 0, $depth = 3, $open = false, $ignore_offlines = false)
 74     {
 75         if (!$this->_setActivePath()) {
 76             return false;
 77         }
 78 
 79         $this->depth = $depth;
 80         $this->open = $open;
 81         if ($ignore_offlines) {
 82             $this->addFilter('status', 1, '==');
 83         }
 84 
 85         return $this->_getNavigation($category_id);
 86     }
 87 
 88     /**
 89      * @see get()
 90      */
 91     public function show($category_id = 0, $depth = 3, $open = false, $ignore_offlines = false)
 92     {
 93         echo $this->get($category_id, $depth, $open, $ignore_offlines);
 94     }
 95 
 96     /**
 97      * Generiert eine Breadcrumb-Navigation.
 98      *
 99      * @param string $startPageLabel Label der Startseite, falls FALSE keine Start-Page anzeigen
100      * @param bool   $includeCurrent True wenn der aktuelle Artikel enthalten sein soll, sonst FALSE
101      * @param int    $category_id    Id der Wurzelkategorie
102      *
103      * @return string
104      */
105     public function getBreadcrumb($startPageLabel, $includeCurrent = false, $category_id = 0)
106     {
107         if (!$this->_setActivePath()) {
108             return false;
109         }
110 
111         $path = $this->path;
112 
113         $i = 1;
114         $lis = '';
115 
116         if ($startPageLabel) {
117             $lis .= '<li class="rex-lvl' . $i . '"><a href="' . rex_getUrl(rex_article::getSiteStartArticleId()) . '">' . rex_escape($startPageLabel) . '</a></li>';
118             ++$i;
119 
120             // StartArticle nicht doppelt anzeigen
121             if (isset($path[0]) && $path[0] == rex_article::getSiteStartArticleId()) {
122                 unset($path[0]);
123             }
124         }
125 
126         $show = !$category_id;
127         foreach ($path as $pathItem) {
128             if (!$show) {
129                 if ($pathItem == $category_id) {
130                     $show = true;
131                 } else {
132                     continue;
133                 }
134             }
135 
136             $cat = rex_category::get($pathItem);
137             $lis .= '<li class="rex-lvl' . $i . '"><a href="' . $cat->getUrl() . '">' . rex_escape($cat->getName()) . '</a></li>';
138             ++$i;
139         }
140 
141         if ($includeCurrent) {
142             if ($art = rex_article::get($this->current_article_id)) {
143                 if (!$art->isStartArticle()) {
144                     $lis .= '<li class="rex-lvl' . $i . '">' . rex_escape($art->getName()) . '</li>';
145                 }
146             } else {
147                 $cat = rex_category::get($this->current_article_id);
148                 $lis .= '<li class="rex-lvl' . $i . '">' . rex_escape($cat->getName()) . '</li>';
149             }
150         }
151 
152         return '<ul class="rex-breadcrumb">' . $lis . '</ul>';
153     }
154 
155     /**
156      * @see getBreadcrumb()
157      */
158     public function showBreadcrumb($startPageLabel = false, $includeCurrent = false, $category_id = 0)
159     {
160         echo $this->getBreadcrumb($startPageLabel, $includeCurrent, $category_id);
161     }
162 
163     public function setClasses($classes)
164     {
165         $this->classes = $classes;
166     }
167 
168     public function setLinkClasses($classes)
169     {
170         $this->linkclasses = $classes;
171     }
172 
173     /**
174      * Fügt einen Filter hinzu.
175      *
176      * @param string     $metafield Datenbankfeld der Kategorie
177      * @param mixed      $value     Wert für den Vergleich
178      * @param string     $type      Art des Vergleichs =/</.
179      * @param int|string $depth     "" wenn auf allen Ebenen, wenn definiert, dann wird der Filter nur auf dieser Ebene angewendet
180      */
181     public function addFilter($metafield = 'id', $value = '1', $type = '=', $depth = '')
182     {
183         $this->filter[] = ['metafield' => $metafield, 'value' => $value, 'type' => $type, 'depth' => $depth];
184     }
185 
186     /**
187      * Fügt einen Callback hinzu.
188      *
189      * @param callable   $callback z.B. myFunc oder myClass::myMethod
190      * @param int|string $depth    "" wenn auf allen Ebenen, wenn definiert, dann wird der Filter nur auf dieser Ebene angewendet
191      */
192     public function addCallback($callback, $depth = '')
193     {
194         if ($callback != '') {
195             $this->callbacks[] = ['callback' => $callback, 'depth' => $depth];
196         }
197     }
198 
199     private function _setActivePath()
200     {
201         $article_id = rex_article::getCurrentId();
202         if ($OOArt = rex_article::get($article_id)) {
203             $path = trim($OOArt->getPath(), '|');
204 
205             $this->path = [];
206             if ($path != '') {
207                 $this->path = explode('|', $path);
208             }
209 
210             $this->current_article_id = $article_id;
211             $this->current_category_id = $OOArt->getCategoryId();
212             return true;
213         }
214 
215         return false;
216     }
217 
218     private function checkFilter(rex_category $category, $depth)
219     {
220         foreach ($this->filter as $f) {
221             if ($f['depth'] == '' || $f['depth'] == $depth) {
222                 $mf = $category->getValue($f['metafield']);
223                 $va = $f['value'];
224                 switch ($f['type']) {
225                     case '<>':
226                     case '!=':
227                         if ($mf == $va) {
228                             return false;
229                         }
230                         break;
231                     case '>':
232                         if ($mf <= $va) {
233                             return false;
234                         }
235                         break;
236                     case '<':
237                         if ($mf >= $va) {
238                             return false;
239                         }
240                         break;
241                     case '=>':
242                     case '>=':
243                         if ($mf < $va) {
244                             return false;
245                         }
246                         break;
247                     case '=<':
248                     case '<=':
249                         if ($mf > $va) {
250                             return false;
251                         }
252                         break;
253                     case 'regex':
254                         if (!preg_match($va, $mf)) {
255                             return false;
256                         }
257                         break;
258                     case '=':
259                     case '==':
260                     default:
261                         // =
262                         if ($mf != $va) {
263                             return false;
264                         }
265                 }
266             }
267         }
268         return true;
269     }
270 
271     private function checkCallbacks(rex_category $category, $depth, &$li, &$a)
272     {
273         foreach ($this->callbacks as $c) {
274             if ($c['depth'] == '' || $c['depth'] == $depth) {
275                 $callback = $c['callback'];
276                 if (is_string($callback)) {
277                     $callback = explode('::', $callback, 2);
278                     if (count($callback) < 2) {
279                         $callback = $callback[0];
280                     }
281                 }
282                 if (is_array($callback) && count($callback) > 1) {
283                     list($class, $method) = $callback;
284                     if (is_object($class)) {
285                         $result = $class->$method($category, $depth, $li, $a);
286                     } else {
287                         $result = $class::$method($category, $depth, $li, $a);
288                     }
289                 } else {
290                     $result = $callback($category, $depth, $li, $a);
291                 }
292                 if (!$result) {
293                     return false;
294                 }
295             }
296         }
297         return true;
298     }
299 
300     protected function _getNavigation($category_id, $depth = 1)
301     {
302         if ($category_id < 1) {
303             $nav_obj = rex_category::getRootCategories();
304         } else {
305             $nav_obj = rex_category::get($category_id)->getChildren();
306         }
307 
308         $lis = [];
309         foreach ($nav_obj as $nav) {
310             $li = [];
311             $a = [];
312             $li['class'] = [];
313             $a['class'] = [];
314             $a['href'] = [$nav->getUrl()];
315             if ($this->checkFilter($nav, $depth) && $this->checkCallbacks($nav, $depth, $li, $a)) {
316                 $li['class'][] = 'rex-article-' . $nav->getId();
317                 // classes abhaengig vom pfad
318                 if ($nav->getId() == $this->current_category_id) {
319                     $li['class'][] = 'rex-current';
320                     $a['class'][] = 'rex-current';
321                 } elseif (in_array($nav->getId(), $this->path)) {
322                     $li['class'][] = 'rex-active';
323                     $a['class'][] = 'rex-active';
324                 } else {
325                     $li['class'][] = 'rex-normal';
326                 }
327                 if (isset($this->linkclasses[($depth - 1)])) {
328                     $a['class'][] = $this->linkclasses[($depth - 1)];
329                 }
330                 if (isset($this->classes[($depth - 1)])) {
331                     $li['class'][] = $this->classes[($depth - 1)];
332                 }
333                 $li_attr = [];
334                 foreach ($li as $attr => $v) {
335                     $li_attr[] = $attr . '="' . implode(' ', $v) . '"';
336                 }
337                 $a_attr = [];
338                 foreach ($a as $attr => $v) {
339                     $a_attr[] = $attr . '="' . implode(' ', $v) . '"';
340                 }
341                 $l = '<li ' . implode(' ', $li_attr) . '>';
342                 $l .= '<a ' . implode(' ', $a_attr) . '>' . rex_escape($nav->getName()) . '</a>';
343                 ++$depth;
344                 if (($this->open ||
345                         $nav->getId() == $this->current_category_id ||
346                         in_array($nav->getId(), $this->path))
347                     && ($this->depth >= $depth || $this->depth < 0)
348                 ) {
349                     $l .= $this->_getNavigation($nav->getId(), $depth);
350                 }
351                 --$depth;
352                 $l .= '</li>';
353                 $lis[] = $l;
354             }
355         }
356         if (count($lis) > 0) {
357             return '<ul class="rex-navi' . $depth . ' rex-navi-depth-' . $depth . ' rex-navi-has-' . count($lis) . '-elements">' . implode('', $lis) . '</ul>';
358         }
359         return '';
360     }
361 }
362