1: <?php
2: /**
3: * The Horde_Tree_Base:: class provides the abstract interface that all
4: * drivers must derive from.
5: *
6: * Copyright 2010-2012 Horde LLC (http://www.horde.org/)
7: *
8: * See the enclosed file COPYING for license information (LGPL). If you
9: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
10: *
11: * @author Michael Slusarz <slusarz@horde.org>
12: * @category Horde
13: * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
14: * @package Tree
15: */
16: abstract class Horde_Tree_Base implements Countable
17: {
18: /**
19: * Allowed parameters for nodes.
20: *
21: * @var array
22: */
23: protected $_allowed = array();
24:
25: /**
26: * The name of this instance.
27: *
28: * @var string
29: */
30: protected $_instance = null;
31:
32: /**
33: * Hash with header information.
34: *
35: * @var array
36: */
37: protected $_header = array();
38:
39: /**
40: * An array containing all the tree nodes.
41: *
42: * @var array
43: */
44: protected $_nodes = array();
45:
46: /**
47: * The top-level nodes in the tree.
48: *
49: * @var array
50: */
51: protected $_root_nodes = array();
52:
53: /**
54: * Keep count of how many extra columns there are on the left side
55: * of the node.
56: *
57: * @var integer
58: */
59: protected $_colsLeft = 0;
60:
61: /**
62: * Keep count of how many extra columns there are on the right side
63: * of the node.
64: *
65: * @var integer
66: */
67: protected $_colsRight = 0;
68:
69: /**
70: * Option values.
71: *
72: * @var array
73: */
74: protected $_options = array(
75: 'lines' => true
76: );
77:
78: /**
79: * Stores the sorting criteria temporarily.
80: *
81: * @var string
82: */
83: protected $_sortCriteria;
84:
85: /**
86: * Should the tree be rendered statically?
87: *
88: * @var boolean
89: */
90: protected $_static = false;
91:
92: /**
93: * Constructor.
94: *
95: * @param string $name The name of this tree instance.
96: * @param array $params Additional parameters.
97: * <pre>
98: * session - (array) Callbacks used to store session data. Must define
99: * two keys: 'get' and 'set'. Function definitions:
100: * (string) = get([string - Instance], [string - ID]);
101: * set([string - Instance], [string - ID], [boolean - value]);
102: * DEFAULT: No session storage
103: * </pre>
104: */
105: public function __construct($name, array $params = array())
106: {
107: $this->_instance = $name;
108: $this->setOption($params);
109: }
110:
111: /**
112: * Provide a simpler renderer to fallback to.
113: *
114: * @return string The next best renderer.
115: * @throws Horde_Tree_Exception
116: */
117: public function fallback()
118: {
119: throw new Horde_Tree_Exception('No fallback renderer found.');
120: }
121:
122: /**
123: * Returns the tree.
124: *
125: * @param boolean $static If true the tree nodes can't be expanded and
126: * collapsed and the tree gets rendered expanded.
127: *
128: * @return string The HTML code of the rendered tree.
129: */
130: abstract public function getTree($static = false);
131:
132: /**
133: * Renders the tree.
134: *
135: * @param boolean $static If true the tree nodes can't be expanded and
136: * collapsed and the tree gets rendered expanded.
137: */
138: public function renderTree($static = false)
139: {
140: echo $this->getTree($static);
141: }
142:
143: /**
144: * Sets an option.
145: *
146: * @param mixed $option The option name -or- an array of option
147: * name/value pairs. See constructor for available
148: * options.
149: * @param mixed $value The option's value.
150: */
151: public function setOption($options, $value = null)
152: {
153: if (!is_array($options)) {
154: $options = array($options => $value);
155: }
156:
157: foreach ($options as $option => $value) {
158: $this->_options[$option] = $value;
159: }
160: }
161:
162: /**
163: * Gets an option's value.
164: *
165: * @param string $option The name of the option to fetch.
166: *
167: * @return mixed The option's value.
168: */
169: public function getOption($option)
170: {
171: return isset($this->_options[$option])
172: ? $this->_options[$option]
173: : null;
174: }
175:
176: /**
177: * Adds a node to the node tree array.
178: *
179: * @param string $id The unique node id.
180: * @param string $parent The parent's unique node id.
181: * @param string $label The text label for the node.
182: * @param string $indent Deprecated, this is calculated automatically
183: * based on the parent node.
184: * @param boolean $expanded Is this level expanded or not.
185: * @param array $params Any other parameters to set (@see
186: * self::addNodeParams() for full details).
187: * @param array $extra_right Any other columns to display to the right of
188: * the tree.
189: * @param array $extra_left Any other columns to display to the left of
190: * the tree.
191: */
192: public function addNode($id, $parent, $label, $indent = null,
193: $expanded = true, $params = array(),
194: $extra_right = array(), $extra_left = array())
195: {
196: $nodeid = $this->_nodeId($id);
197:
198: if ($session = $this->getOption('session')) {
199: $toggle_id = Horde_Util::getFormData(Horde_Tree::TOGGLE . $this->_instance);
200:
201: if ($nodeid == $toggle_id) {
202: /* We have a URL toggle request for this node. */
203: $expanded = (call_user_func($session['get'], $this->_instance, $id) !== null)
204: /* Use session state if it is set. */
205: ? !call_user_func($session['get'], $this->_instance, $nodeid)
206: /* Otherwise use what was passed through the function. */
207: : !$expanded;
208: call_user_func($session['set'], $this->_instance, $nodeid, $expanded);
209: } elseif (($exp_get = call_user_func($session['get'], $this->_instance, $nodeid)) !== null) {
210: /* If we have a saved session state use it. */
211: $expanded = $exp_get;
212: }
213: }
214:
215: $this->_nodes[$nodeid]['label'] = $label;
216: $this->_nodes[$nodeid]['expanded'] = $expanded;
217:
218: /* If any params included here add them now. */
219: if (!empty($params)) {
220: $this->addNodeParams($id, $params);
221: }
222:
223: /* If any extra columns included here add them now. */
224: if (!empty($extra_right)) {
225: $this->addNodeExtra($id, Horde_Tree::EXTRA_RIGHT, $extra_right);
226: }
227: if (!empty($extra_left)) {
228: $this->addNodeExtra($id, Horde_Tree::EXTRA_LEFT, $extra_left);
229: }
230:
231: if (is_null($parent)) {
232: if (!in_array($nodeid, $this->_root_nodes)) {
233: $this->_root_nodes[] = $nodeid;
234: }
235: } else {
236: $parent = $this->_nodeId($parent);
237: if (empty($this->_nodes[$parent]['children'])) {
238: $this->_nodes[$parent]['children'] = array();
239: }
240: if (!in_array($nodeid, $this->_nodes[$parent]['children'])) {
241: $this->_nodes[$parent]['children'][] = $nodeid;
242: }
243: }
244: }
245:
246: /**
247: * Adds additional parameters to a node.
248: *
249: * @param string $id The unique node id.
250: * @param array $params Parameters to set (key/value pairs).
251: */
252: public function addNodeParams($id, $params = array())
253: {
254: $id = $this->_nodeId($id);
255:
256: if (!is_array($params)) {
257: $params = array($params);
258: }
259:
260: foreach ($params as $p_id => $p_val) {
261: // Set only allowed and non-null params.
262: if (!is_null($p_val) && in_array($p_id, $this->_allowed)) {
263: $this->_nodes[$id][$p_id] = is_object($p_val)
264: ? strval($p_val)
265: : $p_val;
266: }
267: }
268: }
269:
270: /**
271: * Adds extra columns to be displayed to the side of the node.
272: *
273: * @param mixed $id The unique node id.
274: * @param integer $side Which side to place the extra columns on.
275: * @param array $extra Extra columns to display.
276: */
277: public function addNodeExtra($id, $side, $extra)
278: {
279: $id = $this->_nodeId($id);
280:
281: if (!is_array($extra)) {
282: $extra = array($extra);
283: }
284:
285: $col_count = count($extra);
286:
287: switch ($side) {
288: case Horde_Tree::EXTRA_LEFT:
289: $this->_nodes[$id]['extra'][Horde_Tree::EXTRA_LEFT] = $extra;
290: if ($col_count > $this->_colsLeft) {
291: $this->_colsLeft = $col_count;
292: }
293: break;
294:
295: case Horde_Tree::EXTRA_RIGHT:
296: $this->_nodes[$id]['extra'][Horde_Tree::EXTRA_RIGHT] = $extra;
297: if ($col_count > $this->_colsRight) {
298: $this->_colsRight = $col_count;
299: }
300: break;
301: }
302: }
303:
304: /**
305: * Sorts the tree by the specified node property.
306: *
307: * @param string $criteria The node property to sort by.
308: * @param integer $id Used internally for recursion.
309: */
310: public function sort($criteria, $id = -1)
311: {
312: if (!isset($this->_nodes[$id]['children'])) {
313: return;
314: }
315:
316: if ($criteria == 'key') {
317: ksort($this->_nodes[$id]['children']);
318: } else {
319: $this->_sortCriteria = $criteria;
320: usort($this->_nodes[$id]['children'], array($this, 'sortHelper'));
321: }
322:
323: foreach ($this->_nodes[$id]['children'] as $child) {
324: $this->sort($criteria, $child);
325: }
326: }
327:
328: /**
329: * Helper method for sort() to compare two tree elements.
330: */
331: public function sortHelper($a, $b)
332: {
333: if (!isset($this->_nodes[$a][$this->_sortCriteria])) {
334: return 1;
335: }
336:
337: if (!isset($this->_nodes[$b][$this->_sortCriteria])) {
338: return -1;
339: }
340:
341: return strcoll($this->_nodes[$a][$this->_sortCriteria],
342: $this->_nodes[$b][$this->_sortCriteria]);
343: }
344:
345: /**
346: * Returns whether the specified node is currently expanded.
347: *
348: * @param mixed $id The unique node id.
349: *
350: * @return boolean True if the specified node is expanded.
351: */
352: public function isExpanded($id)
353: {
354: $id = $this->_nodeId($id);
355:
356: return isset($this->_nodes[$id])
357: ? $this->_nodes[$id]['expanded']
358: : false;
359: }
360:
361: /**
362: * Adds column headers to the tree table.
363: *
364: * @param array $header An array containing hashes with header
365: * information.
366: */
367: public function setHeader($header)
368: {
369: $this->_header = $header;
370: }
371:
372: /**
373: * Set the indent level for each node in the tree.
374: *
375: * @param array $nodes TODO
376: * @param integer $indent TODO
377: */
378: protected function _buildIndents($nodes, $indent = 0)
379: {
380: foreach ($nodes as $id) {
381: $this->_nodes[$id]['indent'] = $indent;
382: if (!empty($this->_nodes[$id]['children'])) {
383: $this->_buildIndents($this->_nodes[$id]['children'], $indent + 1);
384: }
385: }
386: }
387:
388: /**
389: * Check the current environment to see if we can render the tree.
390: *
391: * @return boolean Whether or not this backend will function.
392: */
393: public function isSupported()
394: {
395: return true;
396: }
397:
398: /**
399: * Returns the escaped node ID.
400: *
401: * @param string $id Node ID.
402: *
403: * @return string Escaped node ID.
404: */
405: protected function _nodeId($id)
406: {
407: return rawurlencode($id);
408: }
409:
410: /* Countable methods. */
411:
412: public function count()
413: {
414: return count($this->_nodes);
415: }
416:
417: }
418: