Index: theme/standard/styles_layout.css
===================================================================
RCS file: /cvsroot/moodle/moodle/theme/standard/styles_layout.css,v
retrieving revision 1.698
diff -u -r1.698 styles_layout.css
--- theme/standard/styles_layout.css	9 Jul 2009 07:35:05 -0000	1.698
+++ theme/standard/styles_layout.css	9 Jul 2009 20:08:08 -0000
@@ -1674,6 +1674,24 @@
   margin-right:4px;
 }
 
+.block_tree, .block_tree ul {
+  padding-left: 16px;
+  list-style: none;
+}
+.block_tree .tree_item {
+  padding-left: 16px;
+  margin-left: -16px;
+}
+.block_tree .tree_item {
+    background: url(../../pix/t/expanded.png) center left no-repeat;
+}
+.block_tree .collapsed .tree_item {
+  background-image: url(../../pix/t/collapsed.png);
+}
+.block_tree .collapsed ul {
+  display: none;
+}
+
 .blockconfigtable {
   margin-top: 0;
   margin-right: auto;
Index: lib/javascript-static.js
===================================================================
RCS file: /cvsroot/moodle/moodle/lib/javascript-static.js,v
retrieving revision 1.76
diff -u -r1.76 javascript-static.js
--- lib/javascript-static.js	9 Jul 2009 07:35:07 -0000	1.76
+++ lib/javascript-static.js	9 Jul 2009 20:08:08 -0000
@@ -702,18 +702,45 @@
             fn();
     }
 }
