PageRenderTime 45ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/wildflower/views/helpers/tree.php

http://github.com/klevo/wildflower
PHP | 393 lines | 229 code | 0 blank | 164 comment | 65 complexity | 82b8c302f2679f0b971e1f4b6d0d36f2 MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /* SVN FILE: $Id: tree.php 112 2008-06-19 11:57:33Z AD7six $ */
  3. /**
  4. * Tree Helper.
  5. *
  6. * Used the generate nested representations of hierarchial data
  7. *
  8. * PHP versions 4 and 5
  9. *
  10. * Copyright (c) 2008, Andy Dawson
  11. *
  12. * Licensed under The MIT License
  13. * Redistributions of files must retain the above copyright notice.
  14. *
  15. * @filesource
  16. * @copyright Copyright (c) 2008, Andy Dawson
  17. * @link www.ad7six.com
  18. * @package cake-base
  19. * @subpackage cake-base.app.views.helpers
  20. * @since v 1.0
  21. * @version $Revision: 112 $
  22. * @modifiedBy $LastChangedBy: AD7six $
  23. * @lastModified $Date: 2008-06-19 13:57:33 +0200 (Thu, 19 Jun 2008) $
  24. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  25. */
  26. /**
  27. * Tree helper
  28. *
  29. * Helper to generate tree representations of MPTT or recursively nested data
  30. */
  31. class TreeHelper extends AppHelper {
  32. /**
  33. * name property
  34. *
  35. * @var string 'Tree'
  36. * @access public
  37. */
  38. var $name = 'Tree';
  39. /**
  40. * settings property
  41. *
  42. * @var array
  43. * @access private
  44. */
  45. var $__settings = array();
  46. /**
  47. * typeAttributes property
  48. *
  49. * @var array
  50. * @access private
  51. */
  52. var $__typeAttributes = array();
  53. /**
  54. * typeAttributesNext property
  55. *
  56. * @var array
  57. * @access private
  58. */
  59. var $__typeAttributesNext = array();
  60. /**
  61. * itemAttributes property
  62. *
  63. * @var array
  64. * @access private
  65. */
  66. var $__itemAttributes = array();
  67. /**
  68. * helpers variable
  69. *
  70. * @var array
  71. * @access public
  72. */
  73. var $helpers = array ('Html');
  74. /**
  75. * Tree generation method.
  76. *
  77. * Accepts the results of
  78. * find('all', array('fields' => array('lft', 'rght', 'whatever'), 'order' => 'lft ASC'));
  79. * children(); // if you have the tree behavior of course!
  80. * or findAllThreaded(); and generates a tree structure of the data.
  81. *
  82. * Settings (2nd parameter):
  83. * 'model' => name of the model (key) to look for in the data array. defaults to the first model for the current
  84. * controller. If set to false 2d arrays will be allowed/expected.
  85. * 'alias' => the array key to output for a simple ul (not used if element or callback is specified)
  86. * 'type' => type of output defaults to ul
  87. * 'itemType => type of item output default to li
  88. * 'id' => id for top level 'type'
  89. * 'class' => class for top level 'type'
  90. * 'element' => path to an element to render to get node contents.
  91. * 'callback' => callback to use to get node contents. e.g. array(&$anObject, 'methodName') or 'floatingMethod'
  92. * 'autoPath' => array($left, $right [$classToAdd = 'active']) if set any item in the path will have the class $classToAdd added. MPTT only.
  93. * 'left' => name of the 'lft' field if not lft. only applies to MPTT data
  94. * 'right' => name of the 'rght' field if not lft. only applies to MPTT data
  95. * 'depth' => used internally when running recursively, can be used to override the depth in either mode.
  96. * 'firstChild' => used internally when running recursively.
  97. * 'splitDepth' => if multiple "parallel" types are required, instead of one big type, nominate the depth to do so here
  98. * 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
  99. * style/float them.
  100. * 'splitCount' => the number of "parallel" types. defaults to 3
  101. *
  102. * @param array $data data to loop on
  103. * @param array $settings
  104. * @return string html representation of the passed data
  105. * @access public
  106. */
  107. function generate ($data, $settings = array ()) {
  108. $this->__settings = array_merge(array(
  109. 'model' => null,
  110. 'alias' => 'name',
  111. 'type' => 'ul',
  112. 'itemType' => 'li',
  113. 'id' => false,
  114. 'class' => false,
  115. 'element' => false,
  116. 'callback' => false,
  117. 'autoPath' => false,
  118. 'left' => 'lft',
  119. 'right' => 'rght',
  120. 'depth' => 0,
  121. 'firstChild' => true,
  122. 'splitDepth' => false,
  123. 'splitCount' => 3,
  124. ), (array)$settings);
  125. if ($this->__settings['autoPath'] && !isset($this->__settings['autoPath'][2])) {
  126. $this->__settings['autoPath'][2] = 'active';
  127. }
  128. extract($this->__settings);
  129. $view =& ClassRegistry:: getObject('view');
  130. if ($model === null) {
  131. $model = Inflector::classify($view->params['models'][0]);
  132. }
  133. $stack = array();
  134. if ($depth == 0) {
  135. if ($class) {
  136. $this->addTypeAttribute('class', $class, null, 'previous');
  137. }
  138. if ($id) {
  139. $this->addTypeAttribute('id', $id, null, 'previous');
  140. }
  141. }
  142. $return = '';
  143. $__addType = true;
  144. foreach ($data as $i => $result) {
  145. /* Allow 2d data arrays */
  146. if (!$model) {
  147. $result[$model] = $result;
  148. }
  149. /* BulletProof */
  150. if (!isset($result[$model][$left]) && !isset($result['children'])) {
  151. $result['children'] = array();
  152. }
  153. /* Close open items as appropriate */
  154. while ($stack && ($stack[count($stack)-1] < $result[$model][$right])) {
  155. array_pop($stack);
  156. $return .= "\r\n" . str_repeat("\t",count($stack) + 1) . '</' . $type . '>';
  157. $return .= '</' . $itemType . '>';
  158. }
  159. /* Some useful vars */
  160. $hasChildren = $firstChild = $lastChild = $hasVisibleChildren = false;
  161. $numberOfDirectChildren = $numberOfTotalChildren = 0;
  162. if (isset($result['children'])) {
  163. if ($result['children']) {
  164. $hasChildren = $hasVisibleChildren = true;
  165. $numberOfDirectChildren = count($result['children']);
  166. }
  167. $prevRow = prev($data);
  168. if (!$prevRow) {
  169. $firstChild = true;
  170. }
  171. next($data);
  172. $nextRow = next($data);
  173. if (!$nextRow) {
  174. $lastChild = true;
  175. }
  176. prev($data);
  177. } elseif (isset($result[$model][$left])) {
  178. if ($result[$model][$left] != ($result[$model][$right] - 1)) {
  179. $hasChildren = true;
  180. $numberOfTotalChildren = ($result[$model][$right] - $result[$model][$left] - 1) / 2;
  181. if (isset($data[$i + 1]) && $data[$i + 1][$model][$right] < $result[$model][$right]) {
  182. $hasVisibleChildren = true;
  183. }
  184. }
  185. if (!isset($data[$i - 1]) || ($data[$i - 1][$model][$left] == ($result[$model][$left] - 1))) {
  186. $firstChild = true;
  187. }
  188. if (!isset($data[$i + 1]) || ($stack && $stack[count($stack) - 1] == ($result[$model][$right] + 1))) {
  189. $lastChild = true;
  190. }
  191. }
  192. $elementData = array(
  193. 'data' => $result,
  194. 'depth' => $depth?$depth:count($stack),
  195. 'hasChildren' => $hasChildren,
  196. 'numberOfDirectChildren' => $numberOfDirectChildren,
  197. 'numberOfTotalChildren' => $numberOfTotalChildren,
  198. 'firstChild' => $firstChild,
  199. 'lastChild' => $lastChild,
  200. 'hasVisibleChildren' => $hasVisibleChildren
  201. );
  202. $this->__settings = array_merge($this->__settings, $elementData);
  203. /* Main Content */
  204. if ($element) {
  205. $content = $view->element($element,$elementData);
  206. } elseif ($callback) {
  207. list($content) = array_map($callback, array($elementData));
  208. } else {
  209. $content = $result[$model][$alias];
  210. }
  211. if (!$content) {
  212. continue;
  213. }
  214. /* Prefix */
  215. if ($__addType) {
  216. $typeAttributes = $this->__attributes($type, array('data' => $elementData));
  217. $return .= "\r\n" . str_repeat("\t",count($stack)) . '<' . $type . $typeAttributes . '>';
  218. }
  219. $itemAttributes = $this->__attributes($itemType, $elementData);
  220. $return .= "\r\n" . str_repeat("\t",count($stack) + 1) . '<' . $itemType . $itemAttributes . '>';
  221. $return .= $content;
  222. /* Suffix */
  223. $__addType = false;
  224. if ($hasChildren) {
  225. if ($numberOfDirectChildren) {
  226. $settings['depth'] = $depth + 1;
  227. $return .= $this->__suffix();
  228. $return .= $this->generate($result['children'], $settings);
  229. $return .= '</' . $itemType . '>';
  230. } elseif ($numberOfTotalChildren) {
  231. $__addType = true;
  232. $stack[] = $result[$model][$right];
  233. }
  234. } else {
  235. $return .= '</' . $itemType . '>';
  236. $return .= $this->__suffix();
  237. }
  238. }
  239. /* Cleanup */
  240. while ($stack) {
  241. array_pop($stack);
  242. $return .= "\r\n" . str_repeat("\t",count($stack) + 1) . '</' . $type . '>';
  243. $return .= '</' . $itemType . '>';
  244. }
  245. $return .= "\r\n" . '</' . $type . '>' . "\r\n";
  246. return $return;
  247. }
  248. /**
  249. * addItemAttribute function
  250. *
  251. * Called to modify the attributes of the next <item> to be processed
  252. * Note that the content of a 'node' is processed before generating its wrapping <item> tag
  253. *
  254. * @param string $id
  255. * @param string $key
  256. * @param mixed $value
  257. * @access public
  258. * @return void
  259. */
  260. function addItemAttribute($id = '', $key = '', $value = null) {
  261. if (!is_null($value)) {
  262. $this->__itemAttributes[$id][$key] = $value;
  263. } elseif (!(isset($this->__itemAttributes[$id]) && in_array($key, $this->__itemAttributes[$id]))) {
  264. $this->__itemAttributes[$id][] = $key;
  265. }
  266. }
  267. /**
  268. * addTypeAttribute function
  269. *
  270. * Called to modify the attributes of the next <type> to be processed
  271. * Note that the content of a 'node' is processed before generating its wrapping <type> tag (if appropriate)
  272. * An 'interesting' case is that of a first child with children. To generate the output
  273. * <ul> (1)
  274. * <li>XYZ (3)
  275. * <ul> (2)
  276. * <li>ABC...
  277. * ...
  278. * </ul>
  279. * ...
  280. * The processing order is indicated by the numbers in brackets.
  281. * attributes are allways applied to the next type (2) to be generated
  282. * to set properties of the holding type - pass 'previous' for the 4th param
  283. * i.e.
  284. * // Hide children (2)
  285. * $tree->addTypeAttribute('style', 'display', 'hidden');
  286. * // give top level type (1) a class
  287. * $tree->addTypeAttribute('class', 'hasHiddenGrandChildren', null, 'previous');
  288. *
  289. * @param string $id
  290. * @param string $key
  291. * @param mixed $value
  292. * @access public
  293. * @return void
  294. */
  295. function addTypeAttribute($id = '', $key = '', $value = null, $previousOrNext = 'next') {
  296. $var = '__typeAttributes';
  297. $firstChild = isset($this->__settings['firstChild'])?$this->__settings['firstChild']:true;
  298. if ($previousOrNext == 'next' && $firstChild) {
  299. $var = '__typeAttributesNext';
  300. }
  301. if (!is_null($value)) {
  302. $this->{$var}[$id][$key] = $value;
  303. } elseif (!(isset($this->{$var}[$id]) && in_array($key, $this->{$var}[$id]))) {
  304. $this->{$var}[$id][] = $key;
  305. }
  306. }
  307. /**
  308. * suffix method
  309. *
  310. * Used to close and reopen a ul/ol to allow easier listings
  311. *
  312. * @access private
  313. * @return void
  314. */
  315. function __suffix() {
  316. static $__splitCount = 0;
  317. static $__splitCounter = 0;
  318. extract($this->__settings);
  319. if ($splitDepth) {
  320. if ($depth == $splitDepth -1) {
  321. $total = $numberOfDirectChildren?$numberOfDirectChildren:$numberOfTotalChildren;
  322. if ($total) {
  323. $__splitCounter = 0;
  324. $__splitCount = $total / $splitCount;
  325. $rounded = (int)$__splitCount;
  326. if ($rounded < $__splitCount) {
  327. $__splitCount = $rounded + 1;
  328. }
  329. }
  330. }
  331. if ($depth == $splitDepth) {
  332. $__splitCounter++;
  333. if (($__splitCounter % $__splitCount) == 0) {
  334. return '</' . $type . '><' . $type . '>';
  335. }
  336. }
  337. }
  338. return;
  339. }
  340. /**
  341. * attributes function
  342. *
  343. * Logic to apply styles to tags.
  344. *
  345. * @param mixed $rType
  346. * @param array $elementData
  347. * @access private
  348. * @return void
  349. */
  350. function __attributes($rType, $elementData = array(), $clear = true) {
  351. extract($this->__settings);
  352. if ($rType == $type) {
  353. $attributes = $this->__typeAttributes;
  354. if ($clear) {
  355. $this->__typeAttributes = $this->__typeAttributesNext;
  356. $this->__typeAttributesNext = array();
  357. }
  358. } else {
  359. $attributes = $this->__itemAttributes;
  360. $this->__itemAttributes = array();
  361. if ($clear) {
  362. $this->__itemAttributes = array();
  363. }
  364. }
  365. if ($autoPath && $depth) {
  366. if ($this->__settings['data'][$model][$left] < $autoPath[0] && $this->__settings['data'][$model][$right] > $autoPath[1]) {
  367. $attributes['class'][] = $autoPath[2];
  368. } elseif (isset($autoPath[3]) && $this->__settings['data'][$model][$left] == $autoPath[0]) {
  369. $attributes['class'][] = $autoPath[3];
  370. }
  371. }
  372. if ($attributes) {
  373. foreach ($attributes as $type => $values) {
  374. foreach ($values as $key => $val) {
  375. if (is_array($val)) {
  376. $attributes[$type][$key] = '';
  377. foreach ($val as $vKey => $v) {
  378. $attributes[$type][$key][$vKey] .= $vKey . ':' . $v;
  379. }
  380. $attributes[$type][$key] = implode(';', $attributes[$type][$key]);
  381. }
  382. if (is_string($key)) {
  383. $attributes[$type][$key] = $key . ':' . $val . ';';
  384. }
  385. }
  386. $attributes[$type] = $type . '="' . implode(' ', $attributes[$type]) . '"';
  387. }
  388. return ' ' . implode(' ', $attributes);
  389. }
  390. return '';
  391. }
  392. }
  393. ?>