cakebook / branches / master / views / helpers / tree.php

history
<?php
/* SVN FILE: $Id: tree.php 675 2008-12-08 11:54:00Z ad7six $ */
/**
 * Tree Helper.
 *
 * Long description for tree.php
 *
 * Used the generate nested representations of hierarchial data
 *
 * 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: 675 $
 * @modifiedby    $LastChangedBy: ad7six $
 * @lastmodified  $Date: 2008-12-08 12:54:00 +0100 (Mon, 08 Dec 2008) $
 * @license       http://www.opensource.org/licenses/mit-license.php The MIT License
 */
/**
 * TreeHelper class
 *
 * Helper to generate tree representations of MPTT or recursively nested data
 *
 * @uses          AppHelper
 * @package       base
 * @subpackage    base.views.helpers
 */
class TreeHelper extends AppHelper {
/**
 * name property
 *
 * @var string 'Tree'
 * @access public
 */
	var $name = 'Tree';
/**
 * settings property
 *
 * @var array
 * @access private
 */
	var $__settings = array();
/**
 * typeAttributes property
 *
 * @var array
 * @access private
 */
	var $__typeAttributes = array();
/**
 * typeAttributesNext property
 *
 * @var array
 * @access private
 */
	var $__typeAttributesNext = array();
/**
 * itemAttributes property
 *
 * @var array
 * @access private
 */
	var $__itemAttributes = array();
/**
 * helpers variable
 *
 * @var array
 * @access public
 */
	var $helpers = array ('Html');
/**
 * Tree generation method.
 *
 * Accepts the results of
 * 	find('all', array('fields' => array('lft', 'rght', 'whatever'), 'order' => 'lft ASC'));
 * 	children(); // if you have the tree behavior of course!
 * or 	findAllThreaded(); and generates a tree structure of the data.
 *
 * Settings (2nd parameter):
 *	'model' => name of the model (key) to look for in the data array. defaults to the first model for the current
 * controller. If set to false 2d arrays will be allowed/expected.
 *	'alias' => the array key to output for a simple ul (not used if element or callback is specified)
 *	'type' => type of output defaults to ul
 *	'itemType => type of item output default to li
 *	'id' => id for top level 'type'
 *	'class' => class for top level 'type'
 *	'element' => path to an element to render to get node contents.
 *	'callback' => callback to use to get node contents. e.g. array(&$anObject, 'methodName') or 'floatingMethod'
 *	'autoPath' =>  array($left, $right [$classToAdd = 'active']) if set any item in the path will have the class $classToAdd added. MPTT only.
 *	'left' => name of the 'lft' field if not lft. only applies to MPTT data
 *	'right' => name of the 'rght' field if not lft. only applies to MPTT data
 *	'depth' => used internally when running recursively, can be used to override the depth in either mode.
 *	'firstChild' => used internally when running recursively.
 *	'splitDepth' => if multiple "parallel" types are required, instead of one big type, nominate the depth to do so here
 *		example: useful if you have 30 items to display, and you'd prefer they appeared in the source as 3 lists of 10 to be able to
 *		style/float them.
 *	'splitCount' => the number of "parallel" types. defaults to null (disabled) set the splitCount,
 *		and optionally set the splitDepth to get parallel lists
 *
 * @param array $data data to loop on
 * @param array $settings
 * @return string html representation of the passed data
 * @access public
 */
	function generate ($data, $settings = array ()) {
		$this->__settings = array_merge(array(
			'model' => null,
			'alias' => 'name',
			'type' => 'ul',
			'itemType' => 'li',
			'id' => false,
			'class' => false,
			'element' => false,
			'callback' => false,
			'autoPath' => false,
			'left' => 'lft',
			'right' => 'rght',
			'depth' => 0,
			'firstChild' => true,
			'indent' => null,
			'splitDepth' => false,
			'splitCount' => null,
			'totalNodes' => false
		), (array)$settings);
		if ($this->__settings['autoPath'] && !isset($this->__settings['autoPath'][2])) {
			$this->__settings['autoPath'][2] = 'active';
		}
		extract($this->__settings);
		if ($indent === null && Configure::read()) {
			$indent = true;
		}
		$view =& ClassRegistry:: getObject('view');
		if ($model === null) {
			$model = Inflector::classify($view->params['models'][0]);
		}
		$this->__itemAttributes = $this->__typeAttributes = $this->__typeAttributesNext = array();
		$stack = array();
		if ($depth == 0) {
			if ($class) {
				$this->addTypeAttribute('class', $class, null, 'previous');
			}
			if ($id) {
				$this->addTypeAttribute('id', $id, null, 'previous');
			}
		}
		$return = '';
		if ($indent) {
			$return = "\r\n";
		}
		$__addType = true;
		$this->__settings['totalNodes'] = count($data);
		$keys = array_keys($data);
		foreach ($data as $i => &$result) {
			/* Allow 2d data arrays */
			if ($model && isset($result[$model])) {
				$row =& $result[$model];
			} else {
				$row =& $result;
			}
			/* BulletProof */
			if (!isset($row[$left]) && !isset($result['children'])) {
				$result['children'] = array();
			}
			/* Close open items as appropriate */
			while ($stack && ($stack[count($stack)-1] < $row[$right])) {
				array_pop($stack);
				if ($indent) {
					$whiteSpace = str_repeat("\t",count($stack));
					$return .= "\r\n" . $whiteSpace . "\t";
				}
				if ($type) {
					$return .= '</' . $type . '>';
				}
				if ($itemType) {
					$return .= '</' . $itemType . '>';
				}
			}
			/* Some useful vars */
			$hasChildren = $firstChild = $lastChild = $hasVisibleChildren = false;
			$numberOfDirectChildren = $numberOfTotalChildren = null;
			if (isset($result['children'])) {
				if ($result['children']) {
					$hasChildren = $hasVisibleChildren = true;
					$numberOfDirectChildren = count($result['children']);
				}
				$key = array_search($i, $keys);
				if ($key == 0) {
					$firstChild = true;
				}
				if ($key == count($keys) - 1) {
					$lastChild = true;
				}
			} elseif (isset($row[$left])) {
				if ($row[$left] != ($row[$right] - 1)) {
					$hasChildren = true;
					$numberOfTotalChildren = ($row[$right] - $row[$left] - 1) / 2;
					if (isset($data[$i + 1]) && $data[$i + 1][$model][$right] < $row[$right]) {
						$hasVisibleChildren = true;
					}
				}
				if (!isset($data[$i - 1]) || ($data[$i - 1][$model][$left] == ($row[$left] - 1))) {
					$firstChild = true;
				}
				if (!isset($data[$i + 1]) || ($stack && $stack[count($stack) - 1] == ($row[$right] + 1))) {
					$lastChild = true;
				}
			}
			$elementData = array(
				'data' => $result,
				'depth' => $depth?$depth:count($stack),
				'hasChildren' => $hasChildren,
				'numberOfDirectChildren' => $numberOfDirectChildren,
				'numberOfTotalChildren' => $numberOfTotalChildren,
				'firstChild' => $firstChild,
				'lastChild' => $lastChild,
				'hasVisibleChildren' => $hasVisibleChildren
			);
			$this->__settings = array_merge($this->__settings, $elementData);
			/* Main Content */
			if ($element) {
				$content = $view->element($element,$elementData);
			} elseif ($callback) {
				list($content) = array_map($callback, array($elementData));
			} else {
				$content = $row[$alias];
			}
			if (!$content) {
				continue;
			}
			$whiteSpace = str_repeat("\t", $depth);
			if ($indent && strpos($content, "\r\n", 1)) {
				$content = str_replace("\r\n", "\n" . $whiteSpace . "\t", $content);
			}
			/* Prefix */
			if ($__addType) {
				if ($indent) {
					$return .= "\r\n" . $whiteSpace;
				}
				if ($type) {
					$typeAttributes = $this->__attributes($type, array('data' => $elementData));
					$return .= '<' . $type .  $typeAttributes . '>';
				}
			}
			if ($indent) {
				$return .= "\r\n" . $whiteSpace . "\t";
			}
			if ($itemType) {
				$itemAttributes = $this->__attributes($itemType, $elementData);
				$return .= '<' . $itemType . $itemAttributes . '>';
			}
			$return .= $content;
			/* Suffix */
			$__addType = false;
			if ($hasVisibleChildren) {
				if ($numberOfDirectChildren) {
					$settings['depth'] = $depth + 1;
					$return .= $this->__suffix();
					$return .= $this->generate($result['children'], $settings);
					if ($itemType) {
						$return .= '</' . $itemType . '>';
					}
				} elseif ($numberOfTotalChildren) {
					$__addType = true;
					$stack[] = $row[$right];
				}
			} else {
				if ($itemType) {
					$return .= '</' . $itemType . '>';
				}
				$return .= $this->__suffix();
			}
		}
		/* Cleanup */
		while ($stack) {
			array_pop($stack);
			if ($indent) {
				$whiteSpace = str_repeat("\t",count($stack));
				$return .= "\r\n" . $whiteSpace . "\t";
			}
			if ($type) {
				$return .= '</' . $type . '>';
			}
			if ($itemType) {
				$return .= '</' . $itemType . '>';
			}
		}

		if ($indent) {
			$return .= "\r\n";
		}
		if ($type) {
			$return .= '</' . $type . '>';
			if ($indent) {
				$return .= "\r\n";
			}
		}
		return $return;
	}
/**
 * addItemAttribute function
 *
 * Called to modify the attributes of the next <item> to be processed
 * Note that the content of a 'node' is processed before generating its wrapping <item> tag
 *
 * @param string $id
 * @param string $key
 * @param mixed $value
 * @access public
 * @return void
 */
	function addItemAttribute($id = '', $key = '', $value = null) {
		if (!is_null($value)) {
			$this->__itemAttributes[$id][$key] = $value;
		} elseif (!(isset($this->__itemAttributes[$id]) && in_array($key, $this->__itemAttributes[$id]))) {
			$this->__itemAttributes[$id][] = $key;
		}
	}
/**
 * addTypeAttribute function
 *
 * Called to modify the attributes of the next <type> to be processed
 * Note that the content of a 'node' is processed before generating its wrapping <type> tag (if appropriate)
 * An 'interesting' case is that of a first child with children. To generate the output
 * <ul> (1)
 *      <li>XYZ (3)
 *              <ul> (2)
 *                      <li>ABC...
 *                      ...
 *              </ul>
 *              ...
 * The processing order is indicated by the numbers in brackets.
 * attributes are allways applied to the next type (2) to be generated
 * to set properties of the holding type - pass 'previous' for the 4th param
 * i.e.
 * // Hide children (2)
 * $tree->addTypeAttribute('style', 'display', 'hidden');
 * // give top level type (1) a class
 * $tree->addTypeAttribute('class', 'hasHiddenGrandChildren', null, 'previous');
 *
 * @param string $id
 * @param string $key
 * @param mixed $value
 * @access public
 * @return void
 */
	function addTypeAttribute($id = '', $key = '', $value = null, $previousOrNext = 'next') {
		$var = '__typeAttributes';
		$firstChild = isset($this->__settings['firstChild'])?$this->__settings['firstChild']:true;
		if ($previousOrNext == 'next' && $firstChild) {
			$var = '__typeAttributesNext';
		}
		if (!is_null($value)) {
			$this->{$var}[$id][$key] = $value;
		} elseif (!(isset($this->{$var}[$id]) && in_array($key, $this->{$var}[$id]))) {
			$this->{$var}[$id][] = $key;
		}
	}

/**
 * supressChildren method
 *
 * @return void
 * @access public
 */
	function supressChildren() {
	}
/**
 * suffix method
 *
 * Used to close and reopen a ul/ol to allow easier listings
 *
 * @access private
 * @return void
 */
	function __suffix($reset = false) {
/**
 * splitCount property
 *
 * @static
 * @var int 0
 * @access private
 */
		static $__splitCount = 0;
/**
 * splitCounter property
 *
 * @static
 * @var int 0
 * @access private
 */
		static $__splitCounter = 0;
		if ($reset) {
			$__splitCount = 0;
			$__splitCounter = 0;
		}
		extract($this->__settings);
		if ($splitDepth || $splitCount) {
			if (!$splitDepth)  {
				$__splitCount = $totalNodes / $splitCount;
				$rounded = (int)$__splitCount;
				if ($rounded < $__splitCount) {
					$__splitCount = $rounded + 1;
				}
			} elseif ($depth == $splitDepth -1) {
				$total = $numberOfDirectChildren?$numberOfDirectChildren:$numberOfTotalChildren;
				if ($total) {
					$__splitCounter = 0;
					$__splitCount = $total / $splitCount;
					$rounded = (int)$__splitCount;
					if ($rounded < $__splitCount) {
						$__splitCount = $rounded + 1;
					}
				}
			}
			if (!$splitDepth || $depth == $splitDepth) {
				$__splitCounter++;
				if ($type && ($__splitCounter % $__splitCount) == 0 && !$lastChild) {
					unset ($this->__settings['callback']);
					return '</' . $type . '><' . $type . '>';
				}
			}
		}
		return;
	}
/**
 * attributes function
 *
 * Logic to apply styles to tags.
 *
 * @param mixed $rType
 * @param array $elementData
 * @access private
 * @return void
 */
	function __attributes($rType, $elementData = array(), $clear = true) {
		extract($this->__settings);
		if ($rType == $type) {
			$attributes = $this->__typeAttributes;
			if ($clear) {
				$this->__typeAttributes = $this->__typeAttributesNext;
				$this->__typeAttributesNext = array();
			}
		} else {
			$attributes = $this->__itemAttributes;
			$this->__itemAttributes = array();
			if ($clear) {
				$this->__itemAttributes = array();
			}
		}
		if ($autoPath && $depth) {
			if ($this->__settings['data'][$model][$left] < $autoPath[0] && $this->__settings['data'][$model][$right] > $autoPath[1]) {
				$attributes['class'][] = $autoPath[2];
			} elseif (isset($autoPath[3]) && $this->__settings['data'][$model][$left] == $autoPath[0]) {
				$attributes['class'][] = $autoPath[3];
			}
		}
		if ($attributes) {
			foreach ($attributes as $type => $values) {
				foreach ($values as $key => $val) {
					if (is_array($val)) {
						$attributes[$type][$key] = '';
						foreach ($val as $vKey => $v) {
							$attributes[$type][$key][$vKey] .= $vKey . ':' . $v;
						}
						$attributes[$type][$key] = implode(';', $attributes[$type][$key]);
					}
					if (is_string($key)) {
						$attributes[$type][$key] = $key . ':' . $val . ';';
					}
				}
				$attributes[$type] = $type . '="' . implode(' ', $attributes[$type]) . '"';
			}
			return ' ' . implode(' ', $attributes);
		}
		return '';
	}
}
?>