-
-function getElementsByClassName(oElm, strTagName, oClassNames) {
+/**
+ * Replacement for getElementsByClassName in browsers that aren't cool enough
+ * 
+ * Relying on the built-in getElementsByClassName is far, far faster than
+ * using YUI.
+ * 
+ * Note: the third argument used to be an object with odd behaviour. It now
+ * acts like the 'name' in the HTML5 spec, though the old behaviour is still
+ * mimicked if you pass an object.
+ *
+ * @param {Node} oElm The top-level node for searching. To search a whole
+ *                    document, use `document`.
+ * @param {String} strTagName filter by tag names
+ * @param {String} name same as HTML5 spec
+ */
+function getElementsByClassName(oElm, strTagName, name) {
+    // for backwards compatibility
+    if(typeof name == "object") {
+        var names = new Array();
+        for(var i=0; i<name.length; i++) names.push(names[i]);
+        name = names.join('');
+    }
+    // use native implementation if possible
+    if (oElm.getElementsByClassName && Array.filter) {
+        if (strTagName == '*') {
+            return oElm.getElementsByClassName(name);
+        } else {
+            return Array.filter(oElm.getElementsByClassName(name), function(el) {
+                return el.nodeName.toLowerCase() == strTagName.toLowerCase();
+            });
+        }
+    }
+    // native implementation unavailable, fall back to slow method
     var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
     var arrReturnElements = new Array();
     var arrRegExpClassNames = new Array();
-    if(typeof oClassNames == "object") {
-        for(var i=0; i<oClassNames.length; i++) {
-            arrRegExpClassNames.push(new RegExp("(^|\\s)" + oClassNames[i].replace(/\-/g, "\\-") + "(\\s|$)"));
-        }
-    }
-    else{
-        arrRegExpClassNames.push(new RegExp("(^|\\s)" + oClassNames.replace(/\-/g, "\\-") + "(\\s|$)"));
+    var names = name.split(' ');
+    for(var i=0; i<names.length; i++) {
+        arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
     }
     var oElement;
     var bMatchesAll;
@@ -1084,3 +1111,70 @@
 function create_UFO_object(eid) {
     UFO.create(FO, eid);
 }
+
+block_tree = {
+    /**
+     * Adds all the fancy event handlers to objects of the right class
+     */
+    setup: function() {
+        var trees = getElementsByClassName(document, '*', 'block_tree');
+        for (var i = 0; i<trees.length; i++) {
+            this.list_setup(trees[i]);
+        }
+    },
+    /**
+    * recursive function to deal with the tree setup
+    * 
+    * @param {HTMLElement} list
+    */
+    list_setup: function(list) {
+        var children = list.childNodes;
+        for (var i=0; i<children.length; i++) {
+            if (children[i].nodeType == Node.ELEMENT_NODE)
+                this.item_setup(children[i]);            
+        }
+     },
+    /**
+     * Helper function
+     * 
+     * @param {HTMLElement} item
+     */
+     item_setup: function(item) {
+        div = document.createElement('div');
+        if (!item.firstChild) return;
+        var search = item.firstChild;
+        var ul = false;
+        while (search = search.nextSibling) {
+            if (search.nodeType == Node.ELEMENT_NODE && 
+                search.tagName.toLowerCase() == 'ul') {
+                ul = search;
+                break;
+            }
+        }
+        if (ul == false) return;
+        
+        while (item.firstChild != ul) {
+            div.appendChild(item.removeChild(item.firstChild));
+        }
+        div.className = 'tree_item';
+        item.insertBefore(div, ul);
+        YAHOO.util.Event.addListener(div, 'click', block_tree_item_toggle);
+        this.list_setup(ul);
+    }
+}
+
+/**
+ * Event handler for list items, toggles their collapse state
+ * 
+ * @return boolean
+ */
+function block_tree_item_toggle() {
+   node = this.parentNode;
+   if (YAHOO.util.Dom.hasClass(node, 'collapsed')) {
+       YAHOO.util.Dom.removeClass(node, 'collapsed')
+   } else {
+       YAHOO.util.Dom.addClass(node, 'collapsed')
+   }
+   /* disable any other event handlers.*/
+   return false;
+}
\ No newline at end of file
Index: lib/outputlib.php
===================================================================
RCS file: /cvsroot/moodle/moodle/lib/outputlib.php,v
retrieving revision 1.28
diff -u -r1.28 outputlib.php
--- lib/outputlib.php	9 Jul 2009 07:35:06 -0000	1.28
+++ lib/outputlib.php	9 Jul 2009 20:08:08 -0000
@@ -2099,6 +2099,23 @@
         }
         return $this->output_tag('ul', array('class' => 'list'), implode("\n", $lis));
     }
+    /**
+     * Make nested HTML list out of the items
+     *
+     * @param array[]tree_item $items
+     * @param array[string]string $attrs html attributes passed to the list
+     * @return string HTML
+     */
+    function tree_block_contents($items, $attrs=array()) {
+        if (empty($items)) return '';
+        $lis = array();
+        foreach ($items as $item) {
+            $liattr = array();
+            $lis[] = $this->output_tag('li', $item->attrs($this),
+                $item->content($this) . $this->tree_block_contents($item->children));
+        }
+        return $this->output_tag('ul', $attrs, implode("\n", $lis));
+    }
 
     /**
      * Output all the blocks in a particular region.
Index: blocks/moodleblock.class.php
===================================================================
RCS file: /cvsroot/moodle/moodle/blocks/moodleblock.class.php,v
retrieving revision 1.120
diff -u -r1.120 moodleblock.class.php
--- blocks/moodleblock.class.php	9 Jul 2009 07:35:08 -0000	1.120
+++ blocks/moodleblock.class.php	9 Jul 2009 20:08:08 -0000
@@ -1,4 +1,4 @@
-<?php  // $Id: moodleblock.class.php,v 1.120 2009/07/09 07:35:08 tjhunt Exp $
+<?php
 
 /**
  * This file contains the parent class for moodle blocks, block_base.
@@ -18,6 +18,10 @@
  * Block type of text. Contents of block should be set to standard html text in the content object as items ($this->content->text). Optionally include footer text in $this->content->footer.
  */
 define('BLOCK_TYPE_TEXT',    2);
+/**
+ * Block type of tree. $this->content->items is a list of tree_item objects and $this->content->footer is a string.
+ */
+define('BLOCK_TYPE_TREE',    3);
 
 /**
  * Class for describing a moodle block, all Moodle blocks derive from this class
@@ -492,7 +496,7 @@
             $errors[] = 'title_not_set';
             $correct = false;
         }
-        if (!in_array($this->get_content_type(), array(BLOCK_TYPE_LIST, BLOCK_TYPE_TEXT))) {
+        if (!in_array($this->get_content_type(), array(BLOCK_TYPE_LIST, BLOCK_TYPE_TEXT, BLOCK_TYPE_TREE))) {
             $errors[] = 'invalid_content_type';
             $correct = false;
         }
@@ -822,4 +826,120 @@
     }
 }
 
-?>
+/**
+ * Specialized class for displaying a tree menu
+ *
+ * @author Alan Trick
+ * @package blocks
+ */
+class block_tree extends block_list {
+    // n.b. this extends block_list so we get is_empty() for free
+    
+    public $content_type = BLOCK_TYPE_TREE;
+    
+    /**
+     * Make the formatted HTML ouput.
+     *
+     * @param moodle_core_renderer $output
+     * @return string HTML
+     */
+    protected function formatted_contents($output) {
+        // based of code in admin_tree
+        global $PAGE; // TODO change this when there is a proper way for blocks to get stuff into head.
+        $PAGE->requires->js_function_call('block_tree.setup')->on_dom_ready();
+        
+        $this->get_content();
+        return $output->tree_block_contents($this->content->items,
+            array('class'=>'block_tree'));
+    }
+}
+
+/**
+ * Each different type of tree item has it's own class. This makes it easier
+ * to cover up implementation details that are irrelevant to the block
+ * creators.
+ */
+abstract class tree_item {
+    
+    /**
+     * @var boolean setting this will toggle the initial state of the item
+     */
+    public $collapse = false;
+    /**
+     * @var array[]tree_item
+     */
+    public $children;
+    
+    /**
+     * Base constructor
+     * 
+     * Please call this when overriding it
+     */
+    function __construct() {
+        $this->children = array();
+    }
+    function attrs() {
+        if ($this->collapse) {
+            return array('class'=>'collapsed');
+        }
+        return array();
+    }
+    /**
+     * Render the content in the list item, not including sub-lists
+     * 
+     * @param moodle_renderer_base $render
+     * @return unknown probably HTML, depends on the renderer 
+     */
+    abstract function content($render);
+}
+
+class tree_item_text extends tree_item {
+    
+    protected $text;
+    
+    /**
+     * @param string $text plain text (will be escaped)
+     */
+    function __construct($text) {
+        $this->text = $text;
+        parent::__construct();
+    }
+    /**
+     * @see tree_item::content()
+     */
+    function content($render) {
+        return s($this->text);
+    }
+}
+
+class tree_item_link extends tree_item_text {
+    
+    protected $href;
+    protected $title;
+    
+    /**
+     * @param string $text plain text (will be escaped)
+     * @param string $url fully qualified URL
+     * @param string $title used for title attribute (optional)
+     */
+    function __construct($text, $url, $title = null) {
+        $this->href = $url;
+        if (isset($title)) $this->title = $title;
+        parent::__construct($text);
+    }
+    /**
+     * @see tree_item::content()
+     */
+    function content($render) {
+        /*
+         * TODO: this would be nice, but output_tag is protected
+        $attrs = array();
+        $attrs['href'] = s($this->href);
+        if (isset($this->title)) {
+            $attrs['title'] = s($this->title);
+        }
+        return $render->output_tag('a', $attrs, s($this->text)); */
+        $title = isset($this->title) ? " title='".s($this->title)."'" : '';
+        return sprintf("<a href='%s'%s>%s</a>", s($this->href), $title, s($this->text));
+    }
+}
Index: blocks/tree_test/block_tree_test.php
===================================================================
RCS file: blocks/tree_test/block_tree_test.php
diff -N blocks/tree_test/block_tree_test.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ blocks/tree_test/block_tree_test.php	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,25 @@
+<?php
+
+class block_tree_test extends block_tree {
+
+    function init() {
+        $this->title = 'block_tree test';
+        $this->version = 2009262400;
+    }
+
+    function get_content() {
+        global $CFG;
+        
+        $item = new tree_item_text('Hello World');
+        
+        $child = new tree_item_text('Don\'t look down');
+        $child->children[] = new tree_item_text('You looked down!');
+        $item->children[] = $child;
+        
+        $child = new tree_item_link('The best software evar', 'http://moodle.org');
+        $child->children[] = new tree_item_text('You need to be careful to collapse this');
+        $item->children[] = $child;
+        
+        $this->content->items = array($item);
+    }
+}
