http://mackhankins.com Laravel:JsTree From Directory
Jul 2, 2016  

As a practice while reading Refactoring to Collections by Adam Wathan, I've been refactoring one my helper methods to output a json array from a directory for JsTree. My previous method was a single recursive function full of while and foreach statements, we've all been there.

My Use Case

My job deals with big data in the form of raw media where we allow patrons to order originals by browsing access copies. As you might imagine, the size of those orders can be quite large. Our future interface needs to greatly simplify that process which should keep me in the terminals I want to be in, but I digress.

Once the patron has completed their order, a coworker can select the originals to move them to a web accessible folder. That selection process is where JsTree comes into play, but it needs to be Ajax and have lots of options. Some of these folders total more than thirty-five hundred nodes.

For Development

I pulled in the Laravel-Wallpapers repo by Maksim Surguy for this demo. I manually added the 100x100 directory which is empty.

I've updated this post recently to setup for a tutorial on lazy-loading with ajax.

An Extensive Example

I'm simply returning a flat array for my desired directory which in this case is a merged array of files and folders. I don't need to do the recursive work in the class since the Storage:: facade will do that for me.

$nodes = array_merge(Storage::directories('Laravel-wallpapers'), Storage::files('Laravel-wallpapers'));

Results (partial)

Wow, you have to exclude that .git folder in the class options.
  0 => "Laravel-wallpapers/.git"
  1 => "Laravel-wallpapers/100x100"
  2 => "Laravel-wallpapers/1280x1024"
  3 => "Laravel-wallpapers/1280x800"
  4 => "Laravel-wallpapers/1366x768"
  5 => "Laravel-wallpapers/1440x2560"
  6 => "Laravel-wallpapers/1600x1200"
  7 => "Laravel-wallpapers/1600x2560"
  8 => "Laravel-wallpapers/1800x2880"
  9 => "Laravel-wallpapers/1920x1080"
  10 => "Laravel-wallpapers/1920x1200"
  11 => "Laravel-wallpapers/2560x1440"
  12 => "Laravel-wallpapers/2560x1600"
  13 => "Laravel-wallpapers/2880x1800"
  14 => "Laravel-wallpapers/.git/config"
  15 => "Laravel-wallpapers/.git/description"
  16 => "Laravel-wallpapers/.git/HEAD"
  17 => "Laravel-wallpapers/.git/hooks/applypatch-msg.sample"
  18 => "Laravel-wallpapers/.git/hooks/commit-msg.sample"
  19 => "Laravel-wallpapers/.git/hooks/post-update.sample"
  20 => "Laravel-wallpapers/.git/hooks/pre-applypatch.sample"
  21 => "Laravel-wallpapers/.git/hooks/pre-commit.sample"
  22 => "Laravel-wallpapers/.git/hooks/pre-push.sample"
  23 => "Laravel-wallpapers/.git/hooks/pre-rebase.sample"
  24 => "Laravel-wallpapers/.git/hooks/prepare-commit-msg.sample"
  25 => "Laravel-wallpapers/.git/hooks/update.sample"
  26 => "Laravel-wallpapers/.git/index"
  27 => "Laravel-wallpapers/.git/info/exclude"
  28 => "Laravel-wallpapers/.git/logs/HEAD"
  29 => "Laravel-wallpapers/.git/logs/refs/heads/master"

Here's the view rendering and ajax method from the controller for this example with every option set. At work, I pass an array of options with the node I want to load to public function treeData().

    /**
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function tree()
    {
        return view('tree');
    }

    /**
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function treeData(Request $request)
    {
        $id = 'Laravel-wallpapers';

        if($request->has('id') and $request->id != '#')
        {
            $id = $request->id;
        }

        $nodes = array_merge(
            Storage::directories($id),
            Storage::files($id)
        );

        $tree = new JsTree($nodes, 'Laravel-wallpapers');
        $tree->folderIconClass = 'fa fa-folder';
        $tree->fileIconClass = 'fa fa-file';
        $tree->setExcludedExtensions(['DS_Store', 'gitignore']);
        $tree->setExcludedPaths(['Laravel-wallpapers/1280x1024', 'laravel_dark', 'Laravel-wallpapers/.git']);
        $tree->setDisabledFolders(['Laravel-wallpapers']);
        $tree->setDisabledExtensions(['md', 'png']);
        $tree->setOpenedFolders(['Laravel-wallpapers/1280x800']);
        $tree->setLiFolderAttributes(['class' => 'li-folder-blah']);
        $tree->setAFileAttributes(['class' => 'a-file-blah']);
        $tree->setAFolderAttributes(['class' => 'a-folder-blah']);

        return response()->json($tree->build());
    }

JsTree.php Class

<?php

namespace App\Support;

/**
 * Class JsTree
 *
 * Example Directory:
 * https://github.com/msurguy/Laravel-wallpapers.git
 *
 * Usuage: (http://mackhankins.com/blog/laravel-jstree-from-directory)
 * $tree = new JsTree($nodes, $base);
 * $tree->folderIconClass = 'fa fa-folder';
 * $tree->fileIconClass = 'fa fa-file';
 * $tree->setExcludedExtensions(['DS_Store', 'gitignore']);
 * $tree->setExcludedPaths(['Laravel-wallpapers/1280x1024', 'laravel_dark', 'Laravel-wallpapers/.git']);
 * $tree->setDisabledFolders(['Laravel-wallpapers']);
 * $tree->setDisabledExtensions(['md', 'png', 'jpg']);
 * $tree->setOpenedFolders(['Laravel-wallpapers', 'Laravel-wallpapers/1280x800']);
 * $tree->setLiFolderAttributes(['class' => 'li-folder-blah']);
 * $tree->setAFileAttributes(['class' => 'a-file-blah']);
 * $tree->setAFolderAttributes(['class' => 'a-folder-blah']);
 * return $tree->build();
 * 
 * @package App\Support
 */
class JsTree
{

    /**
     * @var array
     */
    protected $dir;

    /**
     * @var array
     */
    protected $excludedExtensions = [];

    /**
     * @var array
     */
    protected $excludedPaths = [];

    /**
     * @var array
     */
    protected $disabledExtensions = [];

    /**
     * @var array
     */
    protected $disabledFolders = [];

    /**
     * @var array
     */
    protected $openedFolders = [];

    /**
     * @var array
     */
    protected $liFolderAttributes = [];

    /**
     * @var array
     */
    protected $aFileAttributes = [];

    /**
     * @var array
     */
    protected $aFolderAttributes = [];

    /**
     * @var string
     */
    public $fileIconClass;

    /**
     * @var string
     */
    public $folderIconClass;

    /**
     * JsTree constructor.
     * @param $dir
     * @param $base
     */
    public function __construct($dir, $base)
    {
        $this->dir = $dir;
        $this->base = $base;
    }

    /**
     * Just a little helper for the setters.
     *
     * @param $array
     * @param $message
     * @throws \Exception
     */
    private function isArray($array, $message)
    {
        if (!is_array($array)) {
            throw new \Exception($message);
        }
    }

    /**
     * Array of extension without the period you wou like excluded
     *
     * @param array $excludedExtensions
     */
    public function setExcludedExtensions($excludedExtensions)
    {
        $this->isArray($excludedExtensions, 'Exclude extensions must be an array');

        $this->excludedExtensions = $excludedExtensions;
    }

    /**
     * Paths array for exclusion will match partials or children
     *
     * @param array $excludedPaths
     */
    public function setExcludedPaths($excludedPaths)
    {
        $this->isArray($excludedPaths, 'Excluded paths must be an array.');

        $this->excludedPaths = $excludedPaths;
    }

    /**
     * Array for extensions you want disabled
     *
     * @param array $disabledExtensions
     */
    public function setDisabledExtensions($disabledExtensions)
    {
        $this->isArray($disabledExtensions, 'Disabled extensions must be an array');

        $this->disabledExtensions = $disabledExtensions;
    }

    /**
     * An array of folders disabled (expects exact match)
     *
     * @param array $disabledFolders
     */
    public function setDisabledFolders($disabledFolders)
    {
        $this->isArray($disabledFolders, 'Disabled folders must be an array');

        $this->disabledFolders = $disabledFolders;
    }

    /**
     * An array of folders you want opened on load (expects exact match)
     *
     * @param array $openedFolders
     */
    public function setOpenedFolders($openedFolders)
    {
        $this->isArray($openedFolders, 'Open folders must be an array.');

        $this->openedFolders = $openedFolders;
    }

    /**
     * Array of attributes you want to attach to list element for folders.
     *
     * @param array $liFolderAttributes
     */
    public function setLiFolderAttributes($liFolderAttributes)
    {
        $this->isArray($liFolderAttributes, '<li> folder attributes must be an array.');

        $this->liFolderAttributes = $liFolderAttributes;
    }

    /**
     * Array of attributes you want to attach to link element for files.
     *
     * @param array $aFileAttributes
     */
    public function setAFileAttributes($aFileAttributes)
    {
        $this->isArray($aFileAttributes, '<a> file attributes must be an array.');

        $this->aFileAttributes = $aFileAttributes;
    }

    /**
     * Array of attributes you want to attach to link element for folders.
     *
     * @param array $aFolderAttributes
     */
    public function setAFolderAttributes($aFolderAttributes)
    {
        $this->isArray($aFolderAttributes, '<a> folder attributes must be an array.');

        $this->aFolderAttributes = $aFolderAttributes;
    }

    /**
     * Converts the raw array to a collection and filters based on set variable arrays.
     *
     * @param $elements
     * @return static
     */
    protected function filterExcludes($elements)
    {
        $nodes = collect($elements)->filter(function ($element) {

            $ext = pathinfo($element, PATHINFO_EXTENSION);

            return !collect($this->excludedExtensions)->contains($ext);

        })->filter(function ($node) {

            return !collect($this->excludedPaths)->contains(function ($key, $value) use ($node) {

                if (strpos($node, $value) !== false) {
                    return true;
                }

            });

        });

        return $nodes;
    }

    /**
     * Returns disabled : true
     *
     * @param $node
     * @param string $type
     * @return bool
     */
    protected function filterDisabled($node, $type = 'folder')
    {
        if ($type == 'folder') {
            return collect($this->disabledFolders)->contains($node);
        }

        $ext = pathinfo($node, PATHINFO_EXTENSION);

        return collect($this->disabledExtensions)->contains($ext);
    }

    /**
     * Returns opened : true for folders
     *
     * @param $node
     * @return bool
     */
    protected function filterOpened($node)
    {
        return collect($this->openedFolders)->contains($node);
    }

    /**
     * Sets the attributes for desired node_type based on variable arrays.
     *
     * @param string $node_type
     * @param $attr_type
     * @return array
     */
    protected function filterAttributes($node_type = 'folder', $attr_type)
    {
        switch ($node_type) {
            case 'folder':

                if ($attr_type == 'a') {
                    return $this->liFolderAttributes;
                }

                return $this->aFolderAttributes;

                break;

            default:

                return $this->aFileAttributes;

                break;
        }
    }

    /**
     * Builds based on input and options set.
     * ->sort() could be removed if you like
     * Returns array.
     *
     * @return string
     */
    public function build()
    {
        $elements = $this->dir;

        $filters = $this->filterExcludes($elements);

        $nodes = collect($filters)->map(function ($node) {
            $file = (pathinfo($node, PATHINFO_EXTENSION) ? true : false);

            return [
                'id'      => $node,
                'parent'  => ($this->base == dirname($node) ? '#' : dirname($node) ),
                'text'    => basename($node),
                'icon'    => ($file ? $this->fileIconClass : $this->folderIconClass),
                'state'   => [
                    'disabled' => ($file ? $this->filterDisabled($node, 'file') : $this->filterDisabled($node)),
                    'opened'   => ($file ? false : $this->filterOpened($node)),
                ],
                'children' => ($file ? false : true),
                'li_attr' => $this->filterAttributes('folder', 'li'),
                'a_attr'  => ($file ? $this->filterAttributes('file', 'a') : $this->filterAttributes('folder', 'a')),
            ];
        })
            ->sort();


        return $nodes->values()->toArray();
    }

}