* @copyright Catalyst IT Ltd, Morphoss Ltd
*require("MenuSet.php");
*$main_menu = new MenuSet('menu', 'menu', 'menu_active');
* ...
*$other_menu = new MenuSet('submenu', 'submenu', 'submenu_active');
*$other_menu->AddOption("Extra Other","/extraother.php","Submenu option to do extra things.");
*$other_menu->AddOption("Super Other","/superother.php","Submenu option to do super things.");
*$other_menu->AddOption("Meta Other","/metaother.php","Submenu option to do meta things.");
* ...
*$main_menu->AddOption("Do This","/dothis.php","Option to do this thing.");
*$main_menu->AddOption("Do That","/dothat.php","Option to do all of that.");
*$main_menu->AddSubMenu( $other_menu, "Do The Other","/dotheother.php","Submenu to do all of the other things.", true);
* ...
*if ( isset($main_menu) && is_object($main_menu) ) {
* $main_menu->AddOption("Home","/","Go back to the home page");
* echo $main_menu->Render();
*}
*
* In a hierarchical menu tree, like the example above, only one sub-menu will be
* shown, which will be the first one that is found to have active menu options.
*
* The menu display will generally recognise the current URL and mark as active the
* menu option that matches it, but in some cases it might be desirable to force one
* or another option to be marked as active using the appropriate parameter to the
* AddOption or AddSubMenu call.
* @package awl
*/
class MenuSet {
/**#@+
* @access private
*/
/**
* CSS style to use for the div around the options
* @var string
*/
var $div_id;
/**
* CSS style to use for normal menu option
* @var string
*/
var $main_class;
/**
* CSS style to use for active menu option
* @var string
*/
var $active_class;
/**
* An array of MenuOption objects
* @var array
*/
var $options;
/**
* Any menu option that happens to parent this set
* @var reference
*/
var $parent;
/**
* The sortkey used by any previous option
* @var last_sortkey
*/
var $last_sortkey;
/**
* Will be set to true or false when we link active sub-menus, but will be
* unset until we do that.
* @var reference
*/
var $has_active_options;
/**#@-*/
/**
* Start a new MenuSet with no options.
* @param string $div_id An ID for the HTML div that the menu will be presented in.
* @param string $main_class A CSS class for most menu options.
* @param string $active_class A CSS class for active menu options.
*/
function __construct( $div_id, $main_class = '', $active_class = 'active' ) {
$this->options = array();
$this->main_class = $main_class;
$this->active_class = $active_class;
$this->div_id = $div_id;
}
/**
* Add an option, which is a link.
* The call will attempt to work out whether the option should be marked as
* active, and will sometimes get it wrong.
* @param string $label A Label for the new menu option
* @param string $target The URL to target for this option.
* @param string $title Some tooltip help for the title tag.
* @param string $active Whether this option should be marked as Active.
* @param int $sortkey An (optional) value to allow option ordering.
* @param external open this link in a new window/tab.
* @return mixed A reference to the MenuOption that was added, or false if none were added.
*/
function &AddOption( $label, $target, $title="", $active=false, $sortkey=null, $external=false ) {
if ( !isset($sortkey) ) {
$sortkey = (isset($this->last_sortkey) ? $this->last_sortkey + 100 : 1000);
}
$this->last_sortkey = $sortkey;
if ( version_compare(phpversion(), '5.0') < 0) {
$new_option = new MenuOption( $label, $target, $title, $this->main_class, $sortkey );
}
else {
$new_option = new MenuOption( $label, $target, $title, $this->main_class, $sortkey );
}
if ( ($old_option = $this->_OptionExists( $label )) === false ) {
$this->options[] = &$new_option ;
}
else {
dbg_error_log("MenuSet",":AddOption: Replacing existing option # $old_option ($label)");
$this->options[$old_option] = &$new_option; // Overwrite the existing option
}
if ( is_bool($active) && $active == false && $_SERVER['REQUEST_URI'] == $target ) {
// If $active is not set, then we look for an exact match to the current URL
$new_option->Active( $this->active_class );
}
else if ( is_bool($active) && $active ) {
// When active is specified as a boolean, the recognition has been done externally
$new_option->Active( $this->active_class );
}
else if ( is_string($active) && preg_match($active,$_SERVER['REQUEST_URI']) ) {
// If $active is a string, then we match the current URL to that as a Perl regex
$new_option->Active( $this->active_class );
}
if ( $external == true ) $new_option->Set('target', '_blank');
return $new_option ;
}
/**
* Add an option, which is a submenu
* @param object &$submenu_set A reference to a menu tree
* @param string $label A Label for the new menu option
* @param string $target The URL to target for this option.
* @param string $title Some tooltip help for the title tag.
* @param string $active Whether this option should be marked as Active.
* @param int $sortkey An (optional) value to allow option ordering.
* @return mixed A reference to the MenuOption that was added, or false if none were added.
*/
function &AddSubMenu( &$submenu_set, $label, $target, $title="", $active=false, $sortkey=2000 ) {
$new_option =& $this->AddOption( $label, $target, $title, $active, $sortkey );
$submenu_set->parent = &$new_option ;
$new_option->AddSubmenu( $submenu_set );
return $new_option ;
}
/**
* Does the menu have any options that are active.
* Most likely used so that we can then set the parent menu as active.
* @param string $label A Label for the new menu option
* @return boolean Whether the menu has options that are active.
*/
function _HasActive( ) {
if ( isset($this->has_active_options) ) {
return $this->has_active_options;
}
foreach( $this->options AS $k => $v ) {
if ( $v->IsActive() ) {
$rc = true;
return $rc;
}
}
$rc = false;
return $rc;
}
/**
* Find out how many options the menu has.
* @return int The number of options in the menu.
*/
function Size( ) {
return count($this->options);
}
/**
* See if a menu already has this option
* @return boolean Whether the option already exists in the menu.
*/
function _OptionExists( $newlabel ) {
$rc = false;
foreach( $this->options AS $k => $v ) {
if ( $newlabel == $v->label ) return $k;
}
return $rc;
}
/**
* Mark each MenuOption as active that has an active sub-menu entry.
*
* Currently needs to be called manually before rendering but
* really should probably be called as part of the render now,
* and then this could be a private routine.
*/
function LinkActiveSubMenus( ) {
$this->has_active_options = false;
foreach( $this->options AS $k => $v ) {
if ( isset($v->submenu_set) && $v->submenu_set->_HasActive() ) {
// Note that we need to do it this way, since $v is a copy, not a reference
$this->options[$k]->Active( $this->active_class );
$this->has_active_options = true;
}
}
}
/**
* Mark each MenuOption as active that has an active sub-menu entry.
*
* Currently needs to be called manually before rendering but
* really should probably be called as part of the render now,
* and then this could be a private routine.
*/
function MakeSomethingActive( $test_pattern ) {
if ( $this->has_active_options ) return; // Already true.
foreach( $this->options AS $k => $v ) {
if ( isset($v->submenu_set) && $v->submenu_set->_HasActive() ) {
// Note that we need to do it this way, since $v is a copy, not a reference
$this->options[$k]->Active( $this->active_class );
$this->has_active_options = true;
return $this->has_active_options;
}
}
foreach( $this->options AS $k => $v ) {
if ( isset($v->submenu_set) && $v->submenu_set->MakeSomethingActive($test_pattern) ) {
// Note that we need to do it this way, since $v is a copy, not a reference
$this->options[$k]->Active( $this->active_class );
$this->has_active_options = true;
return $this->has_active_options;
}
else {
if ( $this->options[$k]->MaybeActive( $test_pattern, $this->active_class ) ) {
$this->has_active_options = true;
return $this->has_active_options;
}
}
}
return false;
}
/**
* _CompareSequence is used in sorting the menu options into the sequence order
*
* @param objectref $a The first menu option
* @param objectref $b The second menu option
* @return int ( $a == b ? 0 ( $a > b ? 1 : -1 ))
*/
function _CompareSequence( $a, $b ) {
dbg_error_log("MenuSet",":_CompareSequence: Comparing %d with %d", $a->sortkey, $b->sortkey);
return ($a->sortkey - $b->sortkey);
}
/**
* Render the menu tree to an HTML fragment.
*
* @param boolean $submenus_inline Indicate whether to render the sub-menus within
* the menus, or render them entirely separately after we finish rendering the
* top level ones.
* @return string The HTML fragment.
*/
function Render( $submenus_inline = false ) {
if ( !isset($this->has_active_options) ) {
$this->LinkActiveSubMenus();
}
$options = $this->options;
usort($options,"_CompareMenuSequence");
$render_sub_menus = false;
$r = "