Version 0.22
[awl.git] / inc / MenuSet.php
blob057c32c1e7c548906b0625e41b414a46a9254ddd
1 <?php
2 /**
3 * Some intelligence and standardisation around presenting a menu hierarchy.
5 * See the MenuSet class for examples as that is the primary interface.
6 * @see MenuSet
8 * @package awl
9 * @subpackage MenuSet
10 * @author Andrew McMillan <andrew@catalyst.net.nz>
11 * @copyright Catalyst IT Ltd
12 * @license http://gnu.org/copyleft/gpl.html GNU GPL v2
14 require_once("AWLUtilities.php");
16 /**
17 * Each menu option is an object.
18 * @package awl
20 class MenuOption {
21 /**#@+
22 * @access private
24 /**
25 * The label for the menu item
26 * @var string
28 var $label;
30 /**
31 * The target URL for the menu
32 * @var string
34 var $target;
36 /**
37 * The title for the item when moused over, which should be displayed as a tooltip.
38 * @var string
40 var $title;
42 /**
43 * Whether the menu option is active
44 * @var string
46 var $active;
48 /**
49 * For sorting menu options
50 * @var string
52 var $sortkey;
54 /**
55 * Style to render the menu option with.
56 * @var string
58 var $style;
60 /**
61 * The MenuSet that this menu is a parent of
62 * @var string
64 var $submenu_set;
65 /**#@-*/
67 /**
68 * A reference to this menu option itself
69 * @var reference
71 var $self;
73 /**#@+
74 * @access public
76 /**
77 * The rendered HTML fragment (once it has been).
78 * @var string
80 var $rendered;
81 /**#@-*/
83 /**
84 * The thing we click
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;
105 * Convert the menu option into an HTML string
106 * @return string The HTML fragment for the menu option.
108 function Render( ) {
109 $r = sprintf('<a href="%s" class="%s" title="%s"%s>%s</a>',
110 $this->target, $this->style, htmlspecialchars($this->title), "%%attributes%%",
111 htmlspecialchars($this->label), $this->style );
113 // Now process the generic attributes
114 $attribute_values = "";
115 foreach( $this->attributes AS $k => $v ) {
116 if ( substr($k, 0, 1) == '_' ) continue;
117 $attribute_values .= " $k=\"".htmlspecialchars($v)."\"";
119 $r = str_replace( '%%attributes%%', $attribute_values, $r );
121 $this->rendered = $r;
122 return "$r\n";
126 * Set arbitrary attributes of the menu option
127 * @param string $attribute An arbitrary attribute to be set in the hyperlink.
128 * @param string $value A value for this attribute.
130 function Set( $attribute, $value ) {
131 $this->attributes[$attribute] = $value;
135 * Mark it as active, with a fancy style to distinguish that
136 * @param string $style A style used to highlight that the option is active.
138 function Active( $style=false ) {
139 $this->active = true;
140 if ( $style ) $this->style = $style;
144 * This menu option is now promoted to the head of a tree
146 function AddSubmenu( &$submenu_set ) {
147 $this->submenu_set = &$submenu_set;
151 * Whether this option is currently active.
152 * @return boolean The value of the active flag.
154 function IsActive( ) {
155 return ( $this->active );
159 * Whether this option is currently active.
160 * @return boolean The value of the active flag.
162 function MaybeActive( $test_pattern, $active_style ) {
163 if ( is_string($test_pattern) && preg_match($test_pattern,$_SERVER['REQUEST_URI']) ) {
164 $this->Active($active_style);
166 return ( $this->active );
171 * _CompareMenuSequence is used in sorting the menu options into the sequence order
173 * @param objectref $a The first menu option
174 * @param objectref $b The second menu option
175 * @return int ( $a == b ? 0 ( $a > b ? 1 : -1 ))
177 function _CompareMenuSequence( $a, $b ) {
178 dbg_error_log("MenuSet", ":_CompareMenuSequence: Comparing %d with %d", $a->sortkey, $b->sortkey);
179 return ($a->sortkey - $b->sortkey);
185 * A MenuSet is a hierarchy of MenuOptions, some of which might be
186 * MenuSet objects themselves.
188 * The menu options are presented in HTML span tags, and the menus
189 * themselves are presented inside HTML div tags. All layout and
190 * styling is expected to be provide by CSS.
192 * A non-trivial example would look something like this:
193 *<code>
194 *require_once("MenuSet.php");
195 *$main_menu = new MenuSet('menu', 'menu', 'menu_active');
196 * ...
197 *$other_menu = new MenuSet('submenu', 'submenu', 'submenu_active');
198 *$other_menu->AddOption("Extra Other","/extraother.php","Submenu option to do extra things.");
199 *$other_menu->AddOption("Super Other","/superother.php","Submenu option to do super things.");
200 *$other_menu->AddOption("Meta Other","/metaother.php","Submenu option to do meta things.");
201 * ...
202 *$main_menu->AddOption("Do This","/dothis.php","Option to do this thing.");
203 *$main_menu->AddOption("Do That","/dothat.php","Option to do all of that.");
204 *$main_menu->AddSubMenu( $other_menu, "Do The Other","/dotheother.php","Submenu to do all of the other things.", true);
205 * ...
206 *if ( isset($main_menu) && is_object($main_menu) ) {
207 * $main_menu->AddOption("Home","/","Go back to the home page");
208 * echo $main_menu->Render();
210 *</code>
211 * In a hierarchical menu tree, like the example above, only one sub-menu will be
212 * shown, which will be the first one that is found to have active menu options.
214 * The menu display will generally recognise the current URL and mark as active the
215 * menu option that matches it, but in some cases it might be desirable to force one
216 * or another option to be marked as active using the appropriate parameter to the
217 * AddOption or AddSubMenu call.
218 * @package awl
220 class MenuSet {
221 /**#@+
222 * @access private
225 * CSS style to use for the div around the options
226 * @var string
228 var $div_id;
231 * CSS style to use for normal menu option
232 * @var string
234 var $main_class;
237 * CSS style to use for active menu option
238 * @var string
240 var $active_class;
243 * An array of MenuOption objects
244 * @var array
246 var $options;
249 * Any menu option that happens to parent this set
250 * @var reference
252 var $parent;
255 * Will be set to true or false when we link active sub-menus, but will be
256 * unset until we do that.
257 * @var reference
259 var $has_active_options;
260 /**#@-*/
263 * Start a new MenuSet with no options.
264 * @param string $div_id An ID for the HTML div that the menu will be presented in.
265 * @param string $main_class A CSS class for most menu options.
266 * @param string $active_class A CSS class for active menu options.
268 function MenuSet( $div_id, $main_class = '', $active_class = 'active' ) {
269 $this->options = array();
270 $this->main_class = $main_class;
271 $this->active_class = $active_class;
272 $this->div_id = $div_id;
276 * Add an option, which is a link.
277 * The call will attempt to work out whether the option should be marked as
278 * active, and will sometimes get it wrong.
279 * @param string $label A Label for the new menu option
280 * @param string $target The URL to target for this option.
281 * @param string $title Some tooltip help for the title tag.
282 * @param string $active Whether this option should be marked as Active.
283 * @param int $sortkey An (optional) value to allow option ordering.
284 * @return mixed A reference to the MenuOption that was added, or false if none were added.
286 function &AddOption( $label, $target, $title="", $active=false, $sortkey=1000 ) {
287 $new_option =& new MenuOption( $label, $target, $title, $this->main_class, $sortkey );
288 if ( ($old_option = $this->_OptionExists( $label )) === false ) {
289 $this->options[] = &$new_option ;
291 else {
292 dbg_error_log("MenuSet",":AddOption: Replacing existing option # $old_option ($label)");
293 $this->options[$old_option] = &$new_option; // Overwrite the existing option
295 if ( is_bool($active) && $active == false && $_SERVER['REQUEST_URI'] == $target ) {
296 // If $active is not set, then we look for an exact match to the current URL
297 $new_option->Active( $this->active_class );
299 else if ( is_bool($active) && $active ) {
300 // When active is specified as a boolean, the recognition has been done externally
301 $new_option->Active( $this->active_class );
303 else if ( is_string($active) && preg_match($active,$_SERVER['REQUEST_URI']) ) {
304 // If $active is a string, then we match the current URL to that as a Perl regex
305 $new_option->Active( $this->active_class );
307 return $new_option ;
311 * Add an option, which is a submenu
312 * @param object &$submenu_set A reference to a menu tree
313 * @param string $label A Label for the new menu option
314 * @param string $target The URL to target for this option.
315 * @param string $title Some tooltip help for the title tag.
316 * @param string $active Whether this option should be marked as Active.
317 * @param int $sortkey An (optional) value to allow option ordering.
318 * @return mixed A reference to the MenuOption that was added, or false if none were added.
320 function &AddSubMenu( &$submenu_set, $label, $target, $title="", $active=false, $sortkey=2000 ) {
321 $new_option =& $this->AddOption( $label, $target, $title, $active, $sortkey );
322 $submenu_set->parent = &$new_option ;
323 $new_option->AddSubmenu( $submenu_set );
324 return $new_option ;
328 * Does the menu have any options that are active.
329 * Most likely used so that we can then set the parent menu as active.
330 * @param string $label A Label for the new menu option
331 * @return boolean Whether the menu has options that are active.
333 function _HasActive( ) {
334 if ( isset($this->has_active_options) ) {
335 return $this->has_active_options;
337 foreach( $this->options AS $k => $v ) {
338 if ( $v->IsActive() ) {
339 $rc = true;
340 return $rc;
343 $rc = false;
344 return $rc;
348 * Find out how many options the menu has.
349 * @return int The number of options in the menu.
351 function Size( ) {
352 return count($this->options);
356 * See if a menu already has this option
357 * @return boolean Whether the option already exists in the menu.
359 function _OptionExists( $newlabel ) {
360 $rc = false;
361 foreach( $this->options AS $k => $v ) {
362 if ( $newlabel == $v->label ) return $k;
364 return $rc;
368 * Mark each MenuOption as active that has an active sub-menu entry.
370 * Currently needs to be called manually before rendering but
371 * really should probably be called as part of the render now,
372 * and then this could be a private routine.
374 function LinkActiveSubMenus( ) {
375 $this->has_active_options = false;
376 foreach( $this->options AS $k => $v ) {
377 if ( isset($v->submenu_set) && $v->submenu_set->_HasActive() ) {
378 // Note that we need to do it this way, since $v is a copy, not a reference
379 $this->options[$k]->Active( $this->active_class );
380 $this->has_active_options = true;
386 * Mark each MenuOption as active that has an active sub-menu entry.
388 * Currently needs to be called manually before rendering but
389 * really should probably be called as part of the render now,
390 * and then this could be a private routine.
392 function MakeSomethingActive( $test_pattern ) {
393 if ( $this->has_active_options ) return; // Already true.
394 foreach( $this->options AS $k => $v ) {
395 if ( isset($v->submenu_set) && $v->submenu_set->_HasActive() ) {
396 // Note that we need to do it this way, since $v is a copy, not a reference
397 $this->options[$k]->Active( $this->active_class );
398 $this->has_active_options = true;
399 return $this->has_active_options;
403 foreach( $this->options AS $k => $v ) {
404 if ( isset($v->submenu_set) && $v->submenu_set->MakeSomethingActive($test_pattern) ) {
405 // Note that we need to do it this way, since $v is a copy, not a reference
406 $this->options[$k]->Active( $this->active_class );
407 $this->has_active_options = true;
408 return $this->has_active_options;
410 else {
411 if ( $this->options[$k]->MaybeActive( $test_pattern, $this->active_class ) ) {
412 $this->has_active_options = true;
413 return $this->has_active_options;
417 return false;
421 * _CompareSequence is used in sorting the menu options into the sequence order
423 * @param objectref $a The first menu option
424 * @param objectref $b The second menu option
425 * @return int ( $a == b ? 0 ( $a > b ? 1 : -1 ))
427 function _CompareSequence( $a, $b ) {
428 dbg_error_log("MenuSet",":_CompareSequence: Comparing %d with %d", $a->sortkey, $b->sortkey);
429 return ($a->sortkey - $b->sortkey);
434 * Render the menu tree to an HTML fragment.
436 * @param boolean $submenus_inline Indicate whether to render the sub-menus within
437 * the menus, or render them entirely separately after we finish rendering the
438 * top level ones.
439 * @return string The HTML fragment.
441 function Render( $submenus_inline = false ) {
442 if ( !isset($this->has_active_options) ) {
443 $this->LinkActiveSubMenus();
445 $options = $this->options;
446 usort($options,"_CompareMenuSequence");
447 $render_sub_menus = false;
448 $r = "<div id=\"$this->div_id\">\n";
449 foreach( $options AS $k => $v ) {
450 $r .= $v->Render();
451 if ( $v->IsActive() && isset($v->submenu_set) && $v->submenu_set->Size() > 0 ) {
452 $render_sub_menus = $v->submenu_set;
453 if ( $submenus_inline )
454 $render_sub_menus->Render();
457 $r .="</div>\n";
458 if ( !$submenus_inline && $render_sub_menus != false ) {
459 $r .= $render_sub_menus->Render();
461 return $r;