diff --git a/editor/media/editor/jquery.treeTable.css b/editor/media/editor/jquery.treeTable.css
new file mode 100644
index 0000000..f9fccd9
--- /dev/null
+++ b/editor/media/editor/jquery.treeTable.css
@@ -0,0 +1,65 @@
+/* jQuery TreeTable Core 2.0 stylesheet
+ *
+ * This file contains styles that are used to display the tree table. Each tree
+ * table is assigned the +treeTable+ class.
+ * ========================================================================= */
+
+/* jquery.treeTable.collapsible
+ * ------------------------------------------------------------------------- */
+.treeTable tr td .expander {
+ background-position: left center;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ padding: 0;
+ zoom: 1; /* IE7 Hack */
+}
+
+.treeTable tr.collapsed td .expander {
+ background-image: url(toggle-expand-dark.png);
+}
+
+.treeTable tr.expanded td .expander {
+ background-image: url(toggle-collapse-dark.png);
+}
+
+#changelist table.treeTable td.disclosure {
+ font-size: 12px;
+ text-align: left;
+ font-weight: bold;
+ padding-left: 19px;
+}
+#changelist table.treeTable td.disclosure input[type="checkbox"] {
+ margin-right: 10px;
+}
+
+/* jquery.treeTable.sortable
+ * ------------------------------------------------------------------------- */
+.treeTable tr.selected, .treeTable tr.accept {
+ background-color: #3875d7;
+ color: #fff;
+}
+
+.treeTable .ui-draggable-dragging {
+ border-width: 0;
+}
+
+.treeTable tr.selected, .treeTable tr.ghost_row td {
+ padding : 5px;
+ background-color : #ccc;
+ border : dotted 1px #eee;
+ font-weight : bold;
+ text-align : center;
+}
+
+.treeTable tr.collapsed.selected td .expander, .treeTable tr.collapsed.accept td .expander {
+ background-image: url(toggle-expand-light.png);
+}
+
+.treeTable tr.expanded.selected td .expander, .treeTable tr.expanded.accept td .expander {
+ background-image: url(toggle-collapse-light.png);
+}
+
+.treeTable .ui-draggable-dragging {
+ color: #000;
+ z-index: 1;
+}
\ No newline at end of file
diff --git a/editor/media/editor/toggle-collapse-dark.png b/editor/media/editor/toggle-collapse-dark.png
new file mode 100644
index 0000000..76577a5
Binary files /dev/null and b/editor/media/editor/toggle-collapse-dark.png differ
diff --git a/editor/media/editor/toggle-collapse-light.png b/editor/media/editor/toggle-collapse-light.png
new file mode 100644
index 0000000..ed1612f
Binary files /dev/null and b/editor/media/editor/toggle-collapse-light.png differ
diff --git a/editor/media/editor/toggle-expand-dark.png b/editor/media/editor/toggle-expand-dark.png
new file mode 100644
index 0000000..cfb42a4
Binary files /dev/null and b/editor/media/editor/toggle-expand-dark.png differ
diff --git a/editor/media/editor/toggle-expand-light.png b/editor/media/editor/toggle-expand-light.png
new file mode 100644
index 0000000..27b5234
Binary files /dev/null and b/editor/media/editor/toggle-expand-light.png differ
diff --git a/editor/templates/admin/editor/tree_list_results.html b/editor/templates/admin/editor/tree_list_results.html
new file mode 100644
index 0000000..703bf45
--- /dev/null
+++ b/editor/templates/admin/editor/tree_list_results.html
@@ -0,0 +1,24 @@
+{% if result_hidden_fields %}
+
{# DIV for HTML validation #}
+{% for item in result_hidden_fields %}{{ item }}{% endfor %}
+
+{% endif %}
+{% if results %}
+
+{% endif %}
diff --git a/example/static/editor/jquery.treeTable.css b/example/static/editor/jquery.treeTable.css
new file mode 100644
index 0000000..f9fccd9
--- /dev/null
+++ b/example/static/editor/jquery.treeTable.css
@@ -0,0 +1,65 @@
+/* jQuery TreeTable Core 2.0 stylesheet
+ *
+ * This file contains styles that are used to display the tree table. Each tree
+ * table is assigned the +treeTable+ class.
+ * ========================================================================= */
+
+/* jquery.treeTable.collapsible
+ * ------------------------------------------------------------------------- */
+.treeTable tr td .expander {
+ background-position: left center;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ padding: 0;
+ zoom: 1; /* IE7 Hack */
+}
+
+.treeTable tr.collapsed td .expander {
+ background-image: url(toggle-expand-dark.png);
+}
+
+.treeTable tr.expanded td .expander {
+ background-image: url(toggle-collapse-dark.png);
+}
+
+#changelist table.treeTable td.disclosure {
+ font-size: 12px;
+ text-align: left;
+ font-weight: bold;
+ padding-left: 19px;
+}
+#changelist table.treeTable td.disclosure input[type="checkbox"] {
+ margin-right: 10px;
+}
+
+/* jquery.treeTable.sortable
+ * ------------------------------------------------------------------------- */
+.treeTable tr.selected, .treeTable tr.accept {
+ background-color: #3875d7;
+ color: #fff;
+}
+
+.treeTable .ui-draggable-dragging {
+ border-width: 0;
+}
+
+.treeTable tr.selected, .treeTable tr.ghost_row td {
+ padding : 5px;
+ background-color : #ccc;
+ border : dotted 1px #eee;
+ font-weight : bold;
+ text-align : center;
+}
+
+.treeTable tr.collapsed.selected td .expander, .treeTable tr.collapsed.accept td .expander {
+ background-image: url(toggle-expand-light.png);
+}
+
+.treeTable tr.expanded.selected td .expander, .treeTable tr.expanded.accept td .expander {
+ background-image: url(toggle-collapse-light.png);
+}
+
+.treeTable .ui-draggable-dragging {
+ color: #000;
+ z-index: 1;
+}
\ No newline at end of file
diff --git a/example/static/editor/jquery.treeTable.js b/example/static/editor/jquery.treeTable.js
new file mode 100644
index 0000000..522b2c5
--- /dev/null
+++ b/example/static/editor/jquery.treeTable.js
@@ -0,0 +1,458 @@
+/*
+ * jQuery treeTable Plugin 2.3.0
+ * http://ludo.cubicphuse.nl/jquery-plugins/treeTable/
+ *
+ * Copyright 2010, Ludo van den Boom
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ */
+(function($) {
+ // Helps to make options available to all functions
+ // TODO: This gives problems when there are both expandable and non-expandable
+ // trees on a page. The options shouldn't be global to all these instances!
+ var options;
+ var defaultPaddingLeft;
+ var ghost_row;
+
+ $.fn.treeTable = function(opts) {
+ options = $.extend({}, $.fn.treeTable.defaults, opts);
+
+ var _return = this.each(function() {
+ $(this).addClass("treeTable").find("tbody tr").each(function() {
+ // Initialize root nodes only if possible
+ if(!options.expandable || $(this)[0].className.search(options.childPrefix) == -1) {
+ // To optimize performance of indentation, I retrieve the padding-left
+ // value of the first root node. This way I only have to call +css+
+ // once.
+ if (isNaN(defaultPaddingLeft)) {
+ defaultPaddingLeft = parseInt($($(this).children("td")[options.treeColumn]).css('padding-left'), 10);
+ }
+
+ initialize($(this));
+ } else if(options.initialState == "collapsed") {
+ this.style.display = "none"; // Performance! $(this).hide() is slow...
+ }
+ });
+ });
+
+ if(options.draggable)
+ initDragDrop($(this));
+
+ return _return;
+
+ };
+
+ $.fn.treeTable.defaults = {
+ childPrefix: "child-of-",
+ clickableNodeNames: false,
+ expandable: true,
+ indent: 19,
+ initialState: "collapsed",
+ treeColumn: 0,
+ draggable: false,
+ dragTarget: "tbody tr td.drag_handle",
+ dropTarget: "tbody tr",
+ dropCallback : function(node, newParent, e, ui){ },
+ sortable: false,
+ sortableDropCallback : function(node, newPrevious, e, ui){ },
+ branchMovedAsFirstChild : function(node, parent){ },
+ branchMovedAsPrevSibling : function(node, nextSibling){ },
+ branchMovedAsNextSibling : function(node, prevSibling){ }
+ };
+
+ // Recursively hide all node's children in a tree
+ $.fn.collapse = function() {
+ $(this).addClass("collapsed");
+
+ childrenOf($(this)).each(function() {
+ initialize($(this));
+
+ if(!$(this).hasClass("collapsed")) {
+ $(this).collapse();
+ }
+
+ this.style.display = "none"; // Performance! $(this).hide() is slow...
+ });
+ recolor_lines();
+ return this;
+ };
+
+ // Recursively show all node's children in a tree
+ $.fn.expand = function() {
+ $(this).removeClass("collapsed").addClass("expanded");
+
+ childrenOf($(this)).each(function() {
+ initialize($(this));
+
+ if($(this).is(".expanded.parent")) {
+ $(this).expand();
+ }
+
+ $(this).show();
+ });
+ recolor_lines();
+ return this;
+ };
+
+ // Reveal a node by expanding all ancestors
+ $.fn.reveal = function() {
+ $(ancestorsOf($(this)).reverse()).each(function() {
+ initialize($(this));
+ $(this).expand().show();
+ });
+ recolor_lines();
+ return this;
+ };
+
+ // Add an entire branch to +destination+
+ $.fn.appendBranchTo = function(destination) {
+ var node = $(this);
+ var parent = parentOf(node);
+
+ destination = $(destination);
+ var ancestorNames = $.map(ancestorsOf(destination), function(a) {
+ return a.id;
+ });
+
+ // Conditions:
+ // 1: +node+ should not be inserted in a location in a branch if this would
+ // result in +node+ being an ancestor of itself.
+ // 2: +node+ should not have a parent OR the destination should not be the
+ // same as +node+'s current parent (this last condition prevents +node+
+ // from being moved to the same location where it already is).
+ // 3: +node+ should not be inserted as a child of +node+ itself.
+ if($.inArray(node[0].id, ancestorNames) == -1 && (!parent || (destination.attr('id') != parent[0].id)) && destination.attr('id') != node[0].id) {
+ indent(node, ancestorsOf(node).length * options.indent * -1); // Remove indentation
+
+ if(parent) {
+ node.removeClass(options.childPrefix + parent[0].id);
+ add_expandable_widget($(parent));
+ }
+
+ node.addClass(options.childPrefix + destination.attr('id'));
+ move(node, destination); // Recursively move nodes to new location
+ indent(node, ancestorsOf(node).length * options.indent);
+
+ add_expandable_widget(destination);
+
+ options.branchMovedAsFirstChild(node, destination);
+
+ }
+
+ return this;
+ };
+
+ // Reshuffle an entire branch behind another one
+ $.fn.moveBranchBefore = function(destination) {
+ var node = $(this);
+
+ // Move the node
+ node.insertBefore(destination);
+ childrenOf(node).reverse().each(function() {
+ move($(this), node[0]);
+ });
+
+ options.branchMovedAsPrevSibling(node, destination);
+
+ return this;
+
+ };
+
+ // Reshuffle an entire branch in front of another one
+ $.fn.moveBranchAfter = function(destination) {
+ var node = $(this);
+
+ // Move the node
+ if(childrenOf(destination).length > 0) {
+ node.insertAfter(lastChildOf(destination));
+ } else {
+ node.insertAfter(destination);
+ }
+
+ childrenOf(node).reverse().each(function() {
+ move($(this), node[0]);
+ });
+
+ options.branchMovedAsNextSibling(node, destination);
+
+ return this;
+
+ };
+
+ // Add reverse() function from JS Arrays
+ $.fn.reverse = function() {
+ return this.pushStack(this.get().reverse(), arguments);
+ };
+
+ // Toggle an entire branch
+ $.fn.toggleBranch = function() {
+ if($(this).hasClass("collapsed")) {
+ $(this).expand();
+ } else {
+ $(this).removeClass("expanded").collapse();
+ }
+
+ return this;
+ };
+
+ // Get the parent of the node
+ $.fn.parentOf = function() {
+ var classNames = $(this)[0].className.split(' ');
+
+ for(key in classNames) {
+ if(classNames[key].match(options.childPrefix)) {
+ return $("#" + classNames[key].substring(9));
+ }
+ }
+ };
+
+
+
+ var cur_level = 0;
+
+ // This will serialise a table (given a selector) and return a string
+ // for passing to some sort of asyncronous request
+ $.fn.serializeTreeTable = function() {
+ var table_id = this[0].id;
+ var serialised_contents = [];
+ cur_level = 0;
+
+ $(this).find("tbody tr").each(function() {
+ if($(this)[0].className.search("child-of-") == -1 && $(this)[0].className.search("ghost_row") == -1) {
+ serializeChildren(table_id, $(this), serialised_contents, "");
+ cur_level++;
+ }
+ });
+
+ return serialised_contents.join("&");
+
+ };
+
+ function serializeChildren(table_id, parent, serialed, cur_child_string) {
+
+ serialed.push(table_id+"["+cur_level+"]"+cur_child_string+"[id]="+parent[0].id);
+ var kids = $(parent).nextAll("tr.child-of-" + parent[0].id);
+
+ cur_child_string += "[children]";
+
+ if(kids.length > 0)
+ {
+ var kid_level = 0;
+ kids.each(function() {
+ serializeChildren(table_id, $(this), serialed, cur_child_string + "["+kid_level+"]");
+ kid_level++;
+ });
+ }
+ }
+
+
+ // === Private functions
+
+ function ancestorsOf(node) {
+ var ancestors = [];
+ while((node = parentOf(node))) {
+ ancestors[ancestors.length] = node[0];
+ }
+ return ancestors;
+ }
+
+ function childrenOf(node) {
+ return $("table.treeTable tbody tr." + options.childPrefix + node[0].id);
+ }
+
+ function lastChildOf(node) {
+ return $("table.treeTable tbody tr." + options.childPrefix + node[0].id+":last");
+ }
+
+ function getPaddingLeft(node) {
+ var paddingLeft = parseInt(node[0].style.paddingLeft, 10);
+ return (isNaN(paddingLeft)) ? defaultPaddingLeft : paddingLeft;
+ }
+
+ function indent(node, value) {
+ var cell = $(node.children("td")[options.treeColumn]);
+ cell[0].style.paddingLeft = getPaddingLeft(cell) + value + "px";
+
+ childrenOf(node).each(function() {
+ indent($(this), value);
+ });
+ }
+
+ function recolor_lines() {
+ $('tbody tr').removeClass('row1').removeClass('row2');
+ $('tbody tr:visible:even').addClass('row1');
+ $('tbody tr:visible:odd').addClass('row2');
+ };
+
+ function initialize(node) {
+ if(!node.hasClass("initialized")) {
+ node.addClass("initialized");
+
+ var childNodes = childrenOf(node);
+
+ if(!node.hasClass("parent") && childNodes.length > 0) {
+ node.addClass("parent");
+ }
+
+ if(node.hasClass("parent")) {
+ var cell = $(node.children("td")[options.treeColumn]);
+ var padding = getPaddingLeft(cell) + options.indent;
+
+ childNodes.each(function() {
+ $(this).children("td")[options.treeColumn].style.paddingLeft = padding + "px";
+ });
+
+ add_expandable_widget(node);
+
+ }
+ recolor_lines();
+ }
+ }
+
+
+ function initDragDrop(table) {
+
+ // Configure draggable nodes
+ $(options.dragTarget).draggable({
+ helper: function () {
+ var helper = $(this).clone();
+ helper.find('span.expander').remove();
+ return helper;
+ },
+ opacity: 0.75,
+ refreshPositions: true, // Performance?
+ revert: "invalid",
+ revertDuration: 300,
+ scroll: true
+ });
+
+ // If we want a sortable one then we need to create a ghosted row
+ if(options.sortable)
+ {
+ var cell_count = $(options.dropTarget).parents("tr").find("td").length;
+ ghost_row = $("| Drop here to reorder |
");
+ ghost_row.insertBefore($(table).find("tr:first"));
+ ghost_row.hide();
+ }
+
+ // Configure droppable nodes
+ $(options.dropTarget).droppable({
+ accept: options.dragTarget,
+ drop: function(e, ui) {
+
+ if(options.sortable)
+ ghost_row.hide();
+
+ var node = $($(ui.draggable).parents("tr"));
+ var newParent = $(this);
+ if (! newParent.is('tr')) {
+ newParent = newParent.parents('tr');
+ }
+ // Move the branch when we drop on it
+ node.appendBranchTo(newParent);
+
+ // Custom callback
+ options.dropCallback(node, newParent, e, ui);
+
+ },
+ hoverClass: "accept",
+ over: function(e, ui) {
+
+ // Deal with displaying the ghost row when sorting
+ if(options.sortable)
+ {
+ var my_par = $(this).parentOf();
+ var draggy_par = $(ui.draggable.parents("tr")).parentOf();
+
+ if(my_par != undefined && draggy_par != undefined)
+ {
+ if(my_par[0].id == draggy_par[0].id && $(this)[0].id != $(ui.draggable.parents("tr"))[0].id)
+ {
+ ghost_row.insertAfter(this);
+ ghost_row.show();
+ }
+ else
+ $(ghost_row).hide();
+ }
+
+ }
+
+ if(this.id != ui.draggable.parents("tr")[0].id && !$(this).is(".expanded"))
+ $(this).expand();
+
+ }
+ });
+
+ // Sort out resorting if we have it enabled
+ if(options.sortable)
+ {
+
+ $("tr td span.ghost_text").each(function() {
+ $(this).parents("tr").droppable( {
+ accept: options.dragTarget,
+ drop: function(e, ui) {
+ ghost_row.hide();
+ var node = $(ui.draggable.parents("tr"));
+ var newPrevious = $(this).prev("tr");
+ node.moveBranchAfter(newPrevious);
+
+ // Custom callback
+ options.sortableDropCallback(node, newPrevious, e, ui);
+
+ }
+ });
+ });
+
+ }
+
+ }
+
+ // Add expandable button to a node
+ function add_expandable_widget(node)
+ {
+
+ if(options.expandable) {
+
+ var cell = $(node.children("td")[options.treeColumn]);
+
+ if(childrenOf(node).length == 0)
+ {
+ cell.find("span.expander").remove();
+ return;
+ }
+ cell.prepend('');
+ $(cell[0].firstChild).click(function() {
+ node.toggleBranch();
+ });
+
+ if(options.clickableNodeNames) {
+ cell[0].style.cursor = "pointer";
+ $(cell).click(function(e) {
+ // Don't double-toggle if the click is on the existing expander icon
+ if (e.target.className != 'expander') {
+ node.toggleBranch();
+ }
+ });
+ }
+
+ // Check for a class set explicitly by the user, otherwise set the default class
+ if(!(node.hasClass("expanded") || node.hasClass("collapsed"))) {
+ node.addClass(options.initialState);
+ }
+
+ if(node.hasClass("expanded")) {
+ node.expand();
+ }
+ }
+ }
+
+ function move(node, destination) {
+ node.insertAfter(destination);
+ childrenOf(node).reverse().each(function() {
+ move($(this), node[0]);
+ });
+ }
+
+ function parentOf(node) {
+ return $(node).parentOf();
+ }
+})(django.jQuery);
\ No newline at end of file
diff --git a/example/static/editor/toggle-collapse-dark.png b/example/static/editor/toggle-collapse-dark.png
new file mode 100644
index 0000000..76577a5
Binary files /dev/null and b/example/static/editor/toggle-collapse-dark.png differ
diff --git a/example/static/editor/toggle-collapse-light.png b/example/static/editor/toggle-collapse-light.png
new file mode 100644
index 0000000..ed1612f
Binary files /dev/null and b/example/static/editor/toggle-collapse-light.png differ
diff --git a/example/static/editor/toggle-expand-dark.png b/example/static/editor/toggle-expand-dark.png
new file mode 100644
index 0000000..cfb42a4
Binary files /dev/null and b/example/static/editor/toggle-expand-dark.png differ
diff --git a/example/static/editor/toggle-expand-light.png b/example/static/editor/toggle-expand-light.png
new file mode 100644
index 0000000..27b5234
Binary files /dev/null and b/example/static/editor/toggle-expand-light.png differ