1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518:
<?php
/**
* Some intelligence and standardisation around presenting a menu hierarchy.
*
* See the MenuSet class for examples as that is the primary interface.
* @see MenuSet
*
* @package awl
* @subpackage MenuSet
* @author Andrew McMillan <andrew@mcmillan.net.nz>
* @copyright Catalyst IT Ltd, Morphoss Ltd <http://www.morphoss.com/>
* @license http://gnu.org/copyleft/gpl.html GNU GPL v2 or later
*/
require_once("AWLUtilities.php");
/**
* Each menu option is an object.
* @package awl
*/
class MenuOption {
/**#@+
* @access private
*/
/**
* The label for the menu item
* @var string
*/
var $label;
/**
* The target URL for the menu
* @var string
*/
var $target;
/**
* The title for the item when moused over, which should be displayed as a tooltip.
* @var string
*/
var $title;
/**
* Whether the menu option is active
* @var string
*/
var $active;
/**
* For sorting menu options
* @var string
*/
var $sortkey;
/**
* Style to render the menu option with.
* @var string
*/
var $style;
/**
* The MenuSet that this menu is a parent of
* @var string
*/
var $submenu_set;
/**#@-*/
/**
* A reference to this menu option itself
* @var reference
*/
var $self;
/**#@+
* @access public
*/
/**
* The rendered HTML fragment (once it has been).
* @var string
*/
var $rendered;
/**#@-*/
/**
* The thing we click
* @param string $label The label to display for this option.
* @param string $target The URL to target for this option.
* @param string $title Some tooltip help for the title tag.
* @param string $style A base class name for this option.
* @param int $sortkey An (optional) value to allow option ordering.
*/
function MenuOption( $label, $target, $title="", $style="menu", $sortkey=1000 ) {
$this->label = $label;
$this->target = $target;
$this->title = $title;
$this->style = $style;
$this->attributes = array();
$this->active = false;
$this->sortkey = $sortkey;
$this->rendered = "";
$this->self =& $this;
}
/**
* Convert the menu option into an HTML string
* @return string The HTML fragment for the menu option.
*/
function Render( ) {
$r = sprintf('<a href="%s" class="%s" title="%s"%s>%s</a>',
$this->target, $this->style, htmlspecialchars($this->title), "%%attributes%%",
htmlspecialchars($this->label), $this->style );
// Now process the generic attributes
$attribute_values = "";
foreach( $this->attributes AS $k => $v ) {
if ( substr($k, 0, 1) == '_' ) continue;
$attribute_values .= ' '.$k.'="'.htmlspecialchars($v).'"';
}
$r = str_replace( '%%attributes%%', $attribute_values, $r );
$this->rendered = $r;
return "$r";
}
/**
* Set arbitrary attributes of the menu option
* @param string $attribute An arbitrary attribute to be set in the hyperlink.
* @param string $value A value for this attribute.
*/
function Set( $attribute, $value ) {
$this->attributes[$attribute] = $value;
}
/**
* Mark it as active, with a fancy style to distinguish that
* @param string $style A style used to highlight that the option is active.
*/
function Active( $style=false ) {
$this->active = true;
if ( $style ) $this->style = $style;
}
/**
* This menu option is now promoted to the head of a tree
*/
function AddSubmenu( &$submenu_set ) {
$this->submenu_set = &$submenu_set;
}
/**
* Whether this option is currently active.
* @return boolean The value of the active flag.
*/
function IsActive( ) {
return ( $this->active );
}
/**
* Whether this option is currently active.
* @return boolean The value of the active flag.
*/
function MaybeActive( $test_pattern, $active_style ) {
if ( is_string($test_pattern) && preg_match($test_pattern,$_SERVER['REQUEST_URI']) ) {
$this->Active($active_style);
}
return ( $this->active );
}
}
/**
* _CompareMenuSequence 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 _CompareMenuSequence( $a, $b ) {
dbg_error_log("MenuSet", ":_CompareMenuSequence: Comparing %d with %d", $a->sortkey, $b->sortkey);
return ($a->sortkey - $b->sortkey);
}
/**
* A MenuSet is a hierarchy of MenuOptions, some of which might be
* MenuSet objects themselves.
*
* The menu options are presented in HTML span tags, and the menus
* themselves are presented inside HTML div tags. All layout and
* styling is expected to be provide by CSS.
*
* A non-trivial example would look something like this:
*<code>
*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();
*}
*</code>
* 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 MenuSet( $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 = "<div id=\"$this->div_id\">\n";
foreach( $options AS $k => $v ) {
$r .= $v->Render();
if ( $v->IsActive() && isset($v->submenu_set) && $v->submenu_set->Size() > 0 ) {
$render_sub_menus = $v->submenu_set;
if ( $submenus_inline )
$r .= $render_sub_menus->Render();
}
}
$r .="</div>\n";
if ( !$submenus_inline && $render_sub_menus != false ) {
$r .= $render_sub_menus->Render();
}
return $r;
}
/**
* 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 RenderAsCSS( $depth = 0, $skip_empty = true ) {
$this->LinkActiveSubMenus();
if ( $depth > 0 )
$class = "submenu" . $depth;
else
$class = "menu";
$options = $this->options;
usort($options,"_CompareMenuSequence");
$r = "<div id=\"$this->div_id\" class=\"$class\">\n<ul>\n";
foreach( $options AS $k => $v ) {
if ( $skip_empty && isset($v->submenu_set) && $v->submenu_set->Size() < 1 ) continue;
$r .= "<li>".$v->Render();
if ( isset($v->submenu_set) && $v->submenu_set->Size() > 0 ) {
$r .= $v->submenu_set->RenderAsCSS($depth+1);
}
$r .= "</li>\n";
}
$r .="</ul></div>\n";
return $r;
}
}