From 47eab3e7235d12724d02608e79b431e7129144ce Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Wed, 2 Sep 2026 12:35:06 +0600 Subject: [PATCH 1/3] feat(templates): Expose row flags for menu, page, crumbs One tpl can branch on isActive/hasChildren instead of many tpl* chunks. Specialized templates stay. Menu state drives classes, tpl pick, and placeholders from the same facts. Fixes #11 --- core/components/pdotools/docs/changelog.txt | 1 + .../elements/snippets/snippet.pdocrumbs.php | 25 ++- .../elements/snippets/snippet.pdopage.php | 28 ++- .../pdotools/src/Support/CrumbItemState.php | 42 ++++ .../pdotools/src/Support/MenuBuilder.php | 92 ++++----- .../pdotools/src/Support/MenuItemState.php | 188 ++++++++++++++++++ .../pdotools/src/Support/PageItemState.php | 66 ++++++ .../pdotools/src/Support/Paginator.php | 150 +++++++------- .../pdotools/src/Support/TemplateFlags.php | 26 +++ .../tests/Unit/Support/CrumbItemStateTest.php | 24 +++ .../tests/Unit/Support/MenuItemStateTest.php | 130 ++++++++++++ .../tests/Unit/Support/PageItemStateTest.php | 45 +++++ .../tests/Unit/Support/TemplateFlagsTest.php | 33 +++ 13 files changed, 710 insertions(+), 140 deletions(-) create mode 100644 core/components/pdotools/src/Support/CrumbItemState.php create mode 100644 core/components/pdotools/src/Support/MenuItemState.php create mode 100644 core/components/pdotools/src/Support/PageItemState.php create mode 100644 core/components/pdotools/src/Support/TemplateFlags.php create mode 100644 core/components/pdotools/tests/Unit/Support/CrumbItemStateTest.php create mode 100644 core/components/pdotools/tests/Unit/Support/MenuItemStateTest.php create mode 100644 core/components/pdotools/tests/Unit/Support/PageItemStateTest.php create mode 100644 core/components/pdotools/tests/Unit/Support/TemplateFlagsTest.php diff --git a/core/components/pdotools/docs/changelog.txt b/core/components/pdotools/docs/changelog.txt index 0f4bff4..8420d4e 100644 --- a/core/components/pdotools/docs/changelog.txt +++ b/core/components/pdotools/docs/changelog.txt @@ -9,6 +9,7 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) - PHPUnit 9.6 harness (Unit + Integration) and GitHub Actions on PHP 8.1, 8.2, 8.3, 8.4. - Codecov PR comments in informational mode, same as MODX Revolution. +- [pdoTools3#11] [pdoMenu/pdoPage/pdoCrumbs] Template rows expose isFirst, isLast, isActive, hasChildren/hasChilds (menu also isHere, isStart, isCategory, isInner; page isSkip; crumbs isHome). Specialized tpl* chunks still work. - [#306] [pdoMenu] Added the "children" placeholder to tplInner. - [#355] [Fenom] Added type cast modifiers: boolval, doubleval, floatval, intval, strval. - [#356] [Fenom] Enabled the array_merge modifier by default. diff --git a/core/components/pdotools/elements/snippets/snippet.pdocrumbs.php b/core/components/pdotools/elements/snippets/snippet.pdocrumbs.php index 4fdf909..72fb349 100644 --- a/core/components/pdotools/elements/snippets/snippet.pdocrumbs.php +++ b/core/components/pdotools/elements/snippets/snippet.pdocrumbs.php @@ -1,6 +1,7 @@ id && empty($showCurrent)) { continue; } elseif ($row['id'] == $resource->id && !empty($tplCurrent)) { - $tpl = $tplCurrent; + $row['_tpl'] = $tplCurrent; } elseif ($row['id'] == $siteStart && !empty($tplHome)) { - $tpl = $tplHome; + $row['_tpl'] = $tplHome; } else { - $tpl = $pdoFetch->defineChunk($row); + $row['_tpl'] = $pdoFetch->defineChunk($row); + } + $prepared[] = $row; + } + + $total = count($prepared); + foreach ($prepared as $index => $row) { + $row = array_merge( + $row, + CrumbItemState::placeholders($row['id'], $resource->id, $siteStart, $index, $total) + ); + if (isset($return) && $return === 'data') { + $output[] = $row; + continue; } + $tpl = $row['_tpl'] ?? ''; + unset($row['_tpl']); $output[] = empty($tpl) ? '
' . $pdoFetch->getChunk('', $row) . '
' : $pdoFetch->getChunk($tpl, $row, $fastMode); diff --git a/core/components/pdotools/elements/snippets/snippet.pdopage.php b/core/components/pdotools/elements/snippets/snippet.pdopage.php index f4bd8f9..a76d257 100644 --- a/core/components/pdotools/elements/snippets/snippet.pdopage.php +++ b/core/components/pdotools/elements/snippets/snippet.pdopage.php @@ -160,19 +160,39 @@ if (!empty($pageCount) && $pageCount > 1) { $pagination = [ 'first' => $page > 1 && !empty($tplPageFirst) - ? $paginator->makePageLink($url, 1, $tplPageFirst) + ? $paginator->makePageLink( + $url, + 1, + $tplPageFirst, + \ModxPro\PdoTools\Support\PageItemState::placeholders(1, $page, $pageCount) + ) : '', 'prev' => $page > 1 && !empty($tplPagePrev) - ? $paginator->makePageLink($url, $page - 1, $tplPagePrev) + ? $paginator->makePageLink( + $url, + $page - 1, + $tplPagePrev, + \ModxPro\PdoTools\Support\PageItemState::placeholders($page - 1, $page, $pageCount) + ) : '', 'pages' => $pageLimit >= 7 && empty($disableModernPagination) ? $paginator->buildModernPagination($page, $pageCount, $url) : $paginator->buildClassicPagination($page, $pageCount, $url), 'next' => $page < $pageCount && !empty($tplPageNext) - ? $paginator->makePageLink($url, $page + 1, $tplPageNext) + ? $paginator->makePageLink( + $url, + $page + 1, + $tplPageNext, + \ModxPro\PdoTools\Support\PageItemState::placeholders($page + 1, $page, $pageCount) + ) : '', 'last' => $page < $pageCount && !empty($tplPageLast) - ? $paginator->makePageLink($url, $pageCount, $tplPageLast) + ? $paginator->makePageLink( + $url, + $pageCount, + $tplPageLast, + \ModxPro\PdoTools\Support\PageItemState::placeholders($pageCount, $page, $pageCount) + ) : '', ]; diff --git a/core/components/pdotools/src/Support/CrumbItemState.php b/core/components/pdotools/src/Support/CrumbItemState.php new file mode 100644 index 0000000..a78f235 --- /dev/null +++ b/core/components/pdotools/src/Support/CrumbItemState.php @@ -0,0 +1,42 @@ + + */ + public static function boolFlags($id, $currentId, $siteStart, $index, $total) + { + return [ + 'isFirst' => (int)$index === 0, + 'isLast' => $total > 0 && (int)$index === (int)$total - 1, + 'isActive' => (int)$id === (int)$currentId, + 'isHome' => (int)$id === (int)$siteStart, + ]; + } + + /** + * @param int $id + * @param int $currentId + * @param int $siteStart + * @param int $index + * @param int $total + * @return array + */ + public static function placeholders($id, $currentId, $siteStart, $index, $total) + { + return TemplateFlags::toPlaceholders( + self::boolFlags($id, $currentId, $siteStart, $index, $total) + ); + } +} diff --git a/core/components/pdotools/src/Support/MenuBuilder.php b/core/components/pdotools/src/Support/MenuBuilder.php index f2c3ab0..c693bb0 100644 --- a/core/components/pdotools/src/Support/MenuBuilder.php +++ b/core/components/pdotools/src/Support/MenuBuilder.php @@ -180,7 +180,8 @@ public function templateBranch($row = []) $row['menutitle'] = $row['pagetitle']; } - $classes = $this->getClasses($row); + $state = $this->itemState($row); + $classes = $state->classes($this->pdoTools->config()); if (!empty($classes)) { $row['classNames'] = $row['classnames'] = $classes; $row['classes'] = ' class="' . $classes . '"'; @@ -201,7 +202,8 @@ public function templateBranch($row = []) ? $row[$this->pdoTools->config('titleOfLinks')] : $row['pagetitle']; - $tpl = $this->getTpl($row); + $row = array_merge($row, $state->placeholders()); + $tpl = $this->tplFromState($state, $row); $row = $this->addWayFinderPlaceholders($row); return $this->pdoTools->getChunk($tpl, $row, $this->pdoTools->config('fastMode')); @@ -221,6 +223,21 @@ public function isHere($id = 0) } + /** + * @param array $row + * @return MenuItemState + */ + public function itemState(array $row = []) + { + return MenuItemState::fromRow( + $row, + $this->pdoTools->config(), + function ($id) { + return $this->isHere($id); + } + ); + } + /** * Determine style class for current item being processed * @@ -230,41 +247,12 @@ public function isHere($id = 0) */ public function getClasses($row = []) { - $classes = []; - - if (!empty($this->pdoTools->config('rowClass'))) { - $classes[] = $this->pdoTools->config('rowClass'); - } - if ($row['idx'] == 1 && !empty($this->pdoTools->config('firstClass'))) { - $classes[] = $this->pdoTools->config('firstClass'); - } elseif (!empty($row['last']) && !empty($this->pdoTools->config('lastClass'))) { - $classes[] = $this->pdoTools->config('lastClass'); - } - if (!empty($this->pdoTools->config('levelClass'))) { - $classes[] = $this->pdoTools->config('levelClass') . $row['level']; - } - if ($row['children'] && !empty($this->pdoTools->config('parentClass')) && ($row['level'] < $this->pdoTools->config('level') || empty($this->pdoTools->config('level')))) { - $classes[] = $this->pdoTools->config('parentClass'); - } - $row_id = !empty($this->pdoTools->config('useWeblinkUrl')) && !empty($row['content']) && !empty($row['class_key']) && is_numeric(trim($row['content'], '[]~ ')) && $row['class_key'] == modWebLink::class - ? (int)trim($row['content'], '[]~ ') - : $row['id']; - if ($this->isHere($row_id) && !empty($this->pdoTools->config('hereClass'))) { - $classes[] = $this->pdoTools->config('hereClass'); - } - if ($row_id == $this->pdoTools->config('hereId') && !empty($this->pdoTools->config('selfClass'))) { - $classes[] = $this->pdoTools->config('selfClass'); - } - if (!empty($row['class_key']) && $row['class_key'] === modWebLink::class && !empty($this->pdoTools->config('webLinkClass'))) { - $classes[] = $this->pdoTools->config('webLinkClass'); - } - - return implode(' ', $classes); + return $this->itemState($row)->classes($this->pdoTools->config()); } /** - * Determine style class for current item being processed + * Chunk name for the current menu row. * * @param array $row * @@ -272,34 +260,22 @@ public function getClasses($row = []) */ public function getTpl($row = []) { - $row_id = !empty($this->pdoTools->config('useWeblinkUrl')) && !empty($row['class_key']) && !empty($row['content']) && $row['class_key'] === modWebLink::class && is_numeric(trim($row['content'], '[]~ ')) - ? (int)trim($row['content'], '[]~ ') - : $row['id']; - if ($row['level'] === 1 && !empty($this->pdoTools->config('tplStart')) && !empty($this->pdoTools->config('displayStart'))) { - $tpl = 'tplStart'; - } elseif ($row['children'] && $row_id == $this->pdoTools->config('hereId') && !empty($this->pdoTools->config('tplParentRowHere'))) { - $tpl = 'tplParentRowHere'; - } elseif ($row['level'] > 1 && $row_id == $this->pdoTools->config('hereId') && !empty($this->pdoTools->config('tplInnerHere'))) { - $tpl = 'tplInnerHere'; - } elseif ($row_id == $this->pdoTools->config('hereId') && !empty($this->pdoTools->config('tplHere'))) { - $tpl = 'tplHere'; - } elseif ($row['children'] && $this->isHere($row_id) && !empty($this->pdoTools->config('tplParentRowActive'))) { - $tpl = 'tplParentRowActive'; - } elseif ($row['children'] && (empty($row['template']) || strpos($row['link_attributes'], 'category') != false) && !empty($this->pdoTools->config('tplCategoryFolder'))) { - $tpl = 'tplCategoryFolder'; - } // It's a typo, but it is left for backward compatibility - elseif ($row['children'] && (empty($row['template']) || strpos($row['link_attributes'], 'category') != false) && !empty($this->pdoTools->config('tplCategoryFolders'))) { - $tpl = 'tplCategoryFolders'; - } // --- - elseif ($row['children'] && !empty($this->pdoTools->config('tplParentRow'))) { - $tpl = 'tplParentRow'; - } elseif ($row['level'] > 1 && !empty($this->pdoTools->config('tplInnerRow'))) { - $tpl = 'tplInnerRow'; - } else { + return $this->tplFromState($this->itemState($row), $row); + } + + /** + * @param MenuItemState $state + * @param array $row + * @return mixed + */ + protected function tplFromState(MenuItemState $state, array $row) + { + $key = $state->tplKey($this->pdoTools->config()); + if ($key === null) { return $this->pdoTools->defineChunk($row); } - return $this->pdoTools->config($tpl); + return $this->pdoTools->config($key); } diff --git a/core/components/pdotools/src/Support/MenuItemState.php b/core/components/pdotools/src/Support/MenuItemState.php new file mode 100644 index 0000000..0de667a --- /dev/null +++ b/core/components/pdotools/src/Support/MenuItemState.php @@ -0,0 +1,188 @@ +row = $row; + $state->level = (int)($row['level'] ?? 1); + $state->rowId = self::resolveRowId($row, $config); + $state->isFirst = isset($row['idx']) && (int)$row['idx'] === 1; + $state->isLast = !empty($row['last']); + $state->hasChildren = !empty($row['children']); + $state->isActive = $state->rowId == ($config['hereId'] ?? 0); + $state->isHere = (bool)$isHere($state->rowId); + $state->isStart = $state->level === 1 && !empty($config['displayStart']); + $state->isInner = $state->level > 1; + $state->isWebLink = !empty($row['class_key']) && $row['class_key'] === modWebLink::class; + $state->isCategory = $state->hasChildren && ( + empty($row['template']) + || (isset($row['link_attributes']) && strpos((string)$row['link_attributes'], 'category') !== false) + ); + + return $state; + } + + /** + * @param array $row + * @param array $config + * @return int + */ + public static function resolveRowId(array $row, array $config) + { + if ( + !empty($config['useWeblinkUrl']) + && !empty($row['class_key']) + && !empty($row['content']) + && $row['class_key'] === modWebLink::class + && is_numeric(trim($row['content'], '[]~ ')) + ) { + return (int)trim($row['content'], '[]~ '); + } + + return (int)($row['id'] ?? 0); + } + + /** + * @return array + */ + public function boolFlags() + { + return [ + 'isFirst' => $this->isFirst, + 'isLast' => $this->isLast, + 'isActive' => $this->isActive, + 'hasChildren' => $this->hasChildren, + 'isHere' => $this->isHere, + 'isStart' => $this->isStart, + 'isCategory' => $this->isCategory, + 'isInner' => $this->isInner, + ]; + } + + /** + * @return array + */ + public function placeholders() + { + return TemplateFlags::toPlaceholders($this->boolFlags()); + } + + /** + * @param array $config + * @return string + */ + public function classes(array $config) + { + $classes = []; + + if (!empty($config['rowClass'])) { + $classes[] = $config['rowClass']; + } + if ($this->isFirst && !empty($config['firstClass'])) { + $classes[] = $config['firstClass']; + } elseif ($this->isLast && !empty($config['lastClass'])) { + $classes[] = $config['lastClass']; + } + if (!empty($config['levelClass'])) { + $classes[] = $config['levelClass'] . $this->level; + } + if ( + $this->hasChildren + && !empty($config['parentClass']) + && ($this->level < ($config['level'] ?? 0) || empty($config['level'])) + ) { + $classes[] = $config['parentClass']; + } + if ($this->isHere && !empty($config['hereClass'])) { + $classes[] = $config['hereClass']; + } + if ($this->isActive && !empty($config['selfClass'])) { + $classes[] = $config['selfClass']; + } + if ($this->isWebLink && !empty($config['webLinkClass'])) { + $classes[] = $config['webLinkClass']; + } + + return implode(' ', $classes); + } + + /** + * Config key for a specialized tpl*, or null to fall back to defineChunk(). + * + * @param array $config + * @return string|null + */ + public function tplKey(array $config) + { + if ($this->isStart && !empty($config['tplStart'])) { + return 'tplStart'; + } + if ($this->hasChildren && $this->isActive && !empty($config['tplParentRowHere'])) { + return 'tplParentRowHere'; + } + if ($this->isInner && $this->isActive && !empty($config['tplInnerHere'])) { + return 'tplInnerHere'; + } + if ($this->isActive && !empty($config['tplHere'])) { + return 'tplHere'; + } + if ($this->hasChildren && $this->isHere && !empty($config['tplParentRowActive'])) { + return 'tplParentRowActive'; + } + if ($this->isCategory && !empty($config['tplCategoryFolder'])) { + return 'tplCategoryFolder'; + } + // Typo kept for backward compatibility + if ($this->isCategory && !empty($config['tplCategoryFolders'])) { + return 'tplCategoryFolders'; + } + if ($this->hasChildren && !empty($config['tplParentRow'])) { + return 'tplParentRow'; + } + if ($this->isInner && !empty($config['tplInnerRow'])) { + return 'tplInnerRow'; + } + + return null; + } +} diff --git a/core/components/pdotools/src/Support/PageItemState.php b/core/components/pdotools/src/Support/PageItemState.php new file mode 100644 index 0000000..8304978 --- /dev/null +++ b/core/components/pdotools/src/Support/PageItemState.php @@ -0,0 +1,66 @@ + + */ + public static function boolFlags($page, $current, $pages, $skip = false) + { + $page = (int)$page; + $current = (int)$current; + $pages = (int)$pages; + + return [ + 'isFirst' => $page === 1, + 'isLast' => $pages > 0 && $page === $pages, + 'isActive' => !$skip && $page === $current, + 'isSkip' => (bool)$skip, + ]; + } + + /** + * @param int $page + * @param int $current + * @param int $pages + * @param bool $skip + * @return array + */ + public static function placeholders($page, $current, $pages, $skip = false) + { + return TemplateFlags::toPlaceholders(self::boolFlags($page, $current, $pages, $skip)); + } + + /** + * Pick tplPageActive / tplPage / tplPageSkip from config. + * + * @param array $config + * @param int $page + * @param int $current + * @param bool $skip + * @return string + */ + public static function tpl(array $config, $page, $current, $skip = false) + { + if ($skip && !empty($config['tplPageSkip'])) { + return (string)$config['tplPageSkip']; + } + if ((int)$page === (int)$current && !empty($config['tplPageActive'])) { + return (string)$config['tplPageActive']; + } + if (!empty($config['tplPage'])) { + return (string)$config['tplPage']; + } + + return ''; + } +} diff --git a/core/components/pdotools/src/Support/Paginator.php b/core/components/pdotools/src/Support/Paginator.php index 00ee90c..a496584 100644 --- a/core/components/pdotools/src/Support/Paginator.php +++ b/core/components/pdotools/src/Support/Paginator.php @@ -187,10 +187,11 @@ public function getBaseUrl() * @param string $url * @param int $page * @param string $tpl + * @param array $flags Extra placeholders (isActive, isFirst, …) * * @return string $href */ - public function makePageLink($url = '', $page = 1, $tpl = '') + public function makePageLink($url = '', $page = 1, $tpl = '', array $flags = []) { if (empty($url)) { $url = $this->getBaseUrl(); @@ -239,17 +240,75 @@ public function makePageLink($url = '', $page = 1, $tpl = '') $href = preg_replace("/&(?!amp;)/", "&", $href); } - $data = [ + $data = array_merge([ 'page' => $page, 'pageNo' => $page, 'href' => $href, - ]; + ], $flags); return !empty($tpl) ? $this->pdoTools->getChunk($tpl, $data) : $href; } + /** + * Template and flags for one page number (or skip) slot. + * + * @param int $page + * @param int $current + * @param int $pages + * @param bool $skip + * @return array{0:string,1:array} + */ + public function pageItemTplAndFlags($page, $current, $pages, $skip = false) + { + $config = $this->pdoTools->config(); + $tpl = PageItemState::tpl($config, $page, $current, $skip); + $flags = PageItemState::placeholders($page, $current, $pages, $skip); + + return [$tpl, $flags]; + } + + /** + * @param string $url + * @param int $page Slot number + * @param int $current Current page + * @param int $pages Total pages + * @param bool $skip + * @return string + */ + protected function renderPageItem($url, $page, $current, $pages, $skip = false) + { + [$tpl, $flags] = $this->pageItemTplAndFlags($page, $current, $pages, $skip); + if (empty($tpl)) { + return ''; + } + if ($skip && empty($this->pdoTools->config('tplPageSkip'))) { + return ''; + } + + return $this->makePageLink($url, $page, $tpl, $flags); + } + + /** + * Skip chunk without a page href (modern pagination separators). + * + * @param int $pages + * @return string + */ + protected function renderPageSkip($pages = 0) + { + $tpl = $this->pdoTools->config('tplPageSkip'); + if (empty($tpl)) { + return ''; + } + + return $this->pdoTools->getChunk( + $tpl, + PageItemState::placeholders(0, 0, $pages, true) + ); + } + /** * Creates the correct URL of the page, including folder subdomains. * @see https://github.com/modx-pro/pdoTools/issues/318 @@ -316,16 +375,7 @@ public function buildClassicPagination($page = 1, $pages = 5, $url = '') break; } - if ($page == $i && !empty($this->pdoTools->config('tplPageActive'))) { - $tpl = $this->pdoTools->config('tplPageActive'); - } elseif (!empty($this->pdoTools->config('tplPage'))) { - $tpl = $this->pdoTools->config('tplPage'); - } - - $pagination .= !empty($tpl) - ? $this->makePageLink($url, $i, $tpl) - : ''; - + $pagination .= $this->renderPageItem($url, $i, $page, $pages); $i++; } @@ -357,112 +407,64 @@ public function buildModernPagination($page = 1, $pages = 5, $url = '') $pagination = []; // Left for ($i = 1; $i <= $left; $i++) { - if ($page == $i && !empty($this->pdoTools->config('tplPageActive'))) { - $tpl = $this->pdoTools->config('tplPageActive'); - } elseif (!empty($this->pdoTools->config('tplPage'))) { - $tpl = $this->pdoTools->config('tplPage'); - } - $pagination[$i] = !empty($tpl) - ? $this->makePageLink($url, $i, $tpl) - : ''; + $pagination[$i] = $this->renderPageItem($url, $i, $page, $pages); } // Right for ($i = $pages - $right + 1; $i <= $pages; $i++) { - if ($page == $i && !empty($this->pdoTools->config('tplPageActive'))) { - $tpl = $this->pdoTools->config('tplPageActive'); - } elseif (!empty($this->pdoTools->config('tplPage'))) { - $tpl = $this->pdoTools->config('tplPage'); - } - $pagination[$i] = !empty($tpl) - ? $this->makePageLink($url, $i, $tpl) - : ''; + $pagination[$i] = $this->renderPageItem($url, $i, $page, $pages); } // Center if ($page <= $left) { $i = $left + 1; while ($i <= $center + $left) { - if ($i == $center + $left && !empty($this->pdoTools->config('tplPageSkip'))) { - $tpl = $this->pdoTools->config('tplPageSkip'); - } else { - $tpl = $this->pdoTools->config('tplPage'); - } - - $pagination[$i] = !empty($tpl) - ? $this->makePageLink($url, $i, $tpl) - : ''; + $skip = $i == $center + $left && !empty($this->pdoTools->config('tplPageSkip')); + $pagination[$i] = $this->renderPageItem($url, $i, $page, $pages, $skip); $i++; } } elseif ($page > $pages - $right) { $i = $pages - $right - $center + 1; while ($i <= $pages - $right) { - if ($i == $pages - $right - $center + 1 && !empty($this->pdoTools->config('tplPageSkip'))) { - $tpl = $this->pdoTools->config('tplPageSkip'); - } else { - $tpl = $this->pdoTools->config('tplPage'); - } - - $pagination[$i] = !empty($tpl) - ? $this->makePageLink($url, $i, $tpl) - : ''; + $skip = $i == $pages - $right - $center + 1 && !empty($this->pdoTools->config('tplPageSkip')); + $pagination[$i] = $this->renderPageItem($url, $i, $page, $pages, $skip); $i++; } } else { if ($page - $center < $left) { $i = $left + 1; while ($i <= $center + $left) { - if ($page == $i && !empty($this->pdoTools->config('tplPageActive'))) { - $tpl = $this->pdoTools->config('tplPageActive'); - } elseif (!empty($this->pdoTools->config('tplPage'))) { - $tpl = $this->pdoTools->config('tplPage'); - } - $pagination[$i] = !empty($tpl) - ? $this->makePageLink($url, $i, $tpl) - : ''; + $pagination[$i] = $this->renderPageItem($url, $i, $page, $pages); $i++; } if (!empty($this->pdoTools->config('tplPageSkip'))) { $key = ($page + 1 == $left + $center) ? $pages - $right + 1 : $left + $center; - $pagination[$key] = $this->pdoTools->getChunk($this->pdoTools->config('tplPageSkip')); + $pagination[$key] = $this->renderPageSkip($pages); } } elseif ($page + $center - 1 > $pages - $right) { $i = $pages - $right - $center + 1; while ($i <= $pages - $right) { - if ($page === $i && !empty($this->pdoTools->config('tplPageActive'))) { - $tpl = $this->pdoTools->config('tplPageActive'); - } elseif (!empty($this->pdoTools->config('tplPage'))) { - $tpl = $this->pdoTools->config('tplPage'); - } - $pagination[$i] = !empty($tpl) - ? $this->makePageLink($url, $i, $tpl) - : ''; + $pagination[$i] = $this->renderPageItem($url, $i, $page, $pages); $i++; } if (!empty($this->pdoTools->config('tplPageSkip'))) { $key = ($page - 1 == $pages - $right - $center + 1) ? $left : $pages - $right - $center + 1; - $pagination[$key] = $this->pdoTools->getChunk($this->pdoTools->config('tplPageSkip')); + $pagination[$key] = $this->renderPageSkip($pages); } } else { $tmp = (integer)floor(($center - 1) / 2); $i = $page - $tmp; while ($i < $page - $tmp + $center) { - if ($page === $i && !empty($this->pdoTools->config('tplPageActive'))) { - $tpl = $this->pdoTools->config('tplPageActive'); - } elseif (!empty($this->pdoTools->config('tplPage'))) { - $tpl = $this->pdoTools->config('tplPage'); - } - $pagination[$i] = !empty($tpl) - ? $this->makePageLink($url, $i, $tpl) - : ''; + $pagination[$i] = $this->renderPageItem($url, $i, $page, $pages); $i++; } if (!empty($this->pdoTools->config('tplPageSkip'))) { - $pagination[$left] = $pagination[$pages - $right + 1] = $this->pdoTools->getChunk($this->pdoTools->config('tplPageSkip')); + $skip = $this->renderPageSkip($pages); + $pagination[$left] = $pagination[$pages - $right + 1] = $skip; } } } diff --git a/core/components/pdotools/src/Support/TemplateFlags.php b/core/components/pdotools/src/Support/TemplateFlags.php new file mode 100644 index 0000000..bcfc3c2 --- /dev/null +++ b/core/components/pdotools/src/Support/TemplateFlags.php @@ -0,0 +1,26 @@ + $bools + * @return array + */ + public static function toPlaceholders(array $bools) + { + $out = []; + foreach ($bools as $key => $value) { + $out[$key] = $value ? 1 : 0; + } + if (isset($out['hasChildren']) && !isset($out['hasChilds'])) { + $out['hasChilds'] = $out['hasChildren']; + } + + return $out; + } +} diff --git a/core/components/pdotools/tests/Unit/Support/CrumbItemStateTest.php b/core/components/pdotools/tests/Unit/Support/CrumbItemStateTest.php new file mode 100644 index 0000000..ef89d37 --- /dev/null +++ b/core/components/pdotools/tests/Unit/Support/CrumbItemStateTest.php @@ -0,0 +1,24 @@ +assertSame(1, $home['isFirst']); + $this->assertSame(1, $home['isHome']); + $this->assertSame(0, $home['isActive']); + + $current = CrumbItemState::placeholders(9, 9, 1, 2, 3); + $this->assertSame(1, $current['isLast']); + $this->assertSame(1, $current['isActive']); + $this->assertSame(0, $current['isHome']); + } +} diff --git a/core/components/pdotools/tests/Unit/Support/MenuItemStateTest.php b/core/components/pdotools/tests/Unit/Support/MenuItemStateTest.php new file mode 100644 index 0000000..28ade24 --- /dev/null +++ b/core/components/pdotools/tests/Unit/Support/MenuItemStateTest.php @@ -0,0 +1,130 @@ + 42, + 'idx' => 1, + 'last' => false, + 'level' => 1, + 'children' => 3, + 'template' => 1, + 'link_attributes' => '', + ], + ['hereId' => 42, 'displayStart' => true], + static function ($id) { + return (int)$id === 42; + } + ); + + $this->assertTrue($state->isFirst); + $this->assertFalse($state->isLast); + $this->assertTrue($state->isActive); + $this->assertTrue($state->isHere); + $this->assertTrue($state->hasChildren); + $this->assertTrue($state->isStart); + $this->assertFalse($state->isInner); + $this->assertSame(1, $state->placeholders()['hasChilds']); + } + + public function testTplKeyPrefersTplHereWhenConfigured(): void + { + $state = MenuItemState::fromRow( + [ + 'id' => 5, + 'idx' => 2, + 'last' => true, + 'level' => 1, + 'children' => 0, + ], + ['hereId' => 5, 'tplHere' => '@INLINE here'], + static function () { + return true; + } + ); + + $this->assertSame('tplHere', $state->tplKey(['hereId' => 5, 'tplHere' => '@INLINE here'])); + $this->assertNull($state->tplKey(['hereId' => 5])); + } + + public function testCategoryAndParentTpl(): void + { + $state = MenuItemState::fromRow( + [ + 'id' => 9, + 'idx' => 2, + 'last' => false, + 'level' => 2, + 'children' => 2, + 'template' => 0, + 'link_attributes' => '', + ], + ['hereId' => 99], + static function () { + return false; + } + ); + + $this->assertTrue($state->isCategory); + $this->assertTrue($state->isInner); + $this->assertSame( + 'tplCategoryFolder', + $state->tplKey(['tplCategoryFolder' => '@INLINE cat', 'hereId' => 99]) + ); + $this->assertSame( + 'tplParentRow', + $state->tplKey(['tplParentRow' => '@INLINE parent', 'hereId' => 99]) + ); + } + + public function testWeblinkRowIdAndClasses(): void + { + $state = MenuItemState::fromRow( + [ + 'id' => 10, + 'idx' => 1, + 'last' => false, + 'level' => 1, + 'children' => 0, + 'class_key' => modWebLink::class, + 'content' => '[[~20]]', + ], + [ + 'hereId' => 20, + 'useWeblinkUrl' => true, + 'firstClass' => 'first', + 'selfClass' => 'self', + 'hereClass' => 'active', + 'webLinkClass' => 'weblink', + ], + static function ($id) { + return (int)$id === 20; + } + ); + + $this->assertSame(20, $state->rowId); + $this->assertTrue($state->isActive); + $this->assertTrue($state->isWebLink); + $classes = $state->classes([ + 'firstClass' => 'first', + 'selfClass' => 'self', + 'hereClass' => 'active', + 'webLinkClass' => 'weblink', + ]); + $this->assertStringContainsString('first', $classes); + $this->assertStringContainsString('self', $classes); + $this->assertStringContainsString('active', $classes); + $this->assertStringContainsString('weblink', $classes); + } +} diff --git a/core/components/pdotools/tests/Unit/Support/PageItemStateTest.php b/core/components/pdotools/tests/Unit/Support/PageItemStateTest.php new file mode 100644 index 0000000..213fb63 --- /dev/null +++ b/core/components/pdotools/tests/Unit/Support/PageItemStateTest.php @@ -0,0 +1,45 @@ +assertSame(1, $flags['isActive']); + $this->assertSame(0, $flags['isFirst']); + $this->assertSame(0, $flags['isLast']); + $this->assertSame(0, $flags['isSkip']); + } + + public function testFirstAndLast(): void + { + $this->assertSame(1, PageItemState::placeholders(1, 5, 10)['isFirst']); + $this->assertSame(1, PageItemState::placeholders(10, 5, 10)['isLast']); + } + + public function testSkipClearsActive(): void + { + $flags = PageItemState::placeholders(5, 5, 10, true); + $this->assertSame(1, $flags['isSkip']); + $this->assertSame(0, $flags['isActive']); + } + + public function testTplSelection(): void + { + $config = [ + 'tplPage' => 'page', + 'tplPageActive' => 'active', + 'tplPageSkip' => 'skip', + ]; + $this->assertSame('active', PageItemState::tpl($config, 2, 2)); + $this->assertSame('page', PageItemState::tpl($config, 3, 2)); + $this->assertSame('skip', PageItemState::tpl($config, 4, 2, true)); + } +} diff --git a/core/components/pdotools/tests/Unit/Support/TemplateFlagsTest.php b/core/components/pdotools/tests/Unit/Support/TemplateFlagsTest.php new file mode 100644 index 0000000..dc8563f --- /dev/null +++ b/core/components/pdotools/tests/Unit/Support/TemplateFlagsTest.php @@ -0,0 +1,33 @@ +assertSame( + ['isActive' => 1, 'isFirst' => 0], + TemplateFlags::toPlaceholders(['isActive' => true, 'isFirst' => false]) + ); + } + + public function testHasChildsAliasesHasChildren(): void + { + $out = TemplateFlags::toPlaceholders(['hasChildren' => true]); + $this->assertSame(1, $out['hasChildren']); + $this->assertSame(1, $out['hasChilds']); + } + + public function testDoesNotOverwriteExplicitHasChilds(): void + { + $out = TemplateFlags::toPlaceholders(['hasChildren' => true, 'hasChilds' => false]); + $this->assertSame(1, $out['hasChildren']); + $this->assertSame(0, $out['hasChilds']); + } +} From e88454ca2df628c1355aa8795355fea2de6fb251 Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Wed, 2 Sep 2026 12:45:30 +0600 Subject: [PATCH 2/3] refactor(templates): Simplify item-flag APIs after quality review Drop CrumbItemState and pageItemTplAndFlags; keep flags in Paginator and snippet layers; freeze menu config on MenuItemState; fix category-at-0. --- .../elements/snippets/snippet.pdocrumbs.php | 24 +++--- .../elements/snippets/snippet.pdopage.php | 28 +------ .../pdotools/src/Support/CrumbItemState.php | 42 ---------- .../pdotools/src/Support/MenuBuilder.php | 19 ++--- .../pdotools/src/Support/MenuItemState.php | 38 ++++----- .../pdotools/src/Support/PageItemState.php | 27 ++---- .../pdotools/src/Support/Paginator.php | 47 ++++------- .../pdotools/src/Support/TemplateFlags.php | 3 - .../tests/Unit/Support/CrumbItemStateTest.php | 24 ------ .../tests/Unit/Support/MenuItemStateTest.php | 84 ++++++++++++------- .../tests/Unit/Support/PageItemStateTest.php | 1 + .../tests/Unit/Support/TemplateFlagsTest.php | 14 ---- 12 files changed, 125 insertions(+), 226 deletions(-) delete mode 100644 core/components/pdotools/src/Support/CrumbItemState.php delete mode 100644 core/components/pdotools/tests/Unit/Support/CrumbItemStateTest.php diff --git a/core/components/pdotools/elements/snippets/snippet.pdocrumbs.php b/core/components/pdotools/elements/snippets/snippet.pdocrumbs.php index 72fb349..57ae7b4 100644 --- a/core/components/pdotools/elements/snippets/snippet.pdocrumbs.php +++ b/core/components/pdotools/elements/snippets/snippet.pdocrumbs.php @@ -1,7 +1,7 @@ id && empty($showCurrent)) { continue; - } elseif ($row['id'] == $resource->id && !empty($tplCurrent)) { - $row['_tpl'] = $tplCurrent; - } elseif ($row['id'] == $siteStart && !empty($tplHome)) { - $row['_tpl'] = $tplHome; - } else { - $row['_tpl'] = $pdoFetch->defineChunk($row); } $prepared[] = $row; } @@ -198,14 +192,24 @@ foreach ($prepared as $index => $row) { $row = array_merge( $row, - CrumbItemState::placeholders($row['id'], $resource->id, $siteStart, $index, $total) + TemplateFlags::toPlaceholders([ + 'isFirst' => $index === 0, + 'isLast' => $total > 0 && $index === $total - 1, + 'isActive' => (int)$row['id'] === (int)$resource->id, + 'isHome' => (int)$row['id'] === (int)$siteStart, + ]) ); if (isset($return) && $return === 'data') { $output[] = $row; continue; } - $tpl = $row['_tpl'] ?? ''; - unset($row['_tpl']); + if ($row['id'] == $resource->id && !empty($tplCurrent)) { + $tpl = $tplCurrent; + } elseif ($row['id'] == $siteStart && !empty($tplHome)) { + $tpl = $tplHome; + } else { + $tpl = $pdoFetch->defineChunk($row); + } $output[] = empty($tpl) ? '
' . $pdoFetch->getChunk('', $row) . '
' : $pdoFetch->getChunk($tpl, $row, $fastMode); diff --git a/core/components/pdotools/elements/snippets/snippet.pdopage.php b/core/components/pdotools/elements/snippets/snippet.pdopage.php index a76d257..73acf13 100644 --- a/core/components/pdotools/elements/snippets/snippet.pdopage.php +++ b/core/components/pdotools/elements/snippets/snippet.pdopage.php @@ -160,39 +160,19 @@ if (!empty($pageCount) && $pageCount > 1) { $pagination = [ 'first' => $page > 1 && !empty($tplPageFirst) - ? $paginator->makePageLink( - $url, - 1, - $tplPageFirst, - \ModxPro\PdoTools\Support\PageItemState::placeholders(1, $page, $pageCount) - ) + ? $paginator->makePageLink($url, 1, $tplPageFirst, $page, $pageCount) : '', 'prev' => $page > 1 && !empty($tplPagePrev) - ? $paginator->makePageLink( - $url, - $page - 1, - $tplPagePrev, - \ModxPro\PdoTools\Support\PageItemState::placeholders($page - 1, $page, $pageCount) - ) + ? $paginator->makePageLink($url, $page - 1, $tplPagePrev, $page, $pageCount) : '', 'pages' => $pageLimit >= 7 && empty($disableModernPagination) ? $paginator->buildModernPagination($page, $pageCount, $url) : $paginator->buildClassicPagination($page, $pageCount, $url), 'next' => $page < $pageCount && !empty($tplPageNext) - ? $paginator->makePageLink( - $url, - $page + 1, - $tplPageNext, - \ModxPro\PdoTools\Support\PageItemState::placeholders($page + 1, $page, $pageCount) - ) + ? $paginator->makePageLink($url, $page + 1, $tplPageNext, $page, $pageCount) : '', 'last' => $page < $pageCount && !empty($tplPageLast) - ? $paginator->makePageLink( - $url, - $pageCount, - $tplPageLast, - \ModxPro\PdoTools\Support\PageItemState::placeholders($pageCount, $page, $pageCount) - ) + ? $paginator->makePageLink($url, $pageCount, $tplPageLast, $page, $pageCount) : '', ]; diff --git a/core/components/pdotools/src/Support/CrumbItemState.php b/core/components/pdotools/src/Support/CrumbItemState.php deleted file mode 100644 index a78f235..0000000 --- a/core/components/pdotools/src/Support/CrumbItemState.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ - public static function boolFlags($id, $currentId, $siteStart, $index, $total) - { - return [ - 'isFirst' => (int)$index === 0, - 'isLast' => $total > 0 && (int)$index === (int)$total - 1, - 'isActive' => (int)$id === (int)$currentId, - 'isHome' => (int)$id === (int)$siteStart, - ]; - } - - /** - * @param int $id - * @param int $currentId - * @param int $siteStart - * @param int $index - * @param int $total - * @return array - */ - public static function placeholders($id, $currentId, $siteStart, $index, $total) - { - return TemplateFlags::toPlaceholders( - self::boolFlags($id, $currentId, $siteStart, $index, $total) - ); - } -} diff --git a/core/components/pdotools/src/Support/MenuBuilder.php b/core/components/pdotools/src/Support/MenuBuilder.php index c693bb0..6c5fe78 100644 --- a/core/components/pdotools/src/Support/MenuBuilder.php +++ b/core/components/pdotools/src/Support/MenuBuilder.php @@ -181,7 +181,7 @@ public function templateBranch($row = []) } $state = $this->itemState($row); - $classes = $state->classes($this->pdoTools->config()); + $classes = $state->classes(); if (!empty($classes)) { $row['classNames'] = $row['classnames'] = $classes; $row['classes'] = ' class="' . $classes . '"'; @@ -227,15 +227,12 @@ public function isHere($id = 0) * @param array $row * @return MenuItemState */ - public function itemState(array $row = []) + protected function itemState(array $row = []) { - return MenuItemState::fromRow( - $row, - $this->pdoTools->config(), - function ($id) { - return $this->isHere($id); - } - ); + $config = $this->pdoTools->config(); + $rowId = MenuItemState::resolveRowId($row, $config); + + return MenuItemState::fromRow($row, $config, $this->isHere($rowId)); } /** @@ -247,7 +244,7 @@ function ($id) { */ public function getClasses($row = []) { - return $this->itemState($row)->classes($this->pdoTools->config()); + return $this->itemState($row)->classes(); } @@ -270,7 +267,7 @@ public function getTpl($row = []) */ protected function tplFromState(MenuItemState $state, array $row) { - $key = $state->tplKey($this->pdoTools->config()); + $key = $state->tplKey(); if ($key === null) { return $this->pdoTools->defineChunk($row); } diff --git a/core/components/pdotools/src/Support/MenuItemState.php b/core/components/pdotools/src/Support/MenuItemState.php index 0de667a..2b77c1b 100644 --- a/core/components/pdotools/src/Support/MenuItemState.php +++ b/core/components/pdotools/src/Support/MenuItemState.php @@ -32,31 +32,32 @@ class MenuItemState /** @var int */ public $level; /** @var array */ - public $row; + private $config; /** * @param array $row * @param array $config - * @param callable $isHere fn(int $id): bool + * @param bool $isHere Whether this rowId is on the path to the current resource * @return self */ - public static function fromRow(array $row, array $config, callable $isHere) + public static function fromRow(array $row, array $config, $isHere) { $state = new self(); - $state->row = $row; + $state->config = $config; $state->level = (int)($row['level'] ?? 1); $state->rowId = self::resolveRowId($row, $config); $state->isFirst = isset($row['idx']) && (int)$row['idx'] === 1; $state->isLast = !empty($row['last']); $state->hasChildren = !empty($row['children']); $state->isActive = $state->rowId == ($config['hereId'] ?? 0); - $state->isHere = (bool)$isHere($state->rowId); + $state->isHere = (bool)$isHere; $state->isStart = $state->level === 1 && !empty($config['displayStart']); $state->isInner = $state->level > 1; $state->isWebLink = !empty($row['class_key']) && $row['class_key'] === modWebLink::class; + $attrs = isset($row['link_attributes']) ? (string)$row['link_attributes'] : ''; $state->isCategory = $state->hasChildren && ( empty($row['template']) - || (isset($row['link_attributes']) && strpos((string)$row['link_attributes'], 'category') !== false) + || ($attrs !== '' && strpos($attrs, 'category') !== false) ); return $state; @@ -83,11 +84,11 @@ public static function resolveRowId(array $row, array $config) } /** - * @return array + * @return array */ - public function boolFlags() + public function placeholders() { - return [ + $out = TemplateFlags::toPlaceholders([ 'isFirst' => $this->isFirst, 'isLast' => $this->isLast, 'isActive' => $this->isActive, @@ -96,23 +97,18 @@ public function boolFlags() 'isStart' => $this->isStart, 'isCategory' => $this->isCategory, 'isInner' => $this->isInner, - ]; - } + ]); + $out['hasChilds'] = $out['hasChildren']; - /** - * @return array - */ - public function placeholders() - { - return TemplateFlags::toPlaceholders($this->boolFlags()); + return $out; } /** - * @param array $config * @return string */ - public function classes(array $config) + public function classes() { + $config = $this->config; $classes = []; if (!empty($config['rowClass'])) { @@ -149,11 +145,11 @@ public function classes(array $config) /** * Config key for a specialized tpl*, or null to fall back to defineChunk(). * - * @param array $config * @return string|null */ - public function tplKey(array $config) + public function tplKey() { + $config = $this->config; if ($this->isStart && !empty($config['tplStart'])) { return 'tplStart'; } diff --git a/core/components/pdotools/src/Support/PageItemState.php b/core/components/pdotools/src/Support/PageItemState.php index 8304978..db472ce 100644 --- a/core/components/pdotools/src/Support/PageItemState.php +++ b/core/components/pdotools/src/Support/PageItemState.php @@ -3,7 +3,7 @@ namespace ModxPro\PdoTools\Support; /** - * Flags for one pdoPage pagination slot. + * Flags and tpl pick for one pdoPage pagination slot. */ class PageItemState { @@ -12,36 +12,25 @@ class PageItemState * @param int $current Current page * @param int $pages Total pages * @param bool $skip Skip/ellipsis slot - * @return array + * @return array */ - public static function boolFlags($page, $current, $pages, $skip = false) + public static function placeholders($page, $current, $pages, $skip = false) { $page = (int)$page; $current = (int)$current; $pages = (int)$pages; - return [ + return TemplateFlags::toPlaceholders([ 'isFirst' => $page === 1, 'isLast' => $pages > 0 && $page === $pages, 'isActive' => !$skip && $page === $current, 'isSkip' => (bool)$skip, - ]; - } - - /** - * @param int $page - * @param int $current - * @param int $pages - * @param bool $skip - * @return array - */ - public static function placeholders($page, $current, $pages, $skip = false) - { - return TemplateFlags::toPlaceholders(self::boolFlags($page, $current, $pages, $skip)); + ]); } /** * Pick tplPageActive / tplPage / tplPageSkip from config. + * Skip slots only return tplPageSkip (or empty), never fall back to page tpls. * * @param array $config * @param int $page @@ -51,8 +40,8 @@ public static function placeholders($page, $current, $pages, $skip = false) */ public static function tpl(array $config, $page, $current, $skip = false) { - if ($skip && !empty($config['tplPageSkip'])) { - return (string)$config['tplPageSkip']; + if ($skip) { + return !empty($config['tplPageSkip']) ? (string)$config['tplPageSkip'] : ''; } if ((int)$page === (int)$current && !empty($config['tplPageActive'])) { return (string)$config['tplPageActive']; diff --git a/core/components/pdotools/src/Support/Paginator.php b/core/components/pdotools/src/Support/Paginator.php index a496584..716c7a8 100644 --- a/core/components/pdotools/src/Support/Paginator.php +++ b/core/components/pdotools/src/Support/Paginator.php @@ -187,11 +187,13 @@ public function getBaseUrl() * @param string $url * @param int $page * @param string $tpl - * @param array $flags Extra placeholders (isActive, isFirst, …) + * @param int|null $current Current page (for isActive / isFirst / isLast) + * @param int|null $pages Total pages + * @param bool $skip Skip/ellipsis slot * * @return string $href */ - public function makePageLink($url = '', $page = 1, $tpl = '', array $flags = []) + public function makePageLink($url = '', $page = 1, $tpl = '', $current = null, $pages = null, $skip = false) { if (empty($url)) { $url = $this->getBaseUrl(); @@ -240,35 +242,23 @@ public function makePageLink($url = '', $page = 1, $tpl = '', array $flags = []) $href = preg_replace("/&(?!amp;)/", "&", $href); } - $data = array_merge([ + $data = [ 'page' => $page, 'pageNo' => $page, 'href' => $href, - ], $flags); + ]; + if ($current !== null && $pages !== null) { + $data = array_merge( + $data, + PageItemState::placeholders($page, $current, $pages, $skip) + ); + } return !empty($tpl) ? $this->pdoTools->getChunk($tpl, $data) : $href; } - /** - * Template and flags for one page number (or skip) slot. - * - * @param int $page - * @param int $current - * @param int $pages - * @param bool $skip - * @return array{0:string,1:array} - */ - public function pageItemTplAndFlags($page, $current, $pages, $skip = false) - { - $config = $this->pdoTools->config(); - $tpl = PageItemState::tpl($config, $page, $current, $skip); - $flags = PageItemState::placeholders($page, $current, $pages, $skip); - - return [$tpl, $flags]; - } - /** * @param string $url * @param int $page Slot number @@ -279,15 +269,12 @@ public function pageItemTplAndFlags($page, $current, $pages, $skip = false) */ protected function renderPageItem($url, $page, $current, $pages, $skip = false) { - [$tpl, $flags] = $this->pageItemTplAndFlags($page, $current, $pages, $skip); + $tpl = PageItemState::tpl($this->pdoTools->config(), $page, $current, $skip); if (empty($tpl)) { return ''; } - if ($skip && empty($this->pdoTools->config('tplPageSkip'))) { - return ''; - } - return $this->makePageLink($url, $page, $tpl, $flags); + return $this->makePageLink($url, $page, $tpl, $current, $pages, $skip); } /** @@ -342,7 +329,7 @@ public function buildClassicPagination($page = 1, $pages = 5, $url = '') $pageLimit = 0; } else { // -1 because we need to show current page - $tmp = (integer)floor(($pageLimit - 1) / 2); + $tmp = (int)floor(($pageLimit - 1) / 2); $left = $tmp; // Pages from left $right = $pageLimit - $left - 1; // Pages from right @@ -399,7 +386,7 @@ public function buildModernPagination($page = 1, $pages = 5, $url = '') if ($pageLimit >= $pages || $pageLimit < 7) { return $this->buildClassicPagination($page, $pages, $url); } else { - $tmp = (integer)floor($pageLimit / 3); + $tmp = (int)floor($pageLimit / 3); $left = $right = $tmp; $center = $pageLimit - ($tmp * 2); } @@ -456,7 +443,7 @@ public function buildModernPagination($page = 1, $pages = 5, $url = '') $pagination[$key] = $this->renderPageSkip($pages); } } else { - $tmp = (integer)floor(($center - 1) / 2); + $tmp = (int)floor(($center - 1) / 2); $i = $page - $tmp; while ($i < $page - $tmp + $center) { $pagination[$i] = $this->renderPageItem($url, $i, $page, $pages); diff --git a/core/components/pdotools/src/Support/TemplateFlags.php b/core/components/pdotools/src/Support/TemplateFlags.php index bcfc3c2..0da5726 100644 --- a/core/components/pdotools/src/Support/TemplateFlags.php +++ b/core/components/pdotools/src/Support/TemplateFlags.php @@ -17,9 +17,6 @@ public static function toPlaceholders(array $bools) foreach ($bools as $key => $value) { $out[$key] = $value ? 1 : 0; } - if (isset($out['hasChildren']) && !isset($out['hasChilds'])) { - $out['hasChilds'] = $out['hasChildren']; - } return $out; } diff --git a/core/components/pdotools/tests/Unit/Support/CrumbItemStateTest.php b/core/components/pdotools/tests/Unit/Support/CrumbItemStateTest.php deleted file mode 100644 index ef89d37..0000000 --- a/core/components/pdotools/tests/Unit/Support/CrumbItemStateTest.php +++ /dev/null @@ -1,24 +0,0 @@ -assertSame(1, $home['isFirst']); - $this->assertSame(1, $home['isHome']); - $this->assertSame(0, $home['isActive']); - - $current = CrumbItemState::placeholders(9, 9, 1, 2, 3); - $this->assertSame(1, $current['isLast']); - $this->assertSame(1, $current['isActive']); - $this->assertSame(0, $current['isHome']); - } -} diff --git a/core/components/pdotools/tests/Unit/Support/MenuItemStateTest.php b/core/components/pdotools/tests/Unit/Support/MenuItemStateTest.php index 28ade24..e08b2c1 100644 --- a/core/components/pdotools/tests/Unit/Support/MenuItemStateTest.php +++ b/core/components/pdotools/tests/Unit/Support/MenuItemStateTest.php @@ -23,9 +23,7 @@ public function testSelfRowFlags(): void 'link_attributes' => '', ], ['hereId' => 42, 'displayStart' => true], - static function ($id) { - return (int)$id === 42; - } + true ); $this->assertTrue($state->isFirst); @@ -35,11 +33,13 @@ static function ($id) { $this->assertTrue($state->hasChildren); $this->assertTrue($state->isStart); $this->assertFalse($state->isInner); + $this->assertSame(1, $state->placeholders()['hasChildren']); $this->assertSame(1, $state->placeholders()['hasChilds']); } public function testTplKeyPrefersTplHereWhenConfigured(): void { + $config = ['hereId' => 5, 'tplHere' => '@INLINE here']; $state = MenuItemState::fromRow( [ 'id' => 5, @@ -48,14 +48,24 @@ public function testTplKeyPrefersTplHereWhenConfigured(): void 'level' => 1, 'children' => 0, ], - ['hereId' => 5, 'tplHere' => '@INLINE here'], - static function () { - return true; - } + $config, + true ); - $this->assertSame('tplHere', $state->tplKey(['hereId' => 5, 'tplHere' => '@INLINE here'])); - $this->assertNull($state->tplKey(['hereId' => 5])); + $this->assertSame('tplHere', $state->tplKey()); + + $without = MenuItemState::fromRow( + [ + 'id' => 5, + 'idx' => 2, + 'last' => true, + 'level' => 1, + 'children' => 0, + ], + ['hereId' => 5], + true + ); + $this->assertNull($without->tplKey()); } public function testCategoryAndParentTpl(): void @@ -70,22 +80,47 @@ public function testCategoryAndParentTpl(): void 'template' => 0, 'link_attributes' => '', ], - ['hereId' => 99], - static function () { - return false; - } + ['hereId' => 99, 'tplCategoryFolder' => '@INLINE cat'], + false ); $this->assertTrue($state->isCategory); $this->assertTrue($state->isInner); - $this->assertSame( - 'tplCategoryFolder', - $state->tplKey(['tplCategoryFolder' => '@INLINE cat', 'hereId' => 99]) + $this->assertSame('tplCategoryFolder', $state->tplKey()); + + $parentOnly = MenuItemState::fromRow( + [ + 'id' => 9, + 'idx' => 2, + 'last' => false, + 'level' => 2, + 'children' => 2, + 'template' => 0, + 'link_attributes' => '', + ], + ['hereId' => 99, 'tplParentRow' => '@INLINE parent'], + false ); - $this->assertSame( - 'tplParentRow', - $state->tplKey(['tplParentRow' => '@INLINE parent', 'hereId' => 99]) + $this->assertSame('tplParentRow', $parentOnly->tplKey()); + } + + public function testCategoryFromLinkAttributesAtOffsetZero(): void + { + $state = MenuItemState::fromRow( + [ + 'id' => 9, + 'idx' => 1, + 'last' => false, + 'level' => 1, + 'children' => 2, + 'template' => 3, + 'link_attributes' => 'category', + ], + ['hereId' => 99], + false ); + + $this->assertTrue($state->isCategory); } public function testWeblinkRowIdAndClasses(): void @@ -108,20 +143,13 @@ public function testWeblinkRowIdAndClasses(): void 'hereClass' => 'active', 'webLinkClass' => 'weblink', ], - static function ($id) { - return (int)$id === 20; - } + true ); $this->assertSame(20, $state->rowId); $this->assertTrue($state->isActive); $this->assertTrue($state->isWebLink); - $classes = $state->classes([ - 'firstClass' => 'first', - 'selfClass' => 'self', - 'hereClass' => 'active', - 'webLinkClass' => 'weblink', - ]); + $classes = $state->classes(); $this->assertStringContainsString('first', $classes); $this->assertStringContainsString('self', $classes); $this->assertStringContainsString('active', $classes); diff --git a/core/components/pdotools/tests/Unit/Support/PageItemStateTest.php b/core/components/pdotools/tests/Unit/Support/PageItemStateTest.php index 213fb63..2429587 100644 --- a/core/components/pdotools/tests/Unit/Support/PageItemStateTest.php +++ b/core/components/pdotools/tests/Unit/Support/PageItemStateTest.php @@ -41,5 +41,6 @@ public function testTplSelection(): void $this->assertSame('active', PageItemState::tpl($config, 2, 2)); $this->assertSame('page', PageItemState::tpl($config, 3, 2)); $this->assertSame('skip', PageItemState::tpl($config, 4, 2, true)); + $this->assertSame('', PageItemState::tpl(['tplPage' => 'page'], 4, 2, true)); } } diff --git a/core/components/pdotools/tests/Unit/Support/TemplateFlagsTest.php b/core/components/pdotools/tests/Unit/Support/TemplateFlagsTest.php index dc8563f..29af368 100644 --- a/core/components/pdotools/tests/Unit/Support/TemplateFlagsTest.php +++ b/core/components/pdotools/tests/Unit/Support/TemplateFlagsTest.php @@ -16,18 +16,4 @@ public function testBoolsBecomeOnesAndZeros(): void TemplateFlags::toPlaceholders(['isActive' => true, 'isFirst' => false]) ); } - - public function testHasChildsAliasesHasChildren(): void - { - $out = TemplateFlags::toPlaceholders(['hasChildren' => true]); - $this->assertSame(1, $out['hasChildren']); - $this->assertSame(1, $out['hasChilds']); - } - - public function testDoesNotOverwriteExplicitHasChilds(): void - { - $out = TemplateFlags::toPlaceholders(['hasChildren' => true, 'hasChilds' => false]); - $this->assertSame(1, $out['hasChildren']); - $this->assertSame(0, $out['hasChilds']); - } } From 9c73f197c2adc677f9dbb0afd40455546110526a Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Wed, 2 Sep 2026 12:59:19 +0600 Subject: [PATCH 3/3] test(templates): Cover Paginator and MenuBuilder flag wiring Harnesses capture getChunk so makePageLink/renderPageItem and templateBranch flag merges are asserted without a live MODX install. --- .../pdotools/tests/Support/CapturingTools.php | 41 ++++++++ .../tests/Support/MenuBuilderHarness.php | 23 +++++ .../tests/Support/PaginatorHarness.php | 38 ++++++++ .../Unit/Support/MenuBuilderFlagsTest.php | 81 ++++++++++++++++ .../tests/Unit/Support/PaginatorFlagsTest.php | 94 +++++++++++++++++++ 5 files changed, 277 insertions(+) create mode 100644 core/components/pdotools/tests/Support/CapturingTools.php create mode 100644 core/components/pdotools/tests/Support/MenuBuilderHarness.php create mode 100644 core/components/pdotools/tests/Support/PaginatorHarness.php create mode 100644 core/components/pdotools/tests/Unit/Support/MenuBuilderFlagsTest.php create mode 100644 core/components/pdotools/tests/Unit/Support/PaginatorFlagsTest.php diff --git a/core/components/pdotools/tests/Support/CapturingTools.php b/core/components/pdotools/tests/Support/CapturingTools.php new file mode 100644 index 0000000..e0fc1b4 --- /dev/null +++ b/core/components/pdotools/tests/Support/CapturingTools.php @@ -0,0 +1,41 @@ + */ + public $lastChunkProperties = []; + + /** + * @param array $properties + */ + public function getChunk($name = '', array $properties = [], $fastMode = false) + { + $properties = $this->prepareRow($properties); + $this->lastChunkName = (string)$name; + $this->lastChunkProperties = $properties; + + return (string)$name; + } + + public function makeUrl($id, $options = [], $args = []) + { + return '/id/' . (int)$id; + } + + public function defineChunk(array $properties = []) + { + return !empty($this->config['tpl']) ? (string)$this->config['tpl'] : '@INLINE default'; + } +} diff --git a/core/components/pdotools/tests/Support/MenuBuilderHarness.php b/core/components/pdotools/tests/Support/MenuBuilderHarness.php new file mode 100644 index 0000000..e3b570d --- /dev/null +++ b/core/components/pdotools/tests/Support/MenuBuilderHarness.php @@ -0,0 +1,23 @@ + $parentTree + */ + public function __construct(modX $modx, CoreTools $pdoTools, array $parentTree = []) + { + $this->modx = $modx; + $this->pdoTools = $pdoTools; + $this->parentTree = $parentTree; + $this->level = 1; + } +} diff --git a/core/components/pdotools/tests/Support/PaginatorHarness.php b/core/components/pdotools/tests/Support/PaginatorHarness.php new file mode 100644 index 0000000..8bfc497 --- /dev/null +++ b/core/components/pdotools/tests/Support/PaginatorHarness.php @@ -0,0 +1,38 @@ +modx = $modx; + $this->pdoTools = $pdoTools; + } + + /** + * @param string $url + * @param int $page + * @param int $current + * @param int $pages + * @param bool $skip + */ + public function publicRenderPageItem($url, $page, $current, $pages, $skip = false): string + { + return $this->renderPageItem($url, $page, $current, $pages, $skip); + } + + /** + * @param int $pages + */ + public function publicRenderPageSkip($pages = 0): string + { + return $this->renderPageSkip($pages); + } +} diff --git a/core/components/pdotools/tests/Unit/Support/MenuBuilderFlagsTest.php b/core/components/pdotools/tests/Unit/Support/MenuBuilderFlagsTest.php new file mode 100644 index 0000000..1b413fe --- /dev/null +++ b/core/components/pdotools/tests/Unit/Support/MenuBuilderFlagsTest.php @@ -0,0 +1,81 @@ +modx, [ + 'hereId' => 5, + 'firstClass' => 'first', + 'selfClass' => 'self', + 'hereClass' => 'active', + 'tplHere' => '@INLINE here', + 'tpl' => '@INLINE row', + ]); + $menu = new MenuBuilderHarness($this->modx, $tools, [5 => 0]); + + $out = $menu->templateBranch([ + 'id' => 5, + 'idx' => 1, + 'last' => false, + 'pagetitle' => 'Home', + 'children' => [], + ]); + + $this->assertSame('@INLINE here', $out); + $this->assertSame('@INLINE here', $tools->lastChunkName); + $pls = $tools->lastChunkProperties; + $this->assertSame(1, $pls['isActive']); + $this->assertSame(1, $pls['isFirst']); + $this->assertSame(1, $pls['isHere']); + $this->assertSame(0, $pls['hasChildren']); + $this->assertSame(0, $pls['hasChilds']); + $this->assertSame('Home', $pls['menutitle']); + $this->assertSame('/id/5', $pls['link']); + $this->assertStringContainsString('first', $pls['classNames']); + $this->assertStringContainsString('self', $pls['classNames']); + $this->assertStringContainsString('active', $pls['classNames']); + } + + public function testGetTplPrefersSpecializedChunk(): void + { + $tools = new CapturingTools($this->modx, [ + 'hereId' => 9, + 'tplHere' => '@INLINE here', + 'tpl' => '@INLINE row', + ]); + $menu = new MenuBuilderHarness($this->modx, $tools, [9 => 0]); + + $this->assertSame( + '@INLINE here', + $menu->getTpl(['id' => 9, 'idx' => 2, 'level' => 1, 'children' => 0]) + ); + } + + public function testGetClassesUsesFrozenConfig(): void + { + $tools = new CapturingTools($this->modx, [ + 'hereId' => 1, + 'firstClass' => 'first', + 'lastClass' => 'last', + ]); + $menu = new MenuBuilderHarness($this->modx, $tools); + + $classes = $menu->getClasses([ + 'id' => 2, + 'idx' => 1, + 'last' => false, + 'level' => 1, + 'children' => 0, + ]); + $this->assertStringContainsString('first', $classes); + } +} diff --git a/core/components/pdotools/tests/Unit/Support/PaginatorFlagsTest.php b/core/components/pdotools/tests/Unit/Support/PaginatorFlagsTest.php new file mode 100644 index 0000000..4937406 --- /dev/null +++ b/core/components/pdotools/tests/Unit/Support/PaginatorFlagsTest.php @@ -0,0 +1,94 @@ +tools = new CapturingTools($this->modx, [ + 'pageVarKey' => 'page', + 'tplPage' => '@INLINE page', + 'tplPageActive' => '@INLINE active', + 'tplPageSkip' => '@INLINE skip', + ]); + $this->paginator = new PaginatorHarness($this->modx, $this->tools); + } + + public function testMakePageLinkWithoutContextReturnsHrefOnly(): void + { + $href = $this->paginator->makePageLink('/list', 3); + $this->assertSame('/list?page=3', $href); + } + + public function testMakePageLinkMergesFlagsIntoChunk(): void + { + $this->paginator->makePageLink('/list', 3, '@INLINE page', 3, 10); + + $this->assertSame('@INLINE page', $this->tools->lastChunkName); + $pls = $this->tools->lastChunkProperties; + $this->assertSame(3, $pls['page']); + $this->assertSame('/list?page=3', $pls['href']); + $this->assertSame(1, $pls['isActive']); + $this->assertSame(0, $pls['isFirst']); + $this->assertSame(0, $pls['isLast']); + $this->assertSame(0, $pls['isSkip']); + } + + public function testMakePageLinkSkipFlag(): void + { + $this->paginator->makePageLink('/list', 5, '@INLINE skip', 3, 10, true); + $this->assertSame(1, $this->tools->lastChunkProperties['isSkip']); + $this->assertSame(0, $this->tools->lastChunkProperties['isActive']); + } + + public function testRenderPageItemPicksActiveTpl(): void + { + $out = $this->paginator->publicRenderPageItem('/list', 2, 2, 5); + $this->assertSame('@INLINE active', $out); + $this->assertSame(1, $this->tools->lastChunkProperties['isActive']); + } + + public function testRenderPageItemSkipWithoutTplReturnsEmpty(): void + { + $this->tools->setConfig([ + 'pageVarKey' => 'page', + 'tplPage' => '@INLINE page', + ]); + $this->assertSame('', $this->paginator->publicRenderPageItem('/list', 4, 2, 10, true)); + } + + public function testRenderPageItemSkipUsesSkipTpl(): void + { + $out = $this->paginator->publicRenderPageItem('/list', 4, 2, 10, true); + $this->assertSame('@INLINE skip', $out); + $this->assertSame(1, $this->tools->lastChunkProperties['isSkip']); + } + + public function testRenderPageSkipWithoutTplReturnsEmpty(): void + { + $this->tools->setConfig(['pageVarKey' => 'page']); + $this->assertSame('', $this->paginator->publicRenderPageSkip(10)); + } + + public function testRenderPageSkipPassesFlags(): void + { + $out = $this->paginator->publicRenderPageSkip(8); + $this->assertSame('@INLINE skip', $out); + $this->assertSame(1, $this->tools->lastChunkProperties['isSkip']); + $this->assertSame(0, $this->tools->lastChunkProperties['isActive']); + } +}