Skip to content

Commit

Permalink
Add support for menu items as an array and template strings as callables
Browse files Browse the repository at this point in the history
  • Loading branch information
teppokoivula committed Dec 4, 2022
1 parent e6451e3 commit 4ee01a8
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 47 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion MarkupMenu.info.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
135 changes: 127 additions & 8 deletions MarkupMenu.module.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>
* @license Mozilla Public License v2.0 http://mozilla.org/MPL/2.0/
*/
Expand Down Expand Up @@ -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',
],
];

/**
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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 = '';

Expand Down
107 changes: 69 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 `<ul>` wrapped with a `<nav>` element based on said arguments.
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 `<ul>` wrapped with a `<nav>` element based on said arguments.

MarkupMenu provides a number of configurable options, including template strings used for various menu items (such as
the list and nav element mentioned above), as well as hookable methods for those who need full control over how their
menus are laid out.
MarkupMenu provides a number of configurable options, including template strings used for various menu items (such as the list and nav element mentioned above), as well as hookable methods for those who need full control over how their menus are laid out.

## Usage

MarkupMenu is intended for front-end use, but you can of course use it in a module as well. Typically you'll only need
the render() method, which takes an array of options as its only argument:
MarkupMenu is intended for front-end use, but you can of course use it in a module as well. Typically you'll only need the render() method, which takes an array of options as its only argument:

```
```php
echo $modules->get('MarkupMenu')->render([
'root_page' => $pages->get(1),
'current_page' => $page,
]);
```

Note: if you omit the root_page option, site root page is used by default – unless you've specified the menu_items
option, in which case a root page is not necessary at all. If you omit current_page, the menu will be rendered, but
current (active) page can't be highlighted etc.
Note: if you omit the root_page option, site root page is used by default – unless you've specified the menu_items option, in which case a root page is not necessary at all. If you omit current_page, the menu will be rendered, but current (active) page can't be highlighted etc.

Alternatively you can use MarkupMenu to render a predefined list of menu items by providing the menu_items option. Value of said option can be a PageArray or an array of arrays:

```php
echo $modules->get('MarkupMenu')->render([
'current_page' => $page,
'menu_items' => [
[
'title' => 'Home',
'url' => '/',
'id' => 1,
],
[
'title' => 'Search',
'url' => '/search/',
'id' => 26,
],
[
'title' => 'About',
'url' => '/about/',
'id' => 29,
],
],
]);
```

If you provide a predefined array of menu items, you can define current (active) page in the array (by default by assigning boolean true in the 'is_current' property for the active item), but if you instead provide 'current_page' and ID for each menu item like in above example, MarkupMenu can figure out active and parent pages automatically.

## Options

Below you'll find all the available options and their default values. You can override these defaults with the array
you pass to the render method, or you can specify an array of site-wide custom options via the site config setting
`$config->MarkupMenu`:
Below you'll find all the available options and their default values. You can override these defaults with the array you pass to the render method, or you can specify an array of site-wide custom options via the site config setting `$config->MarkupMenu`:

```
```php
$config->MarkupMenu = [

// 'root_page' is the starting point for the menu. This is optional if you specify the 'menu_items' option instead,
// but leaving *both* empty will make MarkupMenu::render() return an empty string.
// 'root_page' is the starting point for the menu. This is optional if you provide 'menu_items'
// instead, but leaving *both* empty will make MarkupMenu::render() return an empty string.
'root_page' => null,

// 'menu_items' is an optional, prepopulated PageArray of first level menu items.
Expand All @@ -58,6 +76,10 @@ $config->MarkupMenu = [
// - {class}: the template class only, mostly useful for adding a prefix to other classes
// - {item}: the item itself, i.e. a Page object and all its field values andproperties
// - {level}: the level of current item, represented by an integer starting from 1
//
// As of MarkupMenu 0.11.0 you can alternatively provide a callable (or an anonymous function)
// that returns the template string in question. This callback function receives the menu item
// and the options array as its arguments.
'templates' => [
'nav' => '<nav class="{classes}">%s</nav>',
'list' => '<ul class="{classes} {class}--level-{level}">%s</ul>',
Expand All @@ -73,37 +95,39 @@ $config->MarkupMenu = [
'root_page' => false,
],

// 'exclude' rules are the opposite of the include rules, and allow you to define the pages not included in the
// menu: pages matching a selector string, non-listable pages ('listable' value of false means that non-listable
// pages are excluded), and pages that would exceed a maximum level or depth ('level_greater_than').
// 'exclude' rules are the opposite of the include rules, and allow you to define the pages not
// included in the menu: pages matching a selector string, non-listable pages ('listable' value
// of false means that non-listable pages are excluded), and pages that would exceed a maximum
// level or depth ('level_greater_than').
//
// **NOTE**: exclude rules are applied *after* initial query, which makes them less performant than include rules;
// in particular one should always prefer the selector in the include rules over the one in the exclude rules.
// **NOTE**: exclude rules are applied *after* initial query, which makes them less performant
// than include rules; in particular one should always prefer the selector in the include rules
// over the one in the exclude rules.
'exclude' => [
'selector' => null,
'listable' => false,
'level_greater_than' => null,
],

// 'collapsed', in the lack of a better name, defines whether your menu should only be rendered up current (active)
// page, or first level if no current page was provided.
// 'collapsed', in the lack of a better name, defines whether your menu should only be rendered
// up current (active) page, or first level if no current page was provided.
'collapsed' => true,

// 'flat_root' is only useful if you've chosen to include the root page in the menu: this option puts the root page
// at the same level as your other first level pages – typically you'd want this, so that your home page shows up
// at the same level as the first level below it.
// 'flat_root' is only useful if you've chosen to include the root page in the menu: this puts
// the root page at the same level as your other first level pages – typically you'd want this,
// so that your home page shows up at the same level as the first level below it.
'flat_root' => true,

// 'placeholder_options' is an array of options that will be passed to WireTextTools or wirePopulateStringTags(),
// used for tag replacements in templates defined via the 'templates' option.
// 'placeholder_options' is an array of options that will be passed to wirePopulateStringTags()
// function, used for tag replacements in templates defined via the 'templates' option.
'placeholder_options' => [],

// 'placeholders' can be used to provide additional custom values for string replacements used within the template
// strings defined via the 'templates' option.
// 'placeholders' can be used to provide additional custom values for string replacements used
// within the template strings defined via the 'templates' option.
'placeholders' => [],

// 'classes' can be used to override default class names added to items when the {classes} tag is used in template
// strings defined via the 'templates' option.
// 'classes' can be used to override default class names added to items when the {classes} tag
// is used in template strings defined via the 'templates' option.
'classes' => [
// 'page_id' => '&--page-id-', // note: page_id is disabled by default!
'nav' => 'menu',
Expand All @@ -115,6 +139,15 @@ $config->MarkupMenu = [
'parent' => '&--parent',
'has_children' => '&--has-children',
],

// 'array_item_keys' are used to pull data from menu items when an array of menu items (arrays)
// is provided via the 'menu_items' option.
'array_item_keys' => [
'id' => 'id',
'is_current' => 'is_current',
'is_parent' => 'is_parent',
'children' => 'children',
],
];
```

Expand All @@ -123,13 +156,11 @@ $config->MarkupMenu = [
- ProcessWire >= 3.0.112
- PHP >= 7.1.0

If you're working on an earlier version of ProcessWire or PHP, I'd highly recommend checking out MarkupSimpleNavigation
module instead: https://github.com/somatonic/MarkupSimpleNavigation.
If you're working on an earlier version of ProcessWire or PHP, I'd highly recommend checking out MarkupSimpleNavigation module instead: https://github.com/somatonic/MarkupSimpleNavigation.

## Installing

This module can be installed just like any other ProcessWire module, by downloading or cloning the MarkupMenu directory
into your /site/modules/ directory. Alternatively you can install the module via Composer:
This module can be installed just like any other ProcessWire module, by downloading or cloning the MarkupMenu directory into your /site/modules/ directory. Alternatively you can install the module via Composer:

```
composer require teppokoivula/markup-menu
Expand Down

0 comments on commit 4ee01a8

Please sign in to comment.