From 4ee01a8b75159a6982ca38c5fd1631c59c861b44 Mon Sep 17 00:00:00 2001 From: Teppo Koivula Date: Sun, 4 Dec 2022 23:09:22 +0200 Subject: [PATCH] Add support for menu items as an array and template strings as callables --- CHANGELOG.md | 6 ++ MarkupMenu.info.json | 2 +- MarkupMenu.module.php | 135 +++++++++++++++++++++++++++++++++++++++--- README.md | 107 +++++++++++++++++++++------------ 4 files changed, 203 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60a6881..cc59869 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.11.0] - 2022-12-04 + +### Added +- Support for providing menu items as a prepopulated array via the menu_items option. +- Support for callables as templates, enabling template string to be defined dynamically. + ## [0.10.0] - 2021-09-02 ### Added diff --git a/MarkupMenu.info.json b/MarkupMenu.info.json index c7c39d3..1d41594 100644 --- a/MarkupMenu.info.json +++ b/MarkupMenu.info.json @@ -1,7 +1,7 @@ { "title": "MarkupMenu", "summary": "MarkupMenu is a module for generating menu markup.", - "version": "0.10.0", + "version": "0.11.0", "author": "Teppo Koivula", "href": "https://github.com/teppokoivula/MarkupMenu", "requires": [ diff --git a/MarkupMenu.module.php b/MarkupMenu.module.php index f0cd9d4..331da23 100644 --- a/MarkupMenu.module.php +++ b/MarkupMenu.module.php @@ -8,7 +8,7 @@ * MarkupMenu is a module for generating menu markup. See README.md for more details. * Some ideas and code in this module are based on the Markup Simple Navigation module. * - * @version 0.10.0 + * @version 0.11.0 * @author Teppo Koivula * @license Mozilla Public License v2.0 http://mozilla.org/MPL/2.0/ */ @@ -54,6 +54,12 @@ class MarkupMenu extends WireData implements Module { 'parent' => '&--parent', 'has_children' => '&--has-children', ], + 'array_item_keys' => [ + 'id' => 'id', + 'is_current' => 'is_current', + 'is_parent' => 'is_parent', + 'children' => 'children', + ], ]; /** @@ -83,12 +89,53 @@ public function render(array $options = []): string { // generate menu markup $menu = ''; if (!empty($options['root_page'] || !empty($options['menu_items']))) { - $menu = $this->renderTree($options, $options['root_page'], $options['menu_items']); + if (is_array($options['menu_items'])) { + $menu = $this->renderArray($options, $options['menu_items']); + } else { + $menu = $this->renderTree($options, $options['root_page'], $options['menu_items']); + } } return $menu; } + /** + * Render menu from fixed array of items + * + * @param array $options Options for rendering + * @param array $items Menu items + * @param int $level Current tree level (depth) + * @return string Rendered menu markup + */ + protected function renderArray(array $options = [], array $items, int $level = 1): string { + + $out = ''; + + // iterate items and render markup for each separately + foreach ($items as $item) { + $out .= $this->renderArrayItem($options, $item, $level); + } + + if (!empty($out) && (!empty($options['templates']['list']) || !empty($options['templates']['nav']))) { + + // set up a placeholders + $placeholders = [ + 'level' => $level, + 'root_page' => $options['root_page'], + ]; + + // generate list markup + $out = $this->applyTemplate('list', $placeholders, $options, null, $out); + + // generate nav markup + if ($level === 1) { + $out = $this->applyTemplate('nav', $placeholders, $options, null, $out); + } + } + + return $out; + } + /** * Render tree of items using recursion * @@ -128,7 +175,6 @@ protected function renderTree(array $options = [], Page $root = null, PageArray if ($level === 1) { $out = $this->applyTemplate('nav', $placeholders, $options, null, $out); } - } return $out; @@ -164,6 +210,78 @@ protected function ___getItems(array $options, Page $root = null, int $level): P return $items; } + /** + * Render markup for a single array item + * + * @param array $options Options for rendering + * @param array $item Menu item being rendered + * @param int $level Current tree level (depth) + * @return string Rendered menu item markup + */ + protected function ___renderArrayItem(array $options = [], array $item = null, int $level = 1): string { + + $out = ''; + + // bail out early if item is empty + if (empty($item)) { + return $out; + } + + // instantiate new MarkupMenuData object and populate with item properties + $data_item = new MarkupMenuData($item); + + // array item keys + $keys = $options['array_item_keys']; + + // default classes + $classes = []; + if (!empty($options['classes']['page_id']) && !empty($item[$keys['id']])) { + $classes['page_id'] = $options['classes']['page_id'] . $item[$keys['id']]; + } + + // is this current page? + $item_is_current = $item[$keys['is_current']] ?? (!empty($item[$keys['id']]) && $options['current_page'] && $options['current_page']->id === $item[$keys['id']]); + if ($item_is_current) $classes['current'] = $options['classes']['current']; + + // is this a parent page? + $item_is_parent = $item[$keys['is_parent']] ?? (!$item_is_current && !empty($item[$keys['id']]) && (!empty($root) && $item->id !== $root->id || !$options['flat_root']) && $options['current_page'] && $options['current_page']->parents->has("id=" . $item[$keys['id']])); + if ($item_is_parent) $classes['parent'] = $options['classes']['parent']; + + // have we reached the level limit? + $level_limit_reached = $options['exclude']['level_greater_than'] && $level >= $options['exclude']['level_greater_than']; + + // does this page have children? + $has_children = (!empty($root) && !empty($item[$keys['id']]) && $item[$keys['id']] !== $root->id || !$options['flat_root']) && !$level_limit_reached && !empty($item[$keys['children']]); + if ($has_children) $classes['has_children'] = $options['classes']['has_children']; + + // should we render the children for this item? + $with_children = $has_children && (!$options['collapsed'] || $item_is_current || $item_is_parent); + + // placeholders for string replacements + $placeholders = array_merge( + [ + 'level' => $level, + 'item' => $data_item, + 'classes' => $classes, + ], + $options['placeholders'] + ); + + // generate markup for menu item + $item_template_name = 'item' . ($item_is_current ? '_current' : ''); + $item_markup = $this->applyTemplate($item_template_name, $placeholders, $options, $data_item); + + // generate markup for menu item children + if ($with_children) { + $item_markup .= $this->renderArray($options, $item[$keys['children']], $level + 1); + } + + // generate markup for current list item + $out .= $this->applyTemplate('list_item', $placeholders, $options, $data_item, $item_markup); + + return $out; + } + /** * Render markup for a single menu item * @@ -272,12 +390,13 @@ protected function getPage($source = null, $default = null): ?Page { * Get template for rendering an element * * @param string $template_name Template name - * @param Page|null $item Item being rendered + * @param Page|MarkupMenuData|null $item Item being rendered * @param array $options An array of options * @return string Template */ - protected function ___getTemplate($template_name, ?Page $item = null, array $options = []): string { - return $options['templates'][$template_name]; + protected function ___getTemplate($template_name, ?WireData $item = null, array $options = []): string { + $template = $options['templates'][$template_name]; + return is_callable($template) ? $template($item, $options) : $template; } /** @@ -286,11 +405,11 @@ protected function ___getTemplate($template_name, ?Page $item = null, array $opt * @param string $template_name Name of the template * @param array $placeholders Array of placeholders for string replacements * @param array $options An array of options - * @param Page|null $item Item being rendered + * @param Page|MarkupMenuData|null $item Item being rendered * @param string|null $content Content to be wrapped in template * @return string Content either wrapped in template, or as-is if no template was defined */ - protected function applyTemplate(string $template_name, array $placeholders, array $options, ?Page $item = null, ?string $content = null): string { + protected function applyTemplate(string $template_name, array $placeholders, array $options, ?WireData $item = null, ?string $content = null): string { $out = ''; diff --git a/README.md b/README.md index a2bc3ec..dd56fa2 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,59 @@ MarkupMenu ProcessWire module ----------------------------- -MarkupMenu is a ProcessWire module for generating markup for navigation menus. When provided a root page as a starting -point and (optional, but recommended) the current (active) page, it generates the markup for a navigation menu as an -unordered list `