Creating a treeview Part 2. Reading real directories and implementing lazy loading

Published on: February 15, 2014 Written by: Thokozani Mhlongo


We will be continuing from our last post on this treeview series. In this part we are going to be including lazy loading using server side programming (PHP), and AJAX. This demonstration is based on a folder within my local computer and this  is how it looks like on from the root directory:

Now from our previous treeview html file we will remove the markup that we manually put to create out treeview because we are going to populate it dynamically. But before we do anything client-side, let's first go look at things from the server end. We will create a script that reads the root directory and look at the result a bit (we will be using JSON). The result we will get from this script will tell us everything about each treeview node on the view (as it should be).

Our PHP script

This code is responsible for reading directories and producing the necessary JSON to make out treeview.

<?php

/**
 * @author Thokozani
 * @copyright 2013
 */

    $file_structure = array();
    $exlusions = array('.' , '..' , '...');
    $extention_exlusions = array(''); //Any exluded file extensions should be put here
    $file_extensions = array('.html' , '.htm' , '.jpg' , '.jpeg' , '.gif' , '.png' , '.tif' , '.bmp' , '.tmp' , '.txt' , '.sql' , '.css' , '.js' , '.php' , '.less', '.xml' , '.zip', '.rar');
    
    $ROOT_DIR = 'C:/xampp/htdocs/treeviewDemo'; //My root folder
    if(!empty($_POST)) $ROOT_DIR = $_POST['key']; //Else if a directory to load from is given
    
    $directories = scandir($ROOT_DIR);
    
    $topLevel = sortByFolder( $directories , $file_extensions , $exlusions , $extention_exlusions );
    
    for( $i = 0; $i < count($topLevel); $i++ ) {
        $file_structure[$i]['title'] = $topLevel[$i];
        $file_structure[$i]['key'] = "{$ROOT_DIR}/{$topLevel[$i]}";
            
        if( !isFile($topLevel[$i] , $file_extensions ) ) {
            $file_structure[$i]['isFolder'] = true;
            $file_structure[$i]['isLazy'] = true;
        } else $file_structure[$i]['isFolder'] = false;
    }
    
    header('Content-type: application/json');
    echo json_encode( $file_structure );
    
    /** Is it a file? */
    function isFile( $item , $file_extensions ) {
        if( strrpos($item , ".") > 0 ) {
            $needle = strtolower ( substr($item , strrpos($item , ".")) );
            if( in_array($needle , $file_extensions) ) return true; //Its a file
            else return false; //Its a folder
        }
        else return false; //Its a folder
    }
    
    
    function getExtension( $item ) {
        return strtolower ( substr($item , strrpos($item , ".")) );
    }
    
    
    function sortByFolder( $directories , $file_extensions , $exlusions , $extention_exlusions ) {
        $folder = array();
        $file = array();
        
        for($i = 0; $i < count( $directories ); $i++) {
            if( !in_array($directories[$i] ,$exlusions) ) { //If it is not the items that need to excluded.
                
                    if( isFile( $directories[$i], $file_extensions ) ) {
                        $tmpCheck = explode('.' , $directories[$i]);
                        
                        if( !in_array( $tmpCheck[1] , $extention_exlusions ) )
                             array_push($file , $directories[$i]);
                    }
                    else array_push($folder , $directories[$i]);    
            }
        }
        
        return array_merge( $folder , $file );
    }

?>

 

This script produces the following result:

 

If we take a look at the result given to use by this script we can see that each JSON object has some sort of metadata about every node of our treeview. We can now take advantage of HTML5 with its introduction of the data- attrtibute to create our own such as data-node-isfolder to distinguish whether the type of node are we dealing with is a folder or not. Each of these peaces of data in each JSON object is going to be a be a data-something attribute to a node.

