3 * Some intelligence and standardisation around presenting a menu hierarchy.
5 * See the MenuSet class for examples as that is the primary interface.
10 * @author Andrew McMillan <andrew@mcmillan.net.nz>
11 * @copyright Catalyst IT Ltd, Morphoss Ltd <http://www.morphoss.com/>
12 * @license http://gnu.org/copyleft/gpl.html GNU GPL v2
14 require_once("AWLUtilities.php");
17 * Each menu option is an object.
25 * The label for the menu item
31 * The target URL for the menu
37 * The title for the item when moused over, which should be displayed as a tooltip.
43 * Whether the menu option is active
49 * For sorting menu options
55 * Style to render the menu option with.
61 * The MenuSet that this menu is a parent of
68 * A reference to this menu option itself
77 * The rendered HTML fragment (once it has been).
85 * @param string $label The label to display for this option.
86 * @param string $target The URL to target for this option.
87 * @param string $title Some tooltip help for the title tag.
88 * @param string $style A base class name for this option.
89 * @param int $sortkey An (optional) value to allow option ordering.
91 function MenuOption( $label, $target, $title="", $style="menu", $sortkey=1000 ) {
92 $this->label
= $label;
93 $this->target
= $target;
94 $this->title
= $title;
95 $this->style
= $style;
96 $this->attributes
= array();
97 $this->active
= false;
98 $this->sortkey
= $sortkey;
100 $this->rendered
= "";
101 $this->self
=& $this;
103 $this->external
= "";
107 * Convert the menu option into an HTML string
108 * @return string The HTML fragment for the menu option.
111 $r = sprintf('<a href="%s" class="%s" title="%s"%s%s>%s</a>',
112 $this->target
, $this->style
, htmlspecialchars($this->title
), "%%attributes%%",
113 htmlspecialchars($this->label
), $this->style
, $this->external
);
115 // Now process the generic attributes
116 $attribute_values = "";
117 foreach( $this->attributes
AS $k => $v ) {
118 if ( substr($k, 0, 1) == '_' ) continue;
119 $attribute_values .= " $k=\"".htmlspecialchars($v)."\"";
121 $r = str_replace( '%%attributes%%', $attribute_values, $r );
123 $this->rendered
= $r;
128 * Set arbitrary attributes of the menu option
129 * @param string $attribute An arbitrary attribute to be set in the hyperlink.
130 * @param string $value A value for this attribute.
132 function Set( $attribute, $value ) {
133 $this->attributes
[$attribute] = $value;
137 * Mark it as active, with a fancy style to distinguish that
138 * @param string $style A style used to highlight that the option is active.
140 function Active( $style=false ) {
141 $this->active
= true;
142 if ( $style ) $this->style
= $style;
146 * This menu option is now promoted to the head of a tree
148 function AddSubmenu( &$submenu_set ) {
149 $this->submenu_set
= &$submenu_set;
153 * Whether this option is currently active.
154 * @return boolean The value of the active flag.
156 function IsActive( ) {
157 return ( $this->active
);
161 * Whether this option is currently active.
162 * @return boolean The value of the active flag.
164 function MaybeActive( $test_pattern, $active_style ) {
165 if ( is_string($test_pattern) && preg_match($test_pattern,$_SERVER['REQUEST_URI']) ) {
166 $this->Active($active_style);
168 return ( $this->active
);
174 * _CompareMenuSequence is used in sorting the menu options into the sequence order
176 * @param objectref $a The first menu option
177 * @param objectref $b The second menu option
178 * @return int ( $a == b ? 0 ( $a > b ? 1 : -1 ))
180 function _CompareMenuSequence( $a, $b ) {
181 dbg_error_log("MenuSet", ":_CompareMenuSequence: Comparing %d with %d", $a->sortkey
, $b->sortkey
);
182 return ($a->sortkey
- $b->sortkey
);
188 * A MenuSet is a hierarchy of MenuOptions, some of which might be
189 * MenuSet objects themselves.
191 * The menu options are presented in HTML span tags, and the menus
192 * themselves are presented inside HTML div tags. All layout and
193 * styling is expected to be provide by CSS.
195 * A non-trivial example would look something like this:
197 *require_once("MenuSet.php");
198 *$main_menu = new MenuSet('menu', 'menu', 'menu_active');
200 *$other_menu = new MenuSet('submenu', 'submenu', 'submenu_active');
201 *$other_menu->AddOption("Extra Other","/extraother.php","Submenu option to do extra things.");
202 *$other_menu->AddOption("Super Other","/superother.php","Submenu option to do super things.");
203 *$other_menu->AddOption("Meta Other","/metaother.php","Submenu option to do meta things.");
205 *$main_menu->AddOption("Do This","/dothis.php","Option to do this thing.");
206 *$main_menu->AddOption("Do That","/dothat.php","Option to do all of that.");
207 *$main_menu->AddSubMenu( $other_menu, "Do The Other","/dotheother.php","Submenu to do all of the other things.", true);
209 *if ( isset($main_menu) && is_object($main_menu) ) {
210 * $main_menu->AddOption("Home","/","Go back to the home page");
211 * echo $main_menu->Render();
214 * In a hierarchical menu tree, like the example above, only one sub-menu will be
215 * shown, which will be the first one that is found to have active menu options.
217 * The menu display will generally recognise the current URL and mark as active the
218 * menu option that matches it, but in some cases it might be desirable to force one
219 * or another option to be marked as active using the appropriate parameter to the
220 * AddOption or AddSubMenu call.
228 * CSS style to use for the div around the options
234 * CSS style to use for normal menu option
240 * CSS style to use for active menu option
246 * An array of MenuOption objects
252 * Any menu option that happens to parent this set
258 * The sortkey used by any previous option
264 * Will be set to true or false when we link active sub-menus, but will be
265 * unset until we do that.
268 var $has_active_options;
272 * Start a new MenuSet with no options.
273 * @param string $div_id An ID for the HTML div that the menu will be presented in.
274 * @param string $main_class A CSS class for most menu options.
275 * @param string $active_class A CSS class for active menu options.
277 function MenuSet( $div_id, $main_class = '', $active_class = 'active' ) {
278 $this->options
= array();
279 $this->main_class
= $main_class;
280 $this->active_class
= $active_class;
281 $this->div_id
= $div_id;
285 * Add an option, which is a link.
286 * The call will attempt to work out whether the option should be marked as
287 * active, and will sometimes get it wrong.
288 * @param string $label A Label for the new menu option
289 * @param string $target The URL to target for this option.
290 * @param string $title Some tooltip help for the title tag.
291 * @param string $active Whether this option should be marked as Active.
292 * @param int $sortkey An (optional) value to allow option ordering.
293 * @param external open this link in a new window/tab.
294 * @return mixed A reference to the MenuOption that was added, or false if none were added.
296 function &AddOption( $label, $target, $title="", $active=false, $sortkey=null, $external=false ) {
297 if ( !isset($sortkey) ) {
298 $sortkey = (isset($this->last_sortkey
) ?
$this->last_sortkey +
100 : 1000);
300 $this->last_sortkey
= $sortkey;
301 if ( version_compare(phpversion(), '5.0') < 0) {
302 $new_option =& new MenuOption( $label, $target, $title, $this->main_class
, $sortkey );
305 $new_option = new MenuOption( $label, $target, $title, $this->main_class
, $sortkey );
307 if ( ($old_option = $this->_OptionExists( $label )) === false ) {
308 $this->options
[] = &$new_option ;
311 dbg_error_log("MenuSet",":AddOption: Replacing existing option # $old_option ($label)");
312 $this->options
[$old_option] = &$new_option; // Overwrite the existing option
314 if ( is_bool($active) && $active == false && $_SERVER['REQUEST_URI'] == $target ) {
315 // If $active is not set, then we look for an exact match to the current URL
316 $new_option->Active( $this->active_class
);
318 else if ( is_bool($active) && $active ) {
319 // When active is specified as a boolean, the recognition has been done externally
320 $new_option->Active( $this->active_class
);
322 else if ( is_string($active) && preg_match($active,$_SERVER['REQUEST_URI']) ) {
323 // If $active is a string, then we match the current URL to that as a Perl regex
324 $new_option->Active( $this->active_class
);
327 if ($external == true) {
328 $this->external
= " target=\"_blank\"";
335 * Add an option, which is a submenu
336 * @param object &$submenu_set A reference to a menu tree
337 * @param string $label A Label for the new menu option
338 * @param string $target The URL to target for this option.
339 * @param string $title Some tooltip help for the title tag.
340 * @param string $active Whether this option should be marked as Active.
341 * @param int $sortkey An (optional) value to allow option ordering.
342 * @return mixed A reference to the MenuOption that was added, or false if none were added.
344 function &AddSubMenu( &$submenu_set, $label, $target, $title="", $active=false, $sortkey=2000 ) {
345 $new_option =& $this->AddOption( $label, $target, $title, $active, $sortkey );
346 $submenu_set->parent
= &$new_option ;
347 $new_option->AddSubmenu( $submenu_set );
352 * Does the menu have any options that are active.
353 * Most likely used so that we can then set the parent menu as active.
354 * @param string $label A Label for the new menu option
355 * @return boolean Whether the menu has options that are active.
357 function _HasActive( ) {
358 if ( isset($this->has_active_options
) ) {
359 return $this->has_active_options
;
361 foreach( $this->options
AS $k => $v ) {
362 if ( $v->IsActive() ) {
372 * Find out how many options the menu has.
373 * @return int The number of options in the menu.
376 return count($this->options
);
380 * See if a menu already has this option
381 * @return boolean Whether the option already exists in the menu.
383 function _OptionExists( $newlabel ) {
385 foreach( $this->options
AS $k => $v ) {
386 if ( $newlabel == $v->label
) return $k;
392 * Mark each MenuOption as active that has an active sub-menu entry.
394 * Currently needs to be called manually before rendering but
395 * really should probably be called as part of the render now,
396 * and then this could be a private routine.
398 function LinkActiveSubMenus( ) {
399 $this->has_active_options
= false;
400 foreach( $this->options
AS $k => $v ) {
401 if ( isset($v->submenu_set
) && $v->submenu_set
->_HasActive() ) {
402 // Note that we need to do it this way, since $v is a copy, not a reference
403 $this->options
[$k]->Active( $this->active_class
);
404 $this->has_active_options
= true;
410 * Mark each MenuOption as active that has an active sub-menu entry.
412 * Currently needs to be called manually before rendering but
413 * really should probably be called as part of the render now,
414 * and then this could be a private routine.
416 function MakeSomethingActive( $test_pattern ) {
417 if ( $this->has_active_options
) return; // Already true.
418 foreach( $this->options
AS $k => $v ) {
419 if ( isset($v->submenu_set
) && $v->submenu_set
->_HasActive() ) {
420 // Note that we need to do it this way, since $v is a copy, not a reference
421 $this->options
[$k]->Active( $this->active_class
);
422 $this->has_active_options
= true;
423 return $this->has_active_options
;
427 foreach( $this->options
AS $k => $v ) {
428 if ( isset($v->submenu_set
) && $v->submenu_set
->MakeSomethingActive($test_pattern) ) {
429 // Note that we need to do it this way, since $v is a copy, not a reference
430 $this->options
[$k]->Active( $this->active_class
);
431 $this->has_active_options
= true;
432 return $this->has_active_options
;
435 if ( $this->options
[$k]->MaybeActive( $test_pattern, $this->active_class
) ) {
436 $this->has_active_options
= true;
437 return $this->has_active_options
;
445 * _CompareSequence is used in sorting the menu options into the sequence order
447 * @param objectref $a The first menu option
448 * @param objectref $b The second menu option
449 * @return int ( $a == b ? 0 ( $a > b ? 1 : -1 ))
451 function _CompareSequence( $a, $b ) {
452 dbg_error_log("MenuSet",":_CompareSequence: Comparing %d with %d", $a->sortkey
, $b->sortkey
);
453 return ($a->sortkey
- $b->sortkey
);
458 * Render the menu tree to an HTML fragment.
460 * @param boolean $submenus_inline Indicate whether to render the sub-menus within
461 * the menus, or render them entirely separately after we finish rendering the
463 * @return string The HTML fragment.
465 function Render( $submenus_inline = false ) {
466 if ( !isset($this->has_active_options
) ) {
467 $this->LinkActiveSubMenus();
469 $options = $this->options
;
470 usort($options,"_CompareMenuSequence");
471 $render_sub_menus = false;
472 $r = "<div id=\"$this->div_id\">\n";
473 foreach( $options AS $k => $v ) {
475 if ( $v->IsActive() && isset($v->submenu_set
) && $v->submenu_set
->Size() > 0 ) {
476 $render_sub_menus = $v->submenu_set
;
477 if ( $submenus_inline )
478 $r .= $render_sub_menus->Render();
482 if ( !$submenus_inline && $render_sub_menus != false ) {
483 $r .= $render_sub_menus->Render();
490 * Render the menu tree to an HTML fragment.
492 * @param boolean $submenus_inline Indicate whether to render the sub-menus within
493 * the menus, or render them entirely separately after we finish rendering the
495 * @return string The HTML fragment.
497 function RenderAsCSS( $depth = 0, $skip_empty = true ) {
498 $this->LinkActiveSubMenus();
501 $class = "submenu" . $depth;
505 $options = $this->options
;
506 usort($options,"_CompareMenuSequence");
508 $r = "<div id=\"$this->div_id\" class=\"$class\">\n<ul>\n";
509 foreach( $options AS $k => $v ) {
510 if ( $skip_empty && isset($v->submenu_set
) && $v->submenu_set
->Size() < 1 ) continue;
511 $r .= "<li>".$v->Render();
512 if ( isset($v->submenu_set
) && $v->submenu_set
->Size() > 0 ) {
513 $r .= $v->submenu_set
->RenderAsCSS($depth+
1);
517 $r .="</ul></div>\n";