cakebook / branches / master / views / helpers / menu.php
history
<?php
/* SVN FILE: $Id: menu.php 916 2009-04-09 09:03:07Z ad7six $ */
/**
* Short description for menu.php
*
* Long description for menu.php
*
* PHP versions 4 and 5
*
* Copyright (c) 2008, Andy Dawson
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @filesource
* @copyright Copyright (c) 2008, Andy Dawson
* @link www.ad7six.com
* @package base
* @subpackage base.views.helpers
* @since v 1.0
* @version $Revision: 916 $
* @modifiedby $LastChangedBy: ad7six $
* @lastmodified $Date: 2009-04-09 11:03:07 +0200 (Thu, 09 Apr 2009) $
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
/**
* MenuHelper class
*
* @uses AppHelper
* @package base
* @subpackage base.views.helpers
*/
class MenuHelper extends AppHelper {
/**
* name property
*
* @var string 'Menu'
* @access public
*/
var $name = 'Menu';
/**
* helpers property
*
* @var array
* @access public
*/
var $helpers = array('Html');
/**
* counter property
*
* @var int 1000
* @access private
*/
var $__counter = 1000;
/**
* defaultSettings property
*
* @var array
* @access private
*/
var $__defaultSettings = array(
'activeMode' => 'url', // url // controller[name] // action[and controller name] // false [do nothing]
'hereMode' => 'active', // active[mark the li as active] // text[no link just text] // false[do nothing]
'hereKey' => null, // the key for the item to mark as active automatic based on activeMode if not specified
'order' => null, // the order the whole section should be output. only used if generating many menus at once
'genericElement' => 'menu/generic', // use is deprecated
'uniqueKey' => 'title', // determins how data is stored internally, and how duplicate items are detected
'overwrite' => false, // Overwrite the menu item if it already has been defined?
'showWarnings' => true, // Trigger an error if trying to redefine a menu item and overwrite is false
'headerTag' => false, // used to automatically wrap the section name in a (e.g.) h3 tag on display
'typeTag' => 'ul', // The tag used for the menu links as a whole.
'itemTag' => 'li', // The tag used for each menu link
'wrap' => false, // a sprintf string to wrap the output of the menu e.g. "<div>%s</div>"
'class' => 'menu', // the class attribute for the top level
'id' => false, // the id attribute for the top level
'splitCount' => false, // inject </ul><ul> after this number of menu items
);
/**
* settings property
*
* @var array
* @access public
*/
var $settings = array();
/**
* section property
*
* The current section
*
* @var string 'menu'
* @access private
*/
var $__section = 'menu';
/**
* data property
*
* Holds the menu data as they get built. References flatData.
*
* @var array
* @access private
*/
var $__data = array();
/**
* flatData property
*
* A flat list of menu data
*
* @var array
* @access private
*/
var $__flatData = array();
/**
* here property
*
* Place holder for router normalized "here"
*
* @var string ''
* @access private
*/
var $__here = '';
/**
* construct method
*
* @param array $options
* @return void
* @access private
*/
function __construct($options = array()) {
$this->__defaultSettings = am($this->__defaultSettings, $options);
parent::__construct($options);
}
/**
* beforeRender method
*
* If genericElement is set, 'render' the named element. This can be used to prevent repeating menu logic if
* for example there are some menu items which don't change based on the specific view file
* The result is echoed to display errors even though the element should contain no output
*
* @access public
* @return void
*/
function beforeRender() {
if (!isset($this->params['requested']) && $this->__defaultSettings['genericElement']) {
$view =& ClassRegistry:: getObject('view');
if ($view) {
echo $view->element($this->__defaultSettings['genericElement']);
}
}
return true;
}
/**
* beforeLayout method
*
* @return void
* @access public
*/
function beforeLayout () {
$this->__counter = 0;
}
/**
* Add a menu item.
*
* Add a menu item syntax examples:
* $menu->add($title, $url); adds an entry with $title and $url to the current menu section
* $menu->add('menu', $title, $url); add specifically to the 'menu' section
* $menu->add('context', $title, $url); add an entry with $title and $url to the menu named "context"
* $menu->add('context', $title, $url, 'subSection'); add an entry with $title and $url to subsection "subSection for the menu named "context"
* $menu->add(array('url' => $url, 'title' => $title, 'options' => array('escapeTitle' => false))); array syntax, not escaping title
* $menu->add(array('url' => $url, 'title' => $title, 'options' => array('htmlAttributes' => array('id' => 'foo'))); array syntax, setting id for link
*
* @param string $section
* @param mixed $title
* @param mixed $url
* @param mixed $under
* @param array $options
* @access public
* @return void
*/
function add($section = null, $title = null, $url = null, $under = null, $options = array()) {
$class = $id = null;
$htmlAttributes = isset($options['htmlAttributes'])?$options['htmlAttributes']:array();
$confirmMessage = false;
$escapeTitle = true;
$order = $this->__counter++;
if (is_array($section)) {
if (isset($section[0]['title'])) {
foreach ($section as $row) {
$this->add($row);
}
return;
} else {
extract(am(array('section' => $this->__section), $section));
}
} elseif ($url == null) {
if ($under) {
$options = $under;
}
$options = $under;
$under = $url;
$url = $title;
$title = $section;
$section = $this->__section;
}
$settings = $this->settings($section);
$section = $this->__section;
if ($settings['uniqueKey'] === 'url') {
$key = Router::normalize($url);
} else {
$key = $title;
}
if (is_array($under)) {
if ($settings['uniqueKey'] === 'url') {
$under = Router::normalize($under);
}
}
if ($under === $key) {
if ($settings['showWarnings']) {
trigger_error ('MenuHelper::add<br />' . $key . ' Menu item cannot have itself as its own parent');
}
return;
}
list($here, $markActive, $url) = $this->__setHere($section, $url, $key, $settings['activeMode'], $settings['hereMode'], $options);
if ($options) {
extract($options);
}
$item = array(
'here' => $here,
'order' => $order,
'markActive' => $markActive,
'url' => $url,
'title' => $title,
'under' => $under,
'id' => $id,
'class' => $class,
'inPath'=> false,
'sibling' => false,
'children' => array(),
'htmlAttributes' => $htmlAttributes,
'confirmMessage' => false,
'escapeTitle' => true
);
if ($under) {
if (!isset($this->__flatData[$section][$under])) {
$parent = array(
'placeholder' => true,
'order' => $order,
'here' => false,
'markActive' => false,
'url' => null,
'title' => null,
'under' => false,
'id' => null,
'class' => null,
'inPath'=> false,
'sibling' => false,
'children' => array(),
'htmlAttributes' => array(),
'confirmMessage' => false,
'escapeTitle' => true
);
if ($settings['uniqueKey'] === 'title') {
$parent[$settings['uniqueKey']] = $under;
} else {
$parent[$settings['uniqueKey']] = Router::normalize($under);
}
$this->__flatData[$section][$under] = $parent;
$this->__data[$section][$under] =& $this->__flatData[$section][$under];
}
$this->__flatData[$section][$key] = $item;
$this->__flatData[$section][$under]['children'][$key] =& $this->__flatData[$section][$key];
} elseif (isset($this->__flatData[$section][$key]) && !empty($this->__flatData[$section][$key]['placeholder'])) {
$item['children'] =& $this->__flatData[$section][$key]['children'];
unset($this->__data[$section][$key]);
unset($this->__flatData[$section][$key]);
$this->__flatData[$section][$key] = $item;
$this->__data[$section][$key] =& $this->__flatData[$section][$key];
} elseif (!isset($this->__flatData[$section][$key]) || $settings['overwrite']) {
$this->__flatData[$section][$key] = $item;
$this->__data[$section][$key] =& $this->__flatData[$section][$key];
} elseif ($settings['showWarnings']) {
if ($settings['uniqueKey'] === 'title') {
$altKey = 'url';
} else {
$altKey = 'title';
}
trigger_error ('MenuHelper::add<br /> Duplicate menu item detected for item "' . $title .
'" in menu "' . $section . '".<br />You can change the field used to detect duplicates' .
' which is currently set to ' . $settings['uniqueKey'] . ', can be changed to ' . $altKey . '.');
} else {
return;
}
if ($settings['hereMode'] === 'text' && $here === true) {
$this->__flatData[$section][$key]['url'] = false;
}
if (!empty($children)) {
foreach ($children as $row) {
$row['under'] = $key;
$this->add($row);
}
}
}
/**
* addAttribute method
*
* @param mixed $tag
* @param string $id
* @param string $key
* @param mixed $value
* @return void
* @access public
*/
function addAttribute($tag, $id = '', $key = '', $value = null) {
if (!is_null($value)) {
$this->__attributes[$tag][$id][$key] = $value;
} elseif (!(isset($this->__attributes[$tag][$id]) && in_array($key, $this->__attributes[$tag][$id]))) {
$this->__attributes[$tag][$id][] = $key;
}
}
/**
* del method
*
* Delete a menu item. Specify the section name alone to delete the entire section.
* Specify the section and key to delete a single menu item.
* Specify just the key to delete an entry from the currently active menu section
*
* @param mixed $section
* @param mixed $key
* @return void
* @access public
*/
function del($section, $key = null) {
if (is_null($key)) {
if (isset($this->__flatData[$section])) {
unset ($this->__flatData[$section]);
unset ($this->__data[$section]);
return;
}
$key = $section;
$section = $this->__section;
}
unset ($this->__flatData[$section][$key]);
unset ($this->__data[$section][$key]);
}
/**
* display menu method
*
* display menu syntax examples:
* echo $menu->display(); echo the currently active menu
* echo $menu->displaydisplay('menu'); as above but explicit
* echo $menu->display('menu', array('element' => 'menus/item'); use an element for each item's content
* echo $menu->display('menu', array('callback' => 'menuItem'); use loose method menuItem for each item's content
* echo $menu->display('menu', array('callback' => array(&$object, 'method'); call $object->method($data) for each item's content
*
* @param mixed $section the section name or the numerical order
* @param array $settings to be passed to the tree helper
* @param bool $createEmpty
* @access public
* @return void
*/
function display($section = null, $settings = array(), $createEmpty = true) {
if (is_array($section)) {
extract(array_merge(array('section' => $this->__section), $section));
}
$settings = $this->settings($section, (array)$settings);
if (!$section) {
$section = $this->__section;
}
if (!isset($this->settings[$section]) || empty($this->__data[$section])) {
$return = '';
} else {
$this->__attributes = array();
$return = $this->__display($section, $settings, $this->__data[$section]);
}
if ($this->settings[$section]['wrap']) {
$return = sprintf($this->settings[$section]['wrap'], $return);
}
if (trim($return) === '' && $createEmpty) {
$typeTag = $this->settings[$section]['typeTag'];
$return = $this->__displayHead($section, $settings, true) . "</$typeTag>";
}
unset ($this->settings[$section]);
unset ($this->__data[$section]);
unset ($this->__flatData[$section]);
return trim($return);
}
/**
* displayAll method
*
* @param array $settings
* @param bool $createEmpty
* @return void
* @access public
*/
function displayAll($settings = array(), $createEmpty = true) {
$return = '';
foreach($this->sections() as $section) {
$return .= $this->display($section, $settings, $createEmpty);
}
return $return;
}
/**
* sections method
*
* Return the names of all sections currently stored by the helper, in the order they should be processed
*
* @access public
* @return mixed array of menu sections if no order passed. name of the section name matching the order if passed.
*/
function sections ($order = null) {
$sequence = array();
foreach ($this->settings as $key => $settings) {
if ($order !== null && $settings['order'] == $order) {
return $key;
} elseif (!isset($sequence[$settings['order']])) {
$sequence[$settings['order']] = $key;
} else {
$sequence[$settings['order'] . rand()] = $key;
}
}
if ($order !== null) {
return false;
}
ksort($sequence);
return $sequence;
}
/**
* settings method
*
* @param mixed $section
* @param array $settings
* @return void
* @access public
*/
function settings($section = null, $settings = array()) {
if ($section === null) {
$section = $this->__section;
} elseif (!$section) {
$section = $this->__section = 'menu';
} else {
$this->__section = $section;
}
if (!$this->__here) {
if (isset($this->params['url']['url'])) {
$this->__here = Router::normalize('/' . $this->params['url']['url']);
} else {
$this->__here = '/';
}
}
if (!isset($this->settings[$section])) {
$settings = array_merge($this->__defaultSettings, $settings);
$this->settings[$section] = $settings;
} elseif ($settings) {
$this->settings[$section] = array_merge($this->settings[$section], $settings);
}
if (!is_numeric($this->settings[$section]['order'])) {
$this->settings[$section]['order'] = count($this->settings);
}
return $this->settings[$section];
}
/**
* attributes method
*
* @param mixed $rType
* @param bool $clear
* @return void
* @access private
*/
function __attributes($tag, $clear = true) {
if (empty($this->__attributes[$tag])) {
return '';
}
foreach ($this->__attributes[$tag] as $i => &$values) {
foreach ($values as $j => &$val) {
if (is_array($val)) {
$_a = array();
foreach ($val as $k => &$v) {
$_a[] = $k . ':' . $v;
}
$val = implode(';', $_a);
}
if (is_string($j)) {
$val = $j . ':' . $val . ';';
}
}
$values = $i . '="' . implode(' ', $values) . '"';
}
$return = ' ' . implode(' ', $this->__attributes[$tag]) . ' ';
if ($clear) {
unset($this->__attributes[$tag]);
}
return $return;
}
/**
* internal callback
*
* Used to return the output from the html helper using the parameters for this menu option
*
* @param mixed $data
* @return void
* @access private
*/
function __menuItem($data) {
if ($data['markActive']) {
$this->addAttribute($this->settings[$this->__section]['itemTag'], 'class', 'active');
}
if ($data['class']) {
$this->addAttribute($this->settings[$this->__section]['itemTag'], 'class', $data['class']);
}
if ($data['id']) {
$this->addAttribute($this->settings[$this->__section]['itemTag'], 'id', $data['id']);
}
if ($data['url'] === false) {
return $data['title'];
} else {
return $this->Html->link($data['title'], $data['url'], $data['htmlAttributes'],
$data['confirmMessage'], $data['escapeTitle']);
}
}
/**
* display method
*
* Generate a menu. Works recurslively for nested menus
*
* @param mixed $section
* @param mixed $settings
* @param mixed $data
* @return void
* @access private
*/
function __display($section, $settings, $data, $header = true, $prefix = "\r\n") {
$return = '';
$start = true;
if ($settings['splitCount']) {
$total = count($data);
$splitCount = $total / $settings['splitCount'];
$rounded = (int)$splitCount;
if ($rounded < $splitCount) {
$splitCount = $rounded + 1;
}
$splitCounter = 0;
}
$typeTag = $settings['typeTag'];
$itemTag = $settings['itemTag'];
$_data = array();
$data = array_reverse($data);
foreach ($data as $i => $row) {
$_data[$row['order']] = $row;
}
ksort($_data);
$data = array_values($_data);
foreach ($data as $i => &$result) {
if ($settings['splitCount']) {
if ($splitCounter && !($splitCounter % $splitCount) && $splitCounter != $total) {
$return .= "$prefix</$typeTag><$typeTag>";
}
$splitCounter++;
}
$contents = $this->menuItem($result);
$attributes = $this->__attributes($itemTag);
$return .= "$prefix\t<$itemTag{$attributes}>$contents";
if (!empty($result['children'])) {
$_settings = am($settings, array('class' => false, 'id' => false));
$return .= $this->__display($section, $_settings, $result['children'], false, $prefix . "\t\t");
$return .= $prefix . "\t";
}
$return .= "</$itemTag>";
if ($start) {
$start = false;
$return = $prefix . $this->__displayHead($section, $settings, $header) . $return;
}
}
$return .= "$prefix</$typeTag>";
return $return;
}
/**
* displayHead method
*
* Optionally announce the start of this menu (create <h3>name of menu</h3>)
* Generate a ul tag with appropriate attributes
*
* @param mixed $section
* @param mixed $settings
* @param bool $header
* @return void
* @access private
*/
function __displayHead($section, $settings, $header = false) {
$return = '';
if ($header) {
$section = Inflector::humanize(Inflector::underscore($section));
if (!empty($settings['headerTag'])) {
$tag = $settings['headerTag'];
$return .= "<$tag>$section</$tag>";
}
if (!empty($settings['class'])) {
$this->addAttribute($settings['typeTag'], 'class', $settings['class']);
}
if (!empty($settings['id'])) {
$this->addAttribute($settings['typeTag'], 'id', $settings['id']);
}
}
$tag = $settings['typeTag'];
$attributes = $this->__attributes($tag);
$return .= "<$tag{$attributes}>";
return $return;
}
/**
* setHere method
*
* Used internally to detect whether the current menu item links to the page currently
* being rendered and modify the url if appropriate
*
* @param mixed $section
* @param mixed $url
* @param mixed $activeMode
* @param mixed $hereMode
* @access private
* @return array($here, $markActive, $url)
*/
function __setHere($section, $url, $key, $activeMode, $hereMode, $options) {
$view =& ClassRegistry:: getObject('view');
if (isset($this->settings[$section]['hereKey']) || !$view) {
return array(false, false, $url);
}
$here = $markActive = false;
if (!empty($options['markActive'])) {
$markActive = true;
} elseif ($activeMode == 'url' && Router::normalize($url) == $this->__here) {
$here = true;
} elseif (is_array($url) &&
(!isset($url['controller']) ||
Inflector::underscore($url['controller']) == Inflector::underscore($view->name))) {
if ($activeMode == 'controller') {
$here = true;
} elseif ($activeMode == 'action' &&
(!isset($url['action']) || $url['action'] == Inflector::underscore($view->action))) {
$here = true;
}
}
if ($here) {
$this->settings[$section]['hereKey'] = $key;
if ($hereMode == 'text') {
$url = false;
} elseif ($hereMode) {
$markActive = true;
}
}
return array($here, $markActive, $url);
}
/**
* addm method
*
* @deprecated
* @param string $section
* @param array $data
* @access public
* @return void
*/
function addm($section = null, $data = array()) {
if (is_array($section)) {
return $this->add($section);
}
$this->__section = $section;
return $this->add($data);
}
/**
*
* addItemAttribute method
*
* @deprecated
* @param string $id
* @param string $key
* @param mixed $value
* @return void
* @access public
*/
function addItemAttribute($id = '', $key = '', $value = null) {
$this->addAttribute($this->settings[$this->__section]['itemTag'], $id, $key, $value);
}
/**
* addTypeAttribute method
*
* @deprecated
* @param string $id
* @param string $key
* @param mixed $value
* @return void
* @access public
*/
function addTypeAttribute($id = '', $key = '', $value = null) {
$this->addAttribute($this->settings[$this->__section]['typeTag'], $id, $key, $value);
}
/**
* internal callback
*
* @deprecated
* @param array $data
* @access public
* @return void
*/
function menuItem(&$data) {
return $this->__menuItem($data);
}
/**
* generate method
*
* @deprecated
* @param mixed $section
* @param array $settings
* @param bool $createEmpty
* @return void
* @access public
*/
function generate($section = null, $settings = array(), $createEmpty = true) {
return $this->display($section, $settings, $createEmpty);
}
}
?>