Handle htmlspecialchars() differences between PHP < 5.4 vs >= 5.4
[awl.git] / inc / MenuSet.php
blobc2942035f60d75f9d29386c535bbd0b2d7156f26
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@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 or later
15 require_once("AWLUtilities.php");
17 /**
18 * Each menu option is an object.
19 * @package awl
21 class MenuOption {
22 /**#@+
23 * @access private
25 /**
26 * The label for the menu item
27 * @var string
29 var $label;
31 /**
32 * The target URL for the menu
33 * @var string
35 var $target;
37 /**
38 * The title for the item when moused over, which should be displayed as a tooltip.
39 * @var string
41 var $title;
43 /**
44 * Whether the menu option is active
45 * @var string
47 var $active;
49 /**
50 * For sorting menu options
51 * @var string
53 var $sortkey;
55 /**
56 * Style to render the menu option with.
57 * @var string
59 var $style;
61 /**
62 * The MenuSet that this menu is a parent of
63 * @var string
65 var $submenu_set;
66 /**#@-*/
68 /**
69 * A reference to this menu option itself
70 * @var reference
72 var $self;
74 /**#@+
75 * @access public
77 /**
78 * The rendered HTML fragment (once it has been).
79 * @var string
81 var $rendered;
82 /**#@-*/
84 /**
85 * The thing we click
86 * @param string $label The label to display for this option.
87 * @param string $target The URL to target for this option.
88 * @param string $title Some tooltip help for the title tag.
89 * @param string $style A base class name for this option.
90 * @param int $sortkey An (optional) value to allow option ordering.
92 function MenuOption( $label, $target, $title="", $style="menu", $sortkey=1000 ) {
93 $this->label = $label;
94 $this->target = $target;
95 $this->title = $title;
96 $this->style = $style;
97 $this->attributes = array();
98 $this->active = false;
99 $this->sortkey = $sortkey;
101 $this->rendered = "";
102 $this->self =& $this;
106 * Convert the menu option into an HTML string
107 * @return string The HTML fragment for the menu option.
109 function Render( ) {
110 $r = sprintf('<a href="%s" class="%s" title="%s"%s>%s</a>',
111 $this->target, $this->style, htmlspecialchars($this->title), "%%attributes%%",
112 htmlspecialchars($this->label), $this->style );
114 // Now process the generic attributes
115 $attribute_values = "";
116 foreach( $this->attributes AS $k => $v ) {
117 if ( substr($k, 0, 1) == '_' ) continue;
118 $attribute_values .= ' '.$k.'="'.htmlspecialchars($v).'"';
120 $r = str_replace( '%%attributes%%', $attribute_values, $r );
122 $this->rendered = $r;
123 return "$r";
127 * Set arbitrary attributes of the menu option
128 * @param string $attribute An arbitrary attribute to be set in the hyperlink.
129 * @param string $value A value for this attribute.
131 function Set( $attribute, $value ) {
132 $this->attributes[$attribute] = $value;
136 * Mark it as active, with a fancy style to distinguish that
137 * @param string $style A style used to highlight that the option is active.
139 function Active( $style=false ) {
140 $this->active = true;
141 if ( $style ) $this->style = $style;
145 * This menu option is now promoted to the head of a tree
147 function AddSubmenu( &$submenu_set ) {
148 $this->submenu_set = &$submenu_set;
152 * Whether this option is currently active.
153 * @return boolean The value of the active flag.
155 function IsActive( ) {
156 return ( $this->active );
160 * Whether this option is currently active.
161 * @return boolean The value of the active flag.
163 function MaybeActive( $test_pattern, $active_style ) {
164 if ( is_string($test_pattern) && preg_match($test_pattern,$_SERVER['REQUEST_URI']) ) {
165 $this->Active($active_style);
167 return ( $this->active );
173 * _CompareMenuSequence is used in sorting the menu options into the sequence order
175 * @param objectref $a The first menu option
176 * @param objectref $b The second menu option
177 * @return int ( $a == b ? 0 ( $a > b ? 1 : -1 ))
179 function _CompareMenuSequence( $a, $b ) {
180 dbg_error_log("MenuSet", ":_CompareMenuSequence: Comparing %d with %d", $a->sortkey, $b->sortkey);
181 return ($a->sortkey - $b->sortkey);
187 * A MenuSet is a hierarchy of MenuOptions, some of which might be
188 * MenuSet objects themselves.
190 * The menu options are presented in HTML span tags, and the menus
191 * themselves are presented inside HTML div tags. All layout and
192 * styling is expected to be provide by CSS.
194 * A non-trivial example would look something like this:
195 *<code>
196 *require("MenuSet.php");
197 *$main_menu = new MenuSet('menu', 'menu', 'menu_active');
198 * ...
199 *$other_menu = new MenuSet('submenu', 'submenu', 'submenu_active');
200 *$other_menu->AddOption("Extra Other","/extraother.php","Submenu option to do extra things.");
201 *$other_menu->AddOption("Super Other","/superother.php","Submenu option to do super things.");
202 *$other_menu->AddOption("Meta Other","/metaother.php","Submenu option to do meta things.");
203 * ...
204 *$main_menu->AddOption("Do This","/dothis.php","Option to do this thing.");
205 *$main_menu->AddOption("Do That","/dothat.php","Option to do all of that.");
206 *$main_menu->AddSubMenu( $other_menu, "Do The Other","/dotheother.php","Submenu to do all of the other things.", true);
207 * ...
208 *if ( isset($main_menu) && is_object($main_menu) ) {
209 * $main_menu->AddOption("Home","/","Go back to the home page");
210 * echo $main_menu->Render();
212 *</code>
213 * In a hierarchical menu tree, like the example above, only one sub-menu will be
214 * shown, which will be the first one that is found to have active menu options.
216 * The menu display will generally recognise the current URL and mark as active the
217 * menu option that matches it, but in some cases it might be desirable to force one
218 * or another option to be marked as active using the appropriate parameter to the
219 * AddOption or AddSubMenu call.
220 * @package awl
222 class MenuSet {
223 /**#@+
224 * @access private
227 * CSS style to use for the div around the options
228 * @var string
230 var $div_id;
233 * CSS style to use for normal menu option
234 * @var string
236 var $main_class;
239 * CSS style to use for active menu option
240 * @var string
242 var $active_class;
245 * An array of MenuOption objects
246 * @var array
248 var $options;
251 * Any menu option that happens to parent this set
252 * @var reference
254 var $parent;
257 * The sortkey used by any previous option
258 * @var last_sortkey
260 var $last_sortkey;
263 * Will be set to true or false when we link active sub-menus, but will be
264 * unset until we do that.
265 * @var reference
267 var $has_active_options;
268 /**#@-*/
271 * Start a new MenuSet with no options.
272 * @param string $div_id An ID for the HTML div that the menu will be presented in.
273 * @param string $main_class A CSS class for most menu options.
274 * @param string $active_class A CSS class for active menu options.
276 function MenuSet( $div_id, $main_class = '', $active_class = 'active' ) {
277 $this->options = array();
278 $this->main_class = $main_class;
279 $this->active_class = $active_class;
280 $this->div_id = $div_id;
284 * Add an option, which is a link.
285 * The call will attempt to work out whether the option should be marked as
286 * active, and will sometimes get it wrong.
287 * @param string $label A Label for the new menu option
288 * @param string $target The URL to target for this option.
289 * @param string $title Some tooltip help for the title tag.
290 * @param string $active Whether this option should be marked as Active.
291 * @param int $sortkey An (optional) value to allow option ordering.
292 * @param external open this link in a new window/tab.
293 * @return mixed A reference to the MenuOption that was added, or false if none were added.
295 function &AddOption( $label, $target, $title="", $active=false, $sortkey=null, $external=false ) {
296 if ( !isset($sortkey) ) {
297 $sortkey = (isset($this->last_sortkey) ? $this->last_sortkey + 100 : 1000);
299 $this->last_sortkey = $sortkey;
300 if ( version_compare(phpversion(), '5.0') < 0) {
301 $new_option = new MenuOption( $label, $target, $title, $this->main_class, $sortkey );
303 else {
304 $new_option = new MenuOption( $label, $target, $title, $this->main_class, $sortkey );
306 if ( ($old_option = $this->_OptionExists( $label )) === false ) {
307 $this->options[] = &$new_option ;
309 else {
310 dbg_error_log("MenuSet",":AddOption: Replacing existing option # $old_option ($label)");
311 $this->options[$old_option] = &$new_option; // Overwrite the existing option
313 if ( is_bool($active) && $active == false && $_SERVER['REQUEST_URI'] == $target ) {
314 // If $active is not set, then we look for an exact match to the current URL
315 $new_option->Active( $this->active_class );
317 else if ( is_bool($active) && $active ) {
318 // When active is specified as a boolean, the recognition has been done externally
319 $new_option->Active( $this->active_class );
321 else if ( is_string($active) && preg_match($active,$_SERVER['REQUEST_URI']) ) {
322 // If $active is a string, then we match the current URL to that as a Perl regex
323 $new_option->Active( $this->active_class );
326 if ( $external == true ) $new_option->Set('target', '_blank');
328 return $new_option ;
332 * Add an option, which is a submenu
333 * @param object &$submenu_set A reference to a menu tree
334 * @param string $label A Label for the new menu option
335 * @param string $target The URL to target for this option.
336 * @param string $title Some tooltip help for the title tag.
337 * @param string $active Whether this option should be marked as Active.
338 * @param int $sortkey An (optional) value to allow option ordering.
339 * @return mixed A reference to the MenuOption that was added, or false if none were added.
341 function &AddSubMenu( &$submenu_set, $label, $target, $title="", $active=false, $sortkey=2000 ) {
342 $new_option =& $this->AddOption( $label, $target, $title, $active, $sortkey );
343 $submenu_set->parent = &$new_option ;
344 $new_option->AddSubmenu( $submenu_set );
345 return $new_option ;
349 * Does the menu have any options that are active.
350 * Most likely used so that we can then set the parent menu as active.
351 * @param string $label A Label for the new menu option
352 * @return boolean Whether the menu has options that are active.
354 function _HasActive( ) {
355 if ( isset($this->has_active_options) ) {
356 return $this->has_active_options;
358 foreach( $this->options AS $k => $v ) {
359 if ( $v->IsActive() ) {
360 $rc = true;
361 return $rc;
364 $rc = false;
365 return $rc;
369 * Find out how many options the menu has.
370 * @return int The number of options in the menu.
372 function Size( ) {
373 return count($this->options);
377 * See if a menu already has this option
378 * @return boolean Whether the option already exists in the menu.
380 function _OptionExists( $newlabel ) {
381 $rc = false;
382 foreach( $this->options AS $k => $v ) {
383 if ( $newlabel == $v->label ) return $k;
385 return $rc;
389 * Mark each MenuOption as active that has an active sub-menu entry.
391 * Currently needs to be called manually before rendering but
392 * really should probably be called as part of the render now,
393 * and then this could be a private routine.
395 function LinkActiveSubMenus( ) {
396 $this->has_active_options = false;
397 foreach( $this->options AS $k => $v ) {
398 if ( isset($v->submenu_set) && $v->submenu_set->_HasActive() ) {
399 // Note that we need to do it this way, since $v is a copy, not a reference
400 $this->options[$k]->Active( $this->active_class );
401 $this->has_active_options = true;
407 * Mark each MenuOption as active that has an active sub-menu entry.
409 * Currently needs to be called manually before rendering but
410 * really should probably be called as part of the render now,
411 * and then this could be a private routine.
413 function MakeSomethingActive( $test_pattern ) {
414 if ( $this->has_active_options ) return; // Already true.
415 foreach( $this->options AS $k => $v ) {
416 if ( isset($v->submenu_set) && $v->submenu_set->_HasActive() ) {
417 // Note that we need to do it this way, since $v is a copy, not a reference
418 $this->options[$k]->Active( $this->active_class );
419 $this->has_active_options = true;
420 return $this->has_active_options;
424 foreach( $this->options AS $k => $v ) {
425 if ( isset($v->submenu_set) && $v->submenu_set->MakeSomethingActive($test_pattern) ) {
426 // Note that we need to do it this way, since $v is a copy, not a reference
427 $this->options[$k]->Active( $this->active_class );
428 $this->has_active_options = true;
429 return $this->has_active_options;
431 else {
432 if ( $this->options[$k]->MaybeActive( $test_pattern, $this->active_class ) ) {
433 $this->has_active_options = true;
434 return $this->has_active_options;
438 return false;
442 * _CompareSequence is used in sorting the menu options into the sequence order
444 * @param objectref $a The first menu option
445 * @param objectref $b The second menu option
446 * @return int ( $a == b ? 0 ( $a > b ? 1 : -1 ))
448 function _CompareSequence( $a, $b ) {
449 dbg_error_log("MenuSet",":_CompareSequence: Comparing %d with %d", $a->sortkey, $b->sortkey);
450 return ($a->sortkey - $b->sortkey);
455 * Render the menu tree to an HTML fragment.
457 * @param boolean $submenus_inline Indicate whether to render the sub-menus within
458 * the menus, or render them entirely separately after we finish rendering the
459 * top level ones.
460 * @return string The HTML fragment.
462 function Render( $submenus_inline = false ) {
463 if ( !isset($this->has_active_options) ) {
464 $this->LinkActiveSubMenus();
466 $options = $this->options;
467 usort($options,"_CompareMenuSequence");
468 $render_sub_menus = false;
469 $r = "<div id=\"$this->div_id\">\n";
470 foreach( $options AS $k => $v ) {
471 $r .= $v->Render();
472 if ( $v->IsActive() && isset($v->submenu_set) && $v->submenu_set->Size() > 0 ) {
473 $render_sub_menus = $v->submenu_set;
474 if ( $submenus_inline )
475 $r .= $render_sub_menus->Render();
478 $r .="</div>\n";
479 if ( !$submenus_inline && $render_sub_menus != false ) {
480 $r .= $render_sub_menus->Render();
482 return $r;
487 * Render the menu tree to an HTML fragment.
489 * @param boolean $submenus_inline Indicate whether to render the sub-menus within
490 * the menus, or render them entirely separately after we finish rendering the
491 * top level ones.
492 * @return string The HTML fragment.
494 function RenderAsCSS( $depth = 0, $skip_empty = true ) {
495 $this->LinkActiveSubMenus();
497 if ( $depth > 0 )
498 $class = "submenu" . $depth;
499 else
500 $class = "menu";
502 $options = $this->options;
503 usort($options,"_CompareMenuSequence");
505 $r = "<div id=\"$this->div_id\" class=\"$class\">\n<ul>\n";
506 foreach( $options AS $k => $v ) {
507 if ( $skip_empty && isset($v->submenu_set) && $v->submenu_set->Size() < 1 ) continue;
508 $r .= "<li>".$v->Render();
509 if ( isset($v->submenu_set) && $v->submenu_set->Size() > 0 ) {
510 $r .= $v->submenu_set->RenderAsCSS($depth+1);
512 $r .= "</li>\n";
514 $r .="</ul></div>\n";
515 return $r;