Overview

Packages

  • awl
    • AuthPlugin
    • AwlDatabase
    • Browser
    • classEditor
    • DataEntry
    • DataUpdate
    • EMail
    • iCalendar
    • MenuSet
    • PgQuery
    • Session
    • Translation
    • User
    • Utilities
    • Validation
    • vCalendar
    • vComponent
    • XMLDocument
    • XMLElement
  • None
  • PHP

Classes

  • MenuOption
  • MenuSet

Functions

  • _CompareMenuSequence
  • Overview
  • Package
  • Function
  • Tree
  • Deprecated
  • Todo
  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: 
AWL API documentation generated by ApiGen 2.8.0