TreeviewLite

TreeviewLite is a YUI3 lightweight gallery plugin. It renders, collapses nodes, and not much else. A basic example is here.

Sortable

The 3.1 release of YUI included a Sortable utility that lets you drag and drop list items within and/or between lists. I needed a Sortable Treeview, and so I thought I'd have a go at it. This is a first effort.

I want to make a treeview that you can drag a node from anywhere to anywhere. If you drag a node and it's got children, they need to move too. If you drag it onto a node that already has children, it will just add it to the list, but if you drag it onto a node without children, it'll create a new child list. Try it on the treeview below.

Update (20/04/2010) - fixed IE problem. The problem? Using the pr version of yui3, not 3.1. I don't know what changed, but something did and it works.

Update 2 (21/04/2010) - using YUI2 to load YUI3 in the page - you need an injected:true in the YUI( { injected: true }).use(...) call - much thanks to Dav Glass for this.

Markup

Start with a plain nested list:
<ol id="myOlList2">
 <li><span>item 1</span></li>
 <li><span>item 2</span>
  <ol>
   <li><span>item 2.1</span></li>
   <li><span>item 2.2</span>
     <ol>
      <li><span>item 2.2.1</span></li>
      <li><span>item 2.2.2</span></li>
      <li><span>item 2.2.3</span></li>
     </ol>
   </li>
  </ol>
 </li>
 <li><span>item 3</span>
   <ol>
    <li><span>item 3.1</span></li>
   </ol>
 </li>
</ol>

As before, you need to wrap the labels in span tags.

End result

This is what we end up with... try dragging some nodes.

  1. item 1
  2. item 2
    1. item 2.1
    2. item 2.2
      1. item 2.2.1
      2. item 2.2.2
      3. item 2.2.3
  3. item 3
    1. item 3.1

Script

We'll be using dd and sortable, gallery-treeviewlite:

YUI().use( 'dd' , 'sortable', 'gallery-treeviewlite', function(Y) { //... });

First of all we set up the Sortable instance, straight from the examples, and plug in the TreeviewLite:

var list1 = new Y.Sortable({ container: '#myOlList2', nodes: 'li', opacity: '.5', opacityNode: "dragNode", moveType: "insert" }); Y.one( "#myOlList2" ).plug( Y.Plugin.TreeviewLite );

And we'll listen to some of the drag-drop events. It's a bit more complicated with nested lists, because in some cases we just append the <li> to the existing list; in others we need to create a new child <ol>. So at the start of the drag we create a new <ol> that we move around as needed.

Y.DD.DDM is the drag-drop manager; it seemed to be the easiest place to listen for the different events.

// nodes we're moving around var addedNode, newNode; // make a new node that can be inserted as a new tree sometimes Y.DD.DDM.on("drag:start", function(ev){ newNode = Y.Node.create( "<ol></ol>" ); newNode.setAttribute("id", Y.guid() ); });

Now as we drag, we need to work out where we are and add the right nodes in the right place, and remove them from where they were previously. This is pretty heavy DOM lifting; there may be a better way to do (any|all) of this.

// insert the nodes where needed Y.DD.DDM.on("drag:over", function(ev){ // remove it from where it was if( addedNode !== undefined ) { addedNode.remove(); } var t = ev.drop.get("node"), // tOl is looking for a child ol below the li tOl = t.one( "ol" ); // if we've over an li, add the new ol child block switch( t.get("nodeName").toLowerCase() ) { case "li": // try and append it to existing ol on the target if( tOl ) { try { tOl.append( ev.drag.get("node") ); } catch(e){ } } // else add a new ol to the target else { // try adding newNode try{ t.append( newNode ); newNode.append( ev.drag.get( "node" ) ); addedNode = newNode; } catch(e){ } } break; // if we're over an ol, just add this as a new li child case "ol": try{ t.append( ev.drag.get("node" ) ); } catch(e){} break; default: break; } });

And at the end of the drag we want to tidy up and re-render the treeview. Unplugging and plugging the treeviewlite isn't a great way to do this; before too long I'll change the treeviewlite to give it an update() method or similar.

// reset things at the end of the drag Y.DD.DDM.after("drag:end", function( ev ){ addedNode = undefined; newNode = undefined; // DD somewhere sets some element styles, which mess up alignment somewhere // in IE var targetNode = ev.target.get("node"); targetNode.removeAttribute( "style" ); // re-read the DOM of the tree to put + and - classes in the right places Y.one( "#myOlList2" ).treeviewLite.renderUI(); } );

This almost works; but it is possible to drop onto the drag, which throws a pile of errors. Sortable is only designed, I think, for 1-d lists, so I needed to overwrite the _onDragOver of Sortable. It's identical except for checking if drag contains drop: // overwrite to check dragging onto itself: Y.Sortable.prototype._onDragOver = function(e){ var NODE = "node", NODES = "nodes", PARENT_NODE = "parentNode", ID = "id"; if (!e.drop.get(NODE).test(this.get(NODES))) { return; } if (e.drag.get(NODE) == e.drop.get(NODE)) { return; } // is drop a child of drag? - this is the bit that's added: if ( e.drag.get(NODE).contains( e.drop.get(NODE) ) ) { return; } switch (this.get('moveType').toLowerCase()) { case 'insert': var dir = ((this._up) ? 'before' : 'after'); e.drop.get(NODE).insert(e.drag.get(NODE), dir); break; case 'swap': Y.DD.DDM.swapNode(e.drag, e.drop); break; case 'move': case 'copy': var dropsort = Y.Sortable.getSortable(e.drop.get(NODE).get(PARENT_NODE)), oldNode, newNode; if (!dropsort) { Y.log('No delegate parent found', 'error'); return; } Y.DD.DDM.getDrop(e.drag.get(NODE)).addToGroup(dropsort.get(ID)); //Same List if (e.drag.get(NODE).get(PARENT_NODE).contains(e.drop.get(NODE))) { Y.DD.DDM.swapNode(e.drag, e.drop); } else { if (this.get('moveType') == 'copy') { //New List oldNode = e.drag.get(NODE); newNode = oldNode.cloneNode(true); newNode.set(ID, ''); e.drag.set(NODE, newNode); dropsort.delegate.createDrop(newNode, [dropsort.get(ID)]); oldNode.setStyles({ top: '', left: '' }); } e.drop.get(NODE).insert(e.drag.get(NODE), 'before'); } break; } };