<?php
/**
* This file implements the Menu class.
*
* This file is part of the evoCore framework - {@link http://evocore.net/}
* See also {@link https://github.com/b2evolution/b2evolution}.
*
* @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
*
* @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
* Parts of this file are copyright (c)2004-2006 by Daniel HAHLER - {@link http://thequod.de/contact}.
*
* @package evocore
*/
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
load_class( '_core/ui/_uiwidget.class.php', 'Widget' );
/**
* Menu class
*
* @package evocore
*/
class Menu extends Widget
{
/**
* The menu structure (array of arrays)
*
* Use {@link add_menu_entries()} to add them here.
*
* @access protected
* @var array
*/
var $_menus = array();
/**
* Add menu entries to the list of entries for a given path.
*
* @param NULL|string|array The path to add the entries to. See {@link get_node_by_path()}.
* @param array Menu entries to add (key (string) => entry (array)).
* An entry can have the following keys:
* 'text': Text/Caption for this entry.
* 'href': The link for this entry.
* 'entries': array of sub-entries
* DEPRECATED 'style': CSS style for this entry.
* DEPRECATED 'onclick': onclick property for this entry.
* DEPRECATED 'name': name attribute of the link/entry.
* @param string Node name after which we should insert the menu entries, NULL - to insert to the end
*/
function add_menu_entries( $path, $new_entries, $after_node = NULL )
{
// Get a reference to the node in the menu list.
$node = & $this->get_node_by_path( $path, true );
/*
if( !is_array($node) )
{
debug_die( 'add_menu_entries() with non-existing path!' );
}
*/
$new_entires_are_inserted = false;
if( !is_null( $after_node ) )
{ // We should insert new entries after specific node
$new_node = $node;
$new_node['entries'] = array();
if( isset( $node['entries'] ) )
{
foreach( $node['entries'] as $node_key => $node_entry )
{
$new_node['entries'][ $node_key ] = $node_entry;
if( $node_key == $after_node )
{ // Insert new entires here after specific node
foreach( $new_entries as $l_key => $l_new_entry )
{
$new_node['entries'][$l_key] = $l_new_entry;
}
$new_entires_are_inserted = true;
}
}
}
$node = $new_node;
}
if( !$new_entires_are_inserted )
{ // Insert new entries to the end if they are still not inserted after specific node
foreach( $new_entries as $l_key => $l_new_entry )
{
$node['entries'][$l_key] = $l_new_entry;
}
}
}
/**
* Insert new menu entries right after the menu entry passed as path
*
* @param NULL|string|array The path. See {@link get_node_by_path()}.
* @param new entries
* @param position after which to insert new entries. If not given, new entries is inserted after Path
* @returns boolean Whether inserting was successfull.
*/
function insert_menu_entries_after( $path, $new_entries, $index = false )
{
$menu_item = '';
if( $index === false )
{ // get menu item after which to insert new entries
if( is_array( $path ) )
{
$menu_item = array_pop( $path );
}
elseif( is_string( $path ) )
{
$menu_item = $path;
$path = NULL;
}
}
$menu = & $this->get_node_by_path( $path );
if( $menu === false )
{ // no such path
return false;
}
$entries = & $menu['entries'];
if( $menu_item )
{ // find index of menu itemafter which to insert new entries
$keys = array_keys( $entries );
if( !empty( $keys ) ) $index = array_search( $menu_item, $keys, true );
}
if( ( $index === false ) || ($index === NULL) )
{
return false;
}
// make new menu entries
$menu['entries'] = array_merge(
array_slice( $entries, 0, $index + 1 ),
$new_entries,
array_slice( $entries, $index )
);
return true;
}
/**
* Get the reference of a node from the menu entries using a path.
*
* @param array|string|NULL The path. NULL means root, string means child of root,
* array means path below root. (eg <code>array('options', 'general')</code>).
* @param boolean Should the node be created if it does not exist already?
* @return array|false The node as array or false, if the path does not exist (and we do not $createIfNotExisting).
*/
function & get_node_by_path( $path, $createIfNotExisting = false )
{
if( is_null($path) )
{ // root element
$path = array();
}
elseif( ! is_array($path) )
{
$path = array($path);
}
$node = & $this->_menus;
foreach( $path as $lStep )
{
if( ! isset($node['entries'][$lStep]) )
{
if( $createIfNotExisting )
{
$node['entries'][$lStep] = array();
}
else
{
$r = false;
return $r;
}
}
$node = & $node['entries'][$lStep];
}
return $node;
}
/**
* Get menu entries for a given path.
*
* @param NULL|string|array The path. See {@link get_node_by_path()}.
* @return array The menu entries (may be empty).
*/
function get_menu_entries( $path )
{
$node = & $this->get_node_by_path( $path );
return isset( $node['entries'] ) ? $node['entries'] : array();
}
/**
* Get the key of a selected entry for a path.
*
* @param NULL|string|array The path. See {@link get_node_by_path()}.
* @return string|false
*/
function get_selected( $path )
{
$node = & $this->get_node_by_path($path);
if( isset($node['selected']) )
{
return $node['selected'];
}
return false;
}
/**
* Get the HTML for the menu entries of a specific path.
*
* @param NULL|string|array The path. See {@link get_node_by_path()}.
* @param string Template name, see {@link get_template()}.
* @param integer Level
* @param boolean TRUE - to get template for empty menu, used to hide menus
* @return string The HTML for the menu.
*/
function get_html_menu( $path = NULL, $template = 'main', $level = 0, $force_empty = false )
{
$r = '';
if( is_null($path) )
{
$path = array();
}
elseif( ! is_array( $path ) )
{
$path = array( $path );
}
$templateForLevel = $this->get_template( $template, $level );
if( $force_empty || !( $menuEntries = $this->get_menu_entries($path) ) )
{ // No menu entries at this level
if( isset($templateForLevel['empty']) )
{
$r .= $templateForLevel['empty'];
}
}
else
{ // There are entries to display:
$r .= $templateForLevel['before'];
$selected = $this->get_selected($path);
foreach( $menuEntries as $loop_key => $loop_details )
{
if( empty( $loop_details ) )
{ // Empty placeholder, skip it. Might happen if the files module is disabled for example, then we had a file placeholder
// in the blog menu that will never be used. So don't display it...
continue;
}
if( !empty( $loop_details['separator'] ) )
{ // Separator
$r .= $templateForLevel['separator'];
continue;
}
if( ! empty( $loop_details['entries'] ) )
{ // Has sub-entries, check if any sub-entry can be displayed
$has_entry = false;
foreach( $loop_details['entries'] as $entry_key => $entry_details )
{
if( !empty( $entry_details ) )
{ // Menu has non-empty entry
$has_entry = true;
break;
}
}
if( ! $has_entry )
{
continue;
}
}
// Menu entry
if( isset( $loop_details['href'] ) )
{
$href = $loop_details['href'];
}
elseif( ! empty( $loop_details['href_eval'] ) )
{ // Useful for passing dynamic context vars (fp>> I AM using it)
$href = eval( $loop_details['href_eval'] );
}
else
{
$href = NULL;
}
$anchor = '<a';
if( !empty($href) )
{
$anchor .= ' href="'.$href.'"';
}
if( isset($loop_details['target']) )
{
$anchor .= ' target="'.$loop_details['target'].'"';
}
if( isset($loop_details['rel']) )
{
$anchor .= ' rel="'.$loop_details['rel'].'"';
}
if( isset($loop_details['style']) )
{
$anchor .= ' style="'.$loop_details['style'].'"';
}
if( isset($loop_details['onclick']) )
{
$anchor .= ' onclick="'.$loop_details['onclick'].'"';
}
if( isset($loop_details['name']) )
{
$anchor .= ' name="'.$loop_details['name'].'"';
}
if( isset($loop_details['title']) )
{
$anchor .= ' title="'.$loop_details['title'].'"';
}
if( isset($loop_details['shortcut']) )
{
$anchor .= ' data-shortcut="'.$loop_details['shortcut'].'"';
}
if( isset($loop_details['shortcut-top']) )
{
$anchor .= ' data-shortcut-top="'.$loop_details['shortcut-top'].'"';
}
// CLASS
$class = '';
if( !empty( $loop_details['class'] ) )
{ // disabled
$class .= ' '.$loop_details['class'];
}
if( !empty( $loop_details['disabled'] ) )
{ // disabled
$class .= ' '.$templateForLevel['disabled_class'];
}
if( ! empty( $class ) )
{ // disabled
$anchor .= ' class="'.trim($class).'"';
}
$anchor .= '>'.(isset($loop_details['text']) ? format_to_output( $loop_details['text'], 'htmlbody' ) : '?');
$anchor_end = '</a>';
if( $loop_key == $selected )
{ // Highlight selected entry
if( isset( $templateForLevel['_props']['recurse'] )
&& $templateForLevel['_props']['recurse'] != 'no'
&& ( $recursePath = array_merge( $path, array($loop_key) ) )
&& $this->get_menu_entries($recursePath) )
{
$r .= isset($templateForLevel['beforeEachSelWithSub']) ? $templateForLevel['beforeEachSelWithSub'] : $templateForLevel['beforeEachSel'];
$r .= $anchor;
if( $templateForLevel['_props']['recurse'] != 'no' && // Recurse:
( ! isset( $templateForLevel['_props']['recurse_level'] ) ||
( isset( $templateForLevel['_props']['recurse_level'] ) &&
$templateForLevel['_props']['recurse_level'] > $level + 1 ) ) )
{ // Display submenus if this level is not limited by param 'recurse_level'
$r .= $templateForLevel['arrow_level_'.( $level + 1 )].'</a>';
$r .= $this->get_html_menu( $recursePath, $template, $level+1 );
}
else
{ // End anchor without sub menus
$r .= $anchor_end;
}
$r .= isset($templateForLevel['afterEachSelWithSub']) ? $templateForLevel['afterEachSelWithSub'] : $templateForLevel['afterEachSel'];
}
else
{
if( isset( $loop_details['order'] ) && $loop_details['order'] == 'group_last' &&
isset( $templateForLevel['beforeEachSelGrpLast'], $templateForLevel['afterEachSelGrpLast'] ) )
{ // This selected menu item is last in a group
$r .= $templateForLevel['beforeEachSelGrpLast'];
$r .= $anchor.$anchor_end;
$r .= $templateForLevel['afterEachSelGrpLast'];
}
else
{ // Normal selected menu item
$r .= $templateForLevel['beforeEachSel'];
$r .= $anchor.$anchor_end;
$r .= $templateForLevel['afterEachSel'];
}
}
}
else
{ // Not selected entry
if( isset( $templateForLevel['_props']['recurse'] )
&& $templateForLevel['_props']['recurse'] == 'always'
&& ( $recursePath = array_merge( $path, array($loop_key) ) )
&& $this->get_menu_entries($recursePath) )
{
$r .= isset($templateForLevel['beforeEachWithSub']) ? $templateForLevel['beforeEachWithSub'] : $templateForLevel['beforeEachSel'];
$r .= $anchor.$templateForLevel['arrow_level_'.( $level + 1 )].'</a>';
// recurse:
$r .= $this->get_html_menu( $recursePath, $template, $level+1 );
$r .= isset($templateForLevel['afterEachWithSub']) ? $templateForLevel['afterEachWithSub'] : $templateForLevel['afterEachSel'];
}
else
{
if( isset( $loop_details['order'] ) && $loop_details['order'] == 'group_last' &&
isset( $templateForLevel['beforeEachGrpLast'], $templateForLevel['afterEachGrpLast'] ) )
{ // This menu item is last in a group
$r .= $templateForLevel['beforeEachGrpLast'];
$r .= $anchor.$anchor_end;
$r .= $templateForLevel['afterEachGrpLast'];
}
else
{ // Normal menu item
$r .= $templateForLevel['beforeEach'];
$r .= $anchor.$anchor_end;
$r .= $templateForLevel['afterEach'];
}
}
}
// Additional attribures for each menu entry
$entry_attrs = '';
$entry_class = '';
if( ! empty( $loop_details['entry_class'] ) )
{ // Css class for entry
if( strpos( $r, '$entry_class$' ) === false )
{
$entry_attrs .= ' class="'.$loop_details['entry_class'].'"';
}
$entry_class .= ' '.$loop_details['entry_class'];
}
$r = str_replace( array( '$entry_attrs$', '$entry_class$' ), array( $entry_attrs, $entry_class ), $r );
}
$r .= $templateForLevel['after'];
}
return $r;
}
/**
* Get a template by name.
*
* This is a method (and not a member array) to allow dynamic generation and T_()
*
* @param string Name of the template ('main', 'sub')
* @return array Associative array which defines layout and optionally properties.
*/
function get_template( $name, $level = 0 )
{
switch( $name )
{
case 'evobar-menu-right':
$arrow_level_2 = '<span class="evobar-icon-left fa fa-caret-left"></span>';
case 'evobar-menu-left':
return array(
'before' => '<ul class="evobar-menu '.$name.'">',
'after' => '</ul>',
'beforeEach' => '<li$entry_attrs$>',
'afterEach' => '</li>',
'beforeEachSel' => '<li class="current$entry_class$"$entry_attrs$>',
'afterEachSel' => '</li>',
'separator' => '<li class="separator"><hr /></li>',
'arrow_level_1' => '<span class="evobar-icon-down fa fa-caret-down"></span>',
'arrow_level_2' => isset( $arrow_level_2 ) ? $arrow_level_2 : '<span class="evobar-icon-right fa fa-caret-right"></span>',
'disabled_class' => 'disabled',
'_props' => array(
'recurse' => 'always', // options are: 'no' 'always' or 'intoselected'
),
);
break;
default:
debug_die( 'Unknown $name for Menu::get_template(): '.var_export($name, true) );
}
}
/**
* Check if menu is empty or contains at least one entry
*
* @return boolean true if the menu is not empty | false otherwise
*/
function has_entires()
{
return !empty( $this->_menus );
}
/**
* Clear menu entries of the given path.
*
* @param NULL|string The path to clear the entries from. See {@link get_node_by_path()}.
*/
function clear_menu_entries( $path )
{
// Get a reference to the node in the menu list.
$node = & $this->get_node_by_path( $path, true );
$node['entries'] = array();
}
}
?>