Our data- attributes

  • data-node-title - This node is the title of the node
  • data-node-key - The most important part of any node is it's key. Its no different than a primary key of a database table for uniqueness and singularity. The key is merely a directory that points to that particular file in the file system. In case of loading new nodes inside a folder (for instance) for the first time, this key will be sent to the referenced script to be used as "the root" or "the point to load from".
  • data-node-isfolder - As we mentioned earlier, this node is for determining whether we are dealing with a folder or not, it will contain either the value true or false.
  • data-node-islazy - This node is specific to folders only. If you've any application such as Facebook or Twitter you would know what lazy loading is. In each of these apps whenever you're close the end of the page an AJAX request will be sent to the server and new content loads up as you are scrolling. In this context, we are going to load "child" nodes of a folder (that's expanded for the first time) when needed.

Of course you can add as many attributes as you'd like such as size, date-modified, etc. Now let's use our script to create the treeview. This is how our code inside the script tag now looks like:

<script type="text/javascript">
    $(document).ready(function() {
        loadTree( $(".treeview") ); //Load on the tree on first load
    });

    var nodeClick = function() {
        var node = $(this).parent(); //Get parent
        var cssclass = $(node).attr("data-node-state");

        if(cssclass === "collapsed") {
            //Check to see if it is a lazy node
            var isLazy = $(node).attr("data-node-islazy");
            if(isLazy == "true") {
                //Perform an AJAX request to load children nodes
                var key = $(node).attr("data-node-key");
                var data = {
                    key : key
                };
                loadTree( node, data , true ); //We're loading child nodes so "true"
            } else {
                //The node children have been loaded before so just expand
                expand(node);
            }
        } else {
            collapse(node);
        }
    };

    function expand( node ) {
        $(node).attr("data-node-state" , "expanded"); //Set state to flag its expanded

        //indicator image of "expanded"
        $(node).find("span:first").removeClass("collapsed").addClass("expanded"); 

        $(node).find("ul:first").slideToggle();
    }

    function collapse( node ) {
        $(node).attr("data-node-state" , "collapsed"); //Set state to flag its collapsed

        //indicator image of "collapsed"
        $(node).find("span:first").removeClass("expanded").addClass("collapsed");

        $(node).find("ul:first").slideToggle();
    }

    function loadTree( parentNode, data, ischildload ) {
        //Defaults
        if(data === undefined) data = {};
        if(ischildload === undefined) ischildload = false;

        $.ajax({
            type: "POST",
            url: "get-directories.php",
            cache: false,
            data : data,
            dataType: "json",
            success: function(data) {
                buildTree( parentNode , data, ischildload );
            //Add click event here now
            }
        });
    }

    function buildTree(parentNode, data, ischildload) {
        if(ischildload) $(parentNode).append("<ul>");

        for(var i = 0; i < data.length; i++) {
            var html = "<li data-node-title=" + data[i].title + 
            "data-node-key=" + data[i].key +
            "data-node-isfolder=" + data[i].isFolder;
            if(data[i].isFolder == true) {
                html += "data-node-state=collapsed data-node-islazy=" + data[i].isLazy + ">" + 
                "<span class=folder collapsed>" + data[i].title + "</span></li>";
            } else html += "><span>" + data[i].title + "</span></li>"

            if(!ischildload) $(parentNode).append(html); 
            else $(parentNode).find("ul:first").append(html);
        }

        if(ischildload) { 
            $(parentNode).append("</ul>");
            expand(parentNode);
            //This node is no longer a lazy loader
            $(parentNode).attr("data-node-islazy" , "false");
        }

        /* IMPORTANT! Prevents event propagation */
        $("span.folder").unbind("click");
        $("span.folder").bind("click", nodeClick); //Re-attach the click event to existing and new nodes
    }
</script>

 

We have a set of didicated functions for expanding and collapsing nodes in our treeview now. The most important functions here though are the functions loadTree() and buildTree().

loadTree() is called in two places - when the tree is loaded for the first time, and when we want to load new nodes. It takes three parameters (two of which are optional except parentNode), one for the node, the second for data that will be passed in the AJAX request, and the last serves as a flag for whether we're loading child nodes (if its set to "true") or if we're loading the tree for the first time (if its set to "false"). This function then calls buildTree() when the result has been succesfully received.

buildTree()uses the result (in JSON) to build more tree nodes. First it checks if we're loading child nodes, and if so it creates a new ul element. Next it loops through all the JSON objects and uses its value pairs to create html to append li elements (with its attributes discussed in Our data attributes) on the newly created ul that's under the parentNode. The last part of this functions is also important. We expand the tree and we set the lazy loading flag to false to indicate we've already loaded child nodes. Another vital thing to notice here is that we detach and re-attach the click event of every node. The reason for this is that the new nodes do not have a click event attached to them. The unbinding process ensures that we prevent events from queuing up for the existing nodes

 

Comments