1: <?php
2: /**
3: * Some intelligence and standardisation around presenting a menu hierarchy.
4: *
5: * See the MenuSet class for examples as that is the primary interface.
6: * @see MenuSet
7: *
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
13: */
14:
15: require_once("AWLUtilities.php");
16:
17: /**
18: * Each menu option is an object.
19: * @package awl
20: */
21: class MenuOption {
22: /**#@+
23: * @access private
24: */
25: /**
26: * The label for the menu item
27: * @var string
28: */
29: var $label;
30:
31: /**
32: * The target URL for the menu
33: * @var string
34: */
35: var $target;
36:
37: /**
38: * The title for the item when moused over, which should be displayed as a tooltip.
39: * @var string
40: */
41: var $title;
42:
43: /**
44: * Whether the menu option is active
45: * @var string
46: */
47: var $active;
48:
49: /**
50: * For sorting menu options
51: * @var string
52: */
53: var $sortkey;
54:
55: /**
56: * Style to render the menu option with.
57: * @var string
58: */
59: var $style;
60:
61: /**
62: * The MenuSet that this menu is a parent of
63: * @var string
64: */
65: var $submenu_set;
66: /**#@-*/
67:
68: /**
69: * A reference to this menu option itself
70: * @var reference
71: */
72: var $self;
73:
74: /**#@+
75: * @access public
76: */
77: /**
78: * The rendered HTML fragment (once it has been).
79: * @var string
80: */
81: var $rendered;
82: /**#@-*/
83:
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.
91: */
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;
100:
101: $this->rendered = "";
102: $this->self =& $this;
103: }
104:
105: /**
106: * Convert the menu option into an HTML string
107: * @return string The HTML fragment for the menu option.
108: */
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 );
113:
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).'"';
119: }
120: $r = str_replace( '%%attributes%%', $attribute_values, $r );
121:
122: $this->rendered = $r;
123: return "$r";
124: }
125:
126: /**
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.
130: */
131: function Set( $attribute, $value ) {
132: $this->attributes[$attribute] = $value;
133: }
134:
135: /**
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.
138: */
139: function Active( $style=false ) {
140: $this->active = true;
141: if ( $style ) $this->style = $style;
142: }
143:
144: /**
145: * This menu option is now promoted to the head of a tree
146: */
147: function AddSubmenu( &$submenu_set ) {
148: $this->submenu_set = &$submenu_set;
149: }
150:
151: /**
152: * Whether this option is currently active.
153: * @return boolean The value of the active flag.
154: */
155: function IsActive( ) {
156: return ( $this->active );
157: }
158:
159: /**
160: * Whether this option is currently active.
161: * @return boolean The value of the active flag.
162: */
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);
166: }
167: return ( $this->active );
168: }
169: }
170:
171:
172: /**
173: * _CompareMenuSequence is used in sorting the menu options into the sequence order
174: *
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 ))
178: */
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);
182: }
183:
184:
185:
186: /**
187: * A MenuSet is a hierarchy of MenuOptions, some of which might be
188: * MenuSet objects themselves.
189: *
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.
193: *
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();
211: *}
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.
215: *
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
221: */
222: class MenuSet {
223: /**#@+
224: * @access private
225: */
226: /**
227: * CSS style to use for the div around the options
228: * @var string
229: */
230: var $div_id;
231:
232: /**
233: * CSS style to use for normal menu option
234: * @var string
235: */
236: var $main_class;
237:
238: /**
239: * CSS style to use for active menu option
240: * @var string
241: */
242: var $active_class;
243:
244: /**
245: * An array of MenuOption objects
246: * @var array
247: */
248: var $options;
249:
250: /**
251: * Any menu option that happens to parent this set
252: * @var reference
253: */
254: var $parent;
255:
256: /**
257: * The sortkey used by any previous option
258: * @var last_sortkey
259: */
260: var $last_sortkey;
261:
262: /**
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
266: */
267: var $has_active_options;
268: /**#@-*/
269:
270: /**
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.
275: */
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;
281: }
282:
283: /**
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.
294: */
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);
298: }
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 );
302: }
303: else {
304: $new_option = new MenuOption( $label, $target, $title, $this->main_class, $sortkey );
305: }
306: if ( ($old_option = $this->_OptionExists( $label )) === false ) {
307: $this->options[] = &$new_option ;
308: }
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
312: }
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 );
316: }
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 );
320: }
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 );
324: }
325:
326: if ( $external == true ) $new_option->Set('target', '_blank');
327:
328: return $new_option ;
329: }
330:
331: /**
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.
340: */
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 ;
346: }
347:
348: /**
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.
353: */
354: function _HasActive( ) {
355: if ( isset($this->has_active_options) ) {
356: return $this->has_active_options;
357: }
358: foreach( $this->options AS $k => $v ) {
359: if ( $v->IsActive() ) {
360: $rc = true;
361: return $rc;
362: }
363: }
364: $rc = false;
365: return $rc;
366: }
367:
368: /**
369: * Find out how many options the menu has.
370: * @return int The number of options in the menu.
371: */
372: function Size( ) {
373: return count($this->options);
374: }
375:
376: /**
377: * See if a menu already has this option
378: * @return boolean Whether the option already exists in the menu.
379: */
380: function _OptionExists( $newlabel ) {
381: $rc = false;
382: foreach( $this->options AS $k => $v ) {
383: if ( $newlabel == $v->label ) return $k;
384: }
385: return $rc;
386: }
387:
388: /**
389: * Mark each MenuOption as active that has an active sub-menu entry.
390: *
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.
394: */
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;
402: }
403: }
404: }
405:
406: /**
407: * Mark each MenuOption as active that has an active sub-menu entry.
408: *
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.
412: */
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;
421: }
422: }
423:
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;
430: }
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;
435: }
436: }
437: }
438: return false;
439: }
440:
441: /**
442: * _CompareSequence is used in sorting the menu options into the sequence order
443: *
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 ))
447: */
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);
451: }
452:
453:
454: /**
455: * Render the menu tree to an HTML fragment.
456: *
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.
461: */
462: function Render( $submenus_inline = false ) {
463: if ( !isset($this->has_active_options) ) {
464: $this->LinkActiveSubMenus();
465: }
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();
476: }
477: }
478: $r .="</div>\n";
479: if ( !$submenus_inline && $render_sub_menus != false ) {
480: $r .= $render_sub_menus->Render();
481: }
482: return $r;
483: }
484:
485:
486: /**
487: * Render the menu tree to an HTML fragment.
488: *
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.
493: */
494: function RenderAsCSS( $depth = 0, $skip_empty = true ) {
495: $this->LinkActiveSubMenus();
496:
497: if ( $depth > 0 )
498: $class = "submenu" . $depth;
499: else
500: $class = "menu";
501:
502: $options = $this->options;
503: usort($options,"_CompareMenuSequence");
504:
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);
511: }
512: $r .= "</li>\n";
513: }
514: $r .="</ul></div>\n";
515: return $r;
516: }
517: }
518: