text: /**
   * @class  elFinder folders tree
   *
   * @author Dmitry (dio) Levashov
   **/
  $.fn.elfindertree = function(fm, opts) {
  	"use strict";
  	var treeclass = fm.res('class', 'tree');
  	
  	this.not('.'+treeclass).each(function() {
  
  		var c = 'class', mobile = fm.UA.Mobile,
  			
  			/**
  			 * Root directory class name
  			 *
  			 * @type String
  			 */
  			root      = fm.res(c, 'treeroot'),
  
  			/**
  			 * Open root dir if not opened yet
  			 *
  			 * @type Boolean
  			 */
  			openRoot  = opts.openRootOnLoad,
  
  			/**
  			 * Open current work dir if not opened yet
  			 *
  			 * @type Boolean
  			 */
  			openCwd   = opts.openCwdOnOpen,
  
  			
  			/**
  			 * Auto loading current directory parents and do expand their node
  			 *
  			 * @type Boolean
  			 */
  			syncTree  = openCwd || opts.syncTree,
  			
  			/**
  			 * Subtree class name
  			 *
  			 * @type String
  			 */
  			subtree   = fm.res(c, 'navsubtree'),
  			
  			/**
  			 * Directory class name
  			 *
  			 * @type String
  			 */
  			navdir    = fm.res(c, 'treedir'),
  			
  			/**
  			 * Directory CSS selector
  			 *
  			 * @type String
  			 */
  			selNavdir = 'span.' + navdir,
  			
  			/**
  			 * Collapsed arrow class name
  			 *
  			 * @type String
  			 */
  			collapsed = fm.res(c, 'navcollapse'),
  			
  			/**
  			 * Expanded arrow class name
  			 *
  			 * @type String
  			 */
  			expanded  = fm.res(c, 'navexpand'),
  			
  			/**
  			 * Class name to mark arrow for directory with already loaded children
  			 *
  			 * @type String
  			 */
  			loaded    = 'elfinder-subtree-loaded',
  			
  			/**
  			 * Class name to mark need subdirs request
  			 *
  			 * @type String
  			 */
  			chksubdir = 'elfinder-subtree-chksubdir',
  			
  			/**
  			 * Arraw class name
  			 *
  			 * @type String
  			 */
  			arrow = fm.res(c, 'navarrow'),
  			
  			/**
  			 * Current directory class name
  			 *
  			 * @type String
  			 */
  			active    = fm.res(c, 'active'),
  			
  			/**
  			 * Droppable dirs dropover class
  			 *
  			 * @type String
  			 */
  			dropover = fm.res(c, 'adroppable'),
  			
  			/**
  			 * Hover class name
  			 *
  			 * @type String
  			 */
  			hover    = fm.res(c, 'hover'),
  			
  			/**
  			 * Disabled dir class name
  			 *
  			 * @type String
  			 */
  			disabled = fm.res(c, 'disabled'),
  			
  			/**
  			 * Draggable dir class name
  			 *
  			 * @type String
  			 */
  			draggable = fm.res(c, 'draggable'),
  			
  			/**
  			 * Droppable dir  class name
  			 *
  			 * @type String
  			 */
  			droppable = fm.res(c, 'droppable'),
  			
  			/**
  			 * root wrapper class
  			 * 
  			 * @type String
  			 */
  			wrapperRoot = 'elfinder-navbar-wrapper-root',
  
  			/**
  			 * Un-disabled cmd `paste` volume's root wrapper class
  			 * 
  			 * @type String
  			 */
  			pastable = 'elfinder-navbar-wrapper-pastable',
  			
  			/**
  			 * Un-disabled cmd `upload` volume's root wrapper class
  			 * 
  			 * @type String
  			 */
  			uploadable = 'elfinder-navbar-wrapper-uploadable',
  			
  			/**
  			 * Is position x inside Navbar
  			 * 
  			 * @param x Numbar
  			 * 
  			 * @return
  			 */
  			insideNavbar = function(x) {
  				var left = navbar.offset().left;
  					
  				return left <= x && x <= left + navbar.width();
  			},
  			
  			/**
  			 * To call subdirs elements queue
  			 * 
  			 * @type Object
  			 */
  			subdirsQue = {},
  			
  			/**
  			 * To exec subdirs elements ids
  			 * 
  			 */
  			subdirsExecQue = [],
  			
  			/**
  			 * Request subdirs to backend
  			 * 
  			 * @param id String
  			 * 
  			 * @return Deferred
  			 */
  			subdirs = function(ids) {
  				var targets = [];
  				$.each(ids, function(i, id) {
  					subdirsQue[id] && targets.push(fm.navId2Hash(id));
  					delete subdirsQue[id];
  				});
  				if (targets.length) {
  					return fm.request({
  						data: {
  							cmd: 'subdirs',
  							targets: targets,
  							preventDefault : true
  						}
  					}).done(function(res) {
  						if (res && res.subdirs) {
  							$.each(res.subdirs, function(hash, subdirs) {
  								var elm = fm.navHash2Elm(hash);
  								elm.removeClass(chksubdir);
  								elm[subdirs? 'addClass' : 'removeClass'](collapsed);
  							});
  						}
  					});
  				}
  			},
  			
  			subdirsJobRes = null,
  			
  			/**
  			 * To check target element is in window of subdirs
  			 * 
  			 * @return void
  			 */
  			checkSubdirs = function() {
  				var ids = Object.keys(subdirsQue);
  				if (ids.length) {
  					subdirsJobRes && subdirsJobRes._abort();
  					execSubdirsTm && clearTimeout(execSubdirsTm);
  					subdirsExecQue = [];
  					subdirsJobRes = fm.asyncJob(function(id) {
  						return fm.isInWindow($('#'+id))? id : null;
  					}, ids, { numPerOnce: 200 })
  					.done(function(arr) {
  						if (arr.length) {
  							subdirsExecQue = arr;
  							execSubdirs();
  						}
  					});
  				}
  			},
  			
  			subdirsPending = 0,
  			execSubdirsTm,
  			
  			/**
  			 * Exec subdirs as batch request
  			 * 
  			 * @return void
  			 */
  			execSubdirs = function() {
  				var cnt = opts.subdirsMaxConn - subdirsPending,
  					atOnce = fm.maxTargets? Math.min(fm.maxTargets, opts.subdirsAtOnce) : opts.subdirsAtOnce,
  					i, ids;
  				execSubdirsTm && cancelAnimationFrame(execSubdirsTm);
  				if (subdirsExecQue.length) {
  					if (cnt > 0) {
  						for (i = 0; i < cnt; i++) {
  							if (subdirsExecQue.length) {
  								subdirsPending++;
  								subdirs(subdirsExecQue.splice(0, atOnce)).always(function() {
  									subdirsPending--;
  									execSubdirs();
  								});
  							}
  						}
  					} else {
  						execSubdirsTm = requestAnimationFrame(function() {
  							subdirsExecQue.length && execSubdirs();
  						});
  					}
  				}
  			},
  			
  			drop = fm.droppable.drop,
  			
  			/**
  			 * Droppable options
  			 *
  			 * @type Object
  			 */
  			droppableopts = $.extend(true, {}, fm.droppable, {
  				// show subfolders on dropover
  				over : function(e, ui) {
  					var dst    = $(this),
  						helper = ui.helper,
  						cl     = hover+' '+dropover,
  						hash, status;
  					e.stopPropagation();
  					helper.data('dropover', helper.data('dropover') + 1);
  					dst.data('dropover', true);
  					if (ui.helper.data('namespace') !== fm.namespace || ! fm.insideWorkzone(e.pageX, e.pageY)) {
  						dst.removeClass(cl);
  						helper.removeClass('elfinder-drag-helper-move elfinder-drag-helper-plus');
  						return;
  					}
  					if (! insideNavbar(e.clientX)) {
  						dst.removeClass(cl);
  						return;
  					}
  					helper.removeClass('elfinder-drag-helper-move elfinder-drag-helper-plus');
  					dst.addClass(hover);
  					if (dst.is('.'+collapsed+':not(.'+expanded+')')) {
  						dst.data('expandTimer', setTimeout(function() {
  							dst.is('.'+collapsed+'.'+hover) && dst.children('.'+arrow).trigger('click');
  						}, 500));
  					}
  					if (dst.is('.elfinder-ro,.elfinder-na')) {
  						dst.removeClass(dropover);
  						//helper.removeClass('elfinder-drag-helper-move elfinder-drag-helper-plus');
  						return;
  					}
  					hash = fm.navId2Hash(dst.attr('id'));
  					dst.data('dropover', hash);
  					$.each(ui.helper.data('files'), function(i, h) {
  						if (h === hash || (fm.file(h).phash === hash && !ui.helper.hasClass('elfinder-drag-helper-plus'))) {
  							dst.removeClass(cl);
  							return false; // break $.each
  						}
  					});
  					if (helper.data('locked')) {
  						status = 'elfinder-drag-helper-plus';
  					} else {
  						status = 'elfinder-drag-helper-move';
  						if (e.shiftKey || e.ctrlKey || e.metaKey) {
  							status += ' elfinder-drag-helper-plus';
  						}
  					}
  					dst.hasClass(dropover) && helper.addClass(status);
  					requestAnimationFrame(function(){ dst.hasClass(dropover) && helper.addClass(status); });
  				},
  				out : function(e, ui) {
  					var dst    = $(this),
  						helper = ui.helper;
  					e.stopPropagation();
  					if (insideNavbar(e.clientX)) {
  						helper.removeClass('elfinder-drag-helper-move elfinder-drag-helper-plus');
  					}
  					helper.data('dropover', Math.max(helper.data('dropover') - 1, 0));
  					dst.data('expandTimer') && clearTimeout(dst.data('expandTimer'));
  					dst.removeData('dropover')
  					   .removeClass(hover+' '+dropover);
  				},
  				deactivate : function() {
  					$(this).removeData('dropover')
  					       .removeClass(hover+' '+dropover);
  				},
  				drop : function(e, ui) {
  					insideNavbar(e.clientX) && drop.call(this, e, ui);
  				}
  			}),
  			
  			spinner = $(fm.res('tpl', 'navspinner')),
  			
  			/**
  			 * Directory html template
  			 *
  			 * @type String
  			 */
  			tpl = fm.res('tpl', 'navdir'),
  			
  			/**
  			 * Permissions marker html template
  			 *
  			 * @type String
  			 */
  			ptpl = fm.res('tpl', 'perms'),
  			
  			/**
  			 * Lock marker html template
  			 *
  			 * @type String
  			 */
  			ltpl = fm.res('tpl', 'lock'),
  			
  			/**
  			 * Symlink marker html template
  			 *
  			 * @type String
  			 */
  			stpl = fm.res('tpl', 'symlink'),
  			
  			/**
  			 * Directory hashes that has more pages
  			 * 
  			 * @type Object
  			 */
  			hasMoreDirs = {},
  			
  			/**
  			 * Html template replacement methods
  			 *
  			 * @type Object
  			 */
  			replace = {
  				id          : function(dir) { return fm.navHash2Id(dir.hash); },
  				name        : function(dir) { return fm.escape(dir.i18 || dir.name); },
  				cssclass    : function(dir) {
  					var cname = (dir.phash && ! dir.isroot ? '' : root)+' '+navdir+' '+fm.perms2class(dir);
  					dir.dirs && !dir.link && (cname += ' ' + collapsed) && dir.dirs == -1 && (cname += ' ' + chksubdir);
  					opts.getClass && (cname += ' ' + opts.getClass(dir));
  					dir.csscls && (cname += ' ' + fm.escape(dir.csscls));
  					return cname;
  				},
  				title       : function(dir) { return opts.attrTitle? (' title="' + fm.escape(fm.path(dir.hash, true) || dir.i18 || dir.name) + '"') : ''; },
  				root        : function(dir) {
  					var cls = '';
  					if (!dir.phash || dir.isroot) {
  						cls += ' '+wrapperRoot;
  						if (!dir.disabled || dir.disabled.length < 1) {
  							cls += ' '+pastable+' '+uploadable;
  						} else {
  							if ($.inArray('paste', dir.disabled) === -1) {
  								cls += ' '+pastable;
  							}
  							if ($.inArray('upload', dir.disabled) === -1) {
  								cls += ' '+uploadable;
  							}
  						}
  						return cls;
  					} else {
  						return '';
  					}
  				},
  				permissions : function(dir) { return !dir.read || !dir.write ? ptpl : ''; },
  				symlink     : function(dir) { return dir.alias ? stpl : ''; },
  				style       : function(dir) { return dir.icon ? fm.getIconStyle(dir) : ''; }
  			},
  			
  			/**
  			 * Return html for given dir
  			 *
  			 * @param  Object  directory
  			 * @return String
  			 */
  			itemhtml = function(dir) {
  				return tpl.replace(/(?:\{([a-z]+)\})/ig, function(m, key) {
  					var res = replace[key] ? replace[key](dir) : (dir[key] || '');
  					if (key === 'id' && dir.dirs == -1) {
  						subdirsQue[res] = res;
  					}
  					return res;
  				});
  			},
  			
  			/**
  			 * Return only dirs from files list
  			 *
  			 * @param  Array   files list
  			 * @param  Boolean do check exists
  			 * @return Array
  			 */
  			filter = function(files, checkExists) {
  				return $.map(files || [], function(f) {
  					return (f.mime === 'directory' && (!checkExists || fm.navHash2Elm(f.hash).length)) ? f : null;
  				});
  			},
  			
  			/**
  			 * Find parent subtree for required directory
  			 *
  			 * @param  String  dir hash
  			 * @return jQuery
  			 */
  			findSubtree = function(hash) {
  				return hash ? fm.navHash2Elm(hash).next('.'+subtree) : tree;
  			},
  			
  			/**
  			 * Find directory (wrapper) in required node
  			 * before which we can insert new directory
  			 *
  			 * @param  jQuery  parent directory
  			 * @param  Object  new directory
  			 * @return jQuery
  			 */
  			findSibling = function(subtree, dir) {
  				var node = subtree.children(':first'),
  					info;
  
  				while (node.length) {
  					info = fm.file(fm.navId2Hash(node.children('[id]').attr('id')));
  					
  					if ((info = fm.file(fm.navId2Hash(node.children('[id]').attr('id')))) 
  					&& compare(dir, info) < 0) {
  						return node;
  					}
  					node = node.next();
  				}
  				return subtree.children('button.elfinder-navbar-pager-next');
  			},
  			
  			/**
  			 * Add new dirs in tree
  			 *
  			 * @param  Array  dirs list
  			 * @return void
  			 */
  			updateTree = function(dirs) {
  				var length  = dirs.length,
  					orphans = [],
  					i = length,
  					tgts = $(),
  					done = {},
  					cwd = fm.cwd(),
  					append = function(parent, dirs, start, direction) {
  						var hashes = {},
  							curStart = 0,
  							max = fm.newAPI? Math.min(10000, Math.max(10, opts.subTreeMax)) : 10000,
  							setHashes = function() {
  								hashes = {};
  								$.each(dirs, function(i, d) {
  									hashes[d.hash] = i;
  								});
  							},
  							change = function(mode) {
  								if (mode === 'prepare') {
  									$.each(dirs, function(i, d) {
  										d.node && parent.append(d.node.hide());
  									});
  								} else if (mode === 'done') {
  									$.each(dirs, function(i, d) {
  										d.node && d.node.detach().show();
  									});
  								}
  							},
  							update = function(e, data) {
  								var i, changed;
  								e.stopPropagation();
  								
  								if (data.select) {
  									render(getStart(data.select));
  									return;
  								}
  								
  								if (data.change) {
  									change(data.change);
  									return;
  								}
  								
  								if (data.removed && data.removed.length) {
  									dirs = $.grep(dirs, function(d) {
  										if (data.removed.indexOf(d.hash) === -1) {
  											return true;
  										} else {
  											!changed && (changed = true);
  											return false;
  										}
  									});
  								}
  								
  								if (data.added && data.added.length) {
  									dirs = dirs.concat($.grep(data.added, function(d) {
  										if (hashes[d.hash] === void(0)) {
  											!changed && (changed = true);
  											return true;
  										} else {
  											return false;
  										}
  									}));
  								}
  								if (changed) {
  									dirs.sort(compare);
  									setHashes();
  									render(curStart);
  								}
  							},
  							getStart = function(target) {
  								if (hashes[target] !== void(0)) {
  									return Math.floor(hashes[target] / max) * max;
  								}
  								return void(0);
  							},
  							target = fm.navId2Hash(parent.prev('[id]').attr('id')),
  							render = function(start, direction) {
  								var html = [],
  									nodes = {},
  									total, page, s, parts, prev, next, prevBtn, nextBtn;
  								delete hasMoreDirs[target];
  								curStart = start;
  								parent.off('update.'+fm.namespace, update);
  								if (dirs.length > max) {
  									parent.on('update.'+fm.namespace, update);
  									if (start === void(0)) {
  										s = 0;
  										setHashes();
  										start = getStart(cwd.hash);
  										if (start === void(0)) {
  											start = 0;
  										}
  									}
  									parts = dirs.slice(start, start + max);
  									hasMoreDirs[target] = parent;
  									prev = start? Math.max(-1, start - max) : -1;
  									next = (start + max >= dirs.length)? 0 : start + max;
  									total = Math.ceil(dirs.length/max);
  									page = Math.ceil(start/max);
  								}
  								$.each(parts || dirs, function(i, d) {
  									html.push(itemhtml(d));
  									if (d.node) {
  										nodes[d.hash] = d.node;
  									}
  								});
  								if (prev > -1) {
  									prevBtn = $('<button class="elfinder-navbar-pager elfinder-navbar-pager-prev"></button>')
  										.text(fm.i18n('btnPrevious', page, total))
  										.button({
  											icons: {
  												primary: "ui-icon-caret-1-n"
  											}
  										})
  										.on('click', function(e) {
  											e.preventDefault();
  											e.stopPropagation();
  											render(prev, 'up');
  										});
  								} else {
  									prevBtn = $();
  								}
  								if (next) {
  									nextBtn = $('<button class="elfinder-navbar-pager elfinder-navbar-pager-next"></button>')
  										.text(fm.i18n('btnNext', page + 2, total))
  										.button({
  											icons: {
  												primary: "ui-icon-caret-1-s"
  											}
  										})
  										.on('click', function(e) {
  											e.preventDefault();
  											e.stopPropagation();
  											render(next, 'down');
  										});
  								} else {
  									nextBtn = $();
  								}
  								detach();
  								parent.empty()[parts? 'addClass' : 'removeClass']('elfinder-navbar-hasmore').append(prevBtn, html.join(''), nextBtn);
  								$.each(nodes, function(h, n) {
  									fm.navHash2Elm(h).parent().replaceWith(n);
  								});
  								if (direction) {
  									autoScroll(fm.navHash2Id(parts[direction === 'up'? parts.length - 1 : 0].hash));
  								}
  								! mobile && fm.lazy(function() { updateDroppable(null, parent); });
  							},
  							detach = function() {
  								$.each(parent.children('.elfinder-navbar-wrapper'), function(i, elm) {
  									var n = $(elm),
  										ch = n.children('[id]:first'),
  										h, c;
  									if (ch.hasClass(loaded)) {
  										h = fm.navId2Hash(ch.attr('id'));
  										if (h && (c = hashes[h]) !== void(0)) {
  											dirs[c].node = n.detach();
  										}
  									}
  								});
  							};
  						
  						render();
  					},
  					dir, html, parent, sibling, init, atonce = {}, updates = [], base, node,
  					lastKey, lastNodes = {};
  				
  				while (i--) {
  					dir = dirs[i];
  
  					if (done[dir.hash] || fm.navHash2Elm(dir.hash).length) {
  						continue;
  					}
  					done[dir.hash] = true;
  					
  					if ((parent = findSubtree(dir.phash)).length) {
  						lastKey = dir.phash || 'treeroot';
  						if (typeof lastNodes[lastKey] === 'undefined') {
  							lastNodes[lastKey] = parent.children(':last');
  						}
  						init = !lastNodes[lastKey].length;
  						if (dir.phash && (init || parent.hasClass('elfinder-navbar-hasmore') || (sibling = findSibling(parent, dir)).length)) {
  							if (init) {
  								if (!atonce[dir.phash]) {
  									atonce[dir.phash] = [];
  								}
  								atonce[dir.phash].push(dir);
  							} else {
  								if (sibling) {
  									node = itemhtml(dir);
  									sibling.before(node);
  									! mobile && (tgts = tgts.add(node));
  								} else {
  									updates.push(dir);
  								}
  							}
  						} else {
  							node = itemhtml(dir);
  							if (init) {
  								parent.prepend(node);
  							} else {
  								lastNodes[lastKey].after(node);
  							}
  							if (!dir.phash || dir.isroot) {
  								base = fm.navHash2Elm(dir.hash).parent();
  							}
  							! mobile && updateDroppable(null, base);
  						}
  					} else {
  						orphans.push(dir);
  					}
  				}
  
  				// When init, html append at once
  				if (Object.keys(atonce).length){
  					$.each(atonce, function(p, dirs){
  						var parent = findSubtree(p),
  						    html   = [];
  						dirs.sort(compare);
  						append(parent, dirs);
  					});
  				}
  				
  				if (updates.length) {
  					parent.trigger('update.' + fm.namespace, { added : updates });
  				}
  				
  				if (orphans.length && orphans.length < length) {
  					updateTree(orphans);
  					return;
  				} 
  				
  				! mobile && tgts.length && fm.lazy(function() { updateDroppable(tgts); });
  				
  			},
  			
  			/**
  			 * sort function by dir.name
  			 * 
  			 */
  			compare = function(dir1, dir2) {
  				if (! fm.sortAlsoTreeview) {
  					return fm.sortRules.name(dir1, dir2);
  				} else {
  					var asc   = fm.sortOrder == 'asc',
  						type  = fm.sortType,
  						rules = fm.sortRules,
  						res;
  					
  					res = asc? rules[fm.sortType](dir1, dir2) : rules[fm.sortType](dir2, dir1);
  					
  					return type !== 'name' && res === 0
  						? res = asc ? rules.name(dir1, dir2) : rules.name(dir2, dir1)
  						: res;
  				}
  			},
  
  			/**
  			 * Timer ID of autoScroll
  			 * 
  			 * @type  Integer
  			 */
  			autoScrTm,
  
  			/**
  			 * Auto scroll to cwd
  			 *
  			 * @return Object  jQuery Deferred
  			 */
  			autoScroll = function(target) {
  				var dfrd = $.Deferred(),
  					current, parent, top, treeH, bottom, tgtTop;
  				autoScrTm && clearTimeout(autoScrTm);
  				autoScrTm = setTimeout(function() {
  					current = $(document.getElementById((target || fm.navHash2Id(fm.cwd().hash))));
  					if (current.length) {
  						// expand parents directory
  						(openCwd? current : current.parent()).parents('.elfinder-navbar-wrapper').children('.'+loaded).addClass(expanded).next('.'+subtree).show();
  						
  						parent = tree.parent().stop(false, true);
  						top = parent.offset().top;
  						treeH = parent.height();
  						bottom = top + treeH - current.outerHeight();
  						tgtTop = current.offset().top;
  						
  						if (tgtTop < top || tgtTop > bottom) {
  							parent.animate({
  								scrollTop : parent.scrollTop() + tgtTop - top - treeH / 3
  							}, {
  								duration : opts.durations.autoScroll,
  								complete : function() {	dfrd.resolve(); }
  							});
  						} else {
  							dfrd.resolve();
  						}
  					} else {
  						dfrd.reject();
  					}
  				}, 100);
  				return dfrd;
  			},
  			/**
  			 * Get hashes array of items of the bottom of the leaf root back from the target
  			 * 
  			 * @param Object elFinder item(directory) object
  			 * @return Array hashes
  			 */
  			getEnds = function(d) {
  				var cur = d || fm.cwd(),
  					res = cur.hash? [ cur.hash ] : [],
  					phash, root, dir;
  				
  				root = fm.root(cur.hash);
  				dir = fm.file(root);
  				while (dir && (phash = dir.phash)) {
  					res.unshift(phash);
  					root = fm.root(phash);
  					dir = fm.file(root);
  					if (fm.navHash2Elm(dir.hash).hasClass(loaded)) {
  						break;
  					}
  				}
  				
  				return res;
  			},
  			
  			/**
  			 * Select pages back in order to display the target
  			 * 
  			 * @param Object elFinder item(directory) object
  			 * @return Object jQuery node object of target node
  			 */
  			selectPages = function(current) {
  				var cur = current || fm.cwd(),
  					curHash = cur.hash,
  					node = fm.navHash2Elm(curHash);
  			
  				if (!node.length) {
  					while(cur && cur.phash) {
  						if (hasMoreDirs[cur.phash] && !fm.navHash2Elm(cur.hash).length) {
  							hasMoreDirs[cur.phash].trigger('update.'+fm.namespace, { select : cur.hash });
  						}
  						cur = fm.file(cur.phash);
  					}
  					node = fm.navHash2Elm(curHash);
  				}
  				
  				return node;
  			},
  			
  			/**
  			 * Flag indicating that synchronization is currently in progress
  			 * 
  			 * @type Boolean
  			 */
  			syncing,
  
  			/**
  			 * Mark current directory as active
  			 * If current directory is not in tree - load it and its parents
  			 *
  			 * @param Array directory objects of cwd
  			 * @param Boolean do auto scroll
  			 * @return Object jQuery Deferred
  			 */
  			sync = function(cwdDirs, aScr) {
  				var cwd     = fm.cwd(),
  					cwdhash = cwd.hash,
  					autoScr = aScr === void(0)? syncTree : aScr,
  					loadParents = function(dir) {
  						var dfd  = $.Deferred(),
  							reqs = [],
  							ends = getEnds(dir),
  							makeReq = function(cmd, h, until) {
  								var data = {
  										cmd    : cmd,
  										target : h
  									};
  								if (until) {
  									data.until = until;
  								}
  								return fm.request({
  									data : data,
  									preventFail : true
  								});
  							},
  							baseHash, baseId;
  						
  						reqs = $.map(ends, function(h) {
  							var d = fm.file(h),
  								isRoot = d? fm.isRoot(d) : false,
  								node = fm.navHash2Elm(h),
  								getPhash = function(h, dep) {
  									var d, ph,
  										depth = dep || 1;
  									ph = (d = fm.file(h))? d.phash : false;
  									if (ph && depth > 1) {
  										return getPhash(ph, --depth);
  									}
  									return ph;
  								},
  								until,
  								closest = (function() {
  									var phash = getPhash(h);
  									until = phash;
  									while (phash) {
  										if (fm.navHash2Elm(phash).hasClass(loaded)) {
  											break;
  										}
  										until = phash;
  										phash = getPhash(phash);
  									}
  									if (!phash) {
  										until = void(0);
  										phash = fm.root(h);
  									}
  									return phash;
  								})(),
  								cmd;
  							
  							if (!node.hasClass(loaded) && (isRoot || !d || !fm.navHash2Elm(d.phash).hasClass(loaded))) {
  								if (isRoot || closest === getPhash(h) || closest === getPhash(h, 2)) {
  									until = void(0);
  									cmd = 'tree';
  									if (!isRoot) {
  										h = getPhash(h);
  									}
  								} else {
  									cmd = 'parents';
  								}
  								if (!baseHash) {
  									baseHash = (cmd === 'tree')? h : closest;
  								}
  								return makeReq(cmd, h, until);
  							}
  							return null;
  						});
  						
  						if (reqs.length) {
  							selectPages(fm.file(baseHash));
  							baseId = fm.navHash2Id(baseHash);
  							autoScr && autoScroll(baseId);
  							baseNode = $('#'+baseId);
  							spinner = $(fm.res('tpl', 'navspinner')).insertBefore(baseNode.children('.'+arrow));
  							baseNode.removeClass(collapsed);
  							
  							$.when.apply($, reqs)
  							.done(function() {
  								var res = {},data, treeDirs, dirs, argLen, i;
  								argLen = arguments.length;
  								if (argLen > 0) {
  									for (i = 0; i < argLen; i++) {
  										data = arguments[i].tree || [];
  										res[ends[i]] = Object.assign([], filter(data));
  									}
  								}
  								dfd.resolve(res);
  							})
  							.fail(function() {
  								dfd.reject();
  							});
  							
  							return dfd;
  						} else {
  							return dfd.resolve();
  						}
  					},
  					done= function(res, dfrd) {
  						var open = function() {
  								if (openRoot && baseNode) {
  									findSubtree(baseNode.hash).show().prev(selNavdir).addClass(expanded);
  									openRoot = false;
  								}
  								if (autoScr) {
  									autoScroll().done(checkSubdirs);
  								} else {
  									checkSubdirs();
  								}
  							},
  							current;
  						
  						if (res) {
  							$.each(res, function(endHash, dirs) {
  								dirs && updateTree(dirs);
  								selectPages(fm.file(endHash));
  								dirs && updateArrows(dirs, loaded);
  							});
  						}
  						
  						if (cwdDirs) {
  							(fm.api < 2.1) && cwdDirs.push(cwd);
  							updateTree(cwdDirs);
  						}
  						
  						// set current node
  						current = selectPages();
  						
  						if (!current.hasClass(active)) {
  							tree.find(selNavdir+'.'+active).removeClass(active);
  							current.addClass(active);
  						}
  						
  						// mark as loaded to cwd parents
  						current.parents('.elfinder-navbar-wrapper').children('.'+navdir).addClass(loaded);
  						
  						if (res) {
  							fm.lazy(open).done(function() {
  								dfrd.resolve();
  							});
  						} else {
  							open();
  							dfrd.resolve();
  						}
  					},
  					rmSpinner = function(fail) {
  						if (baseNode) {
  							spinner.remove();
  							baseNode.addClass(collapsed + (fail? '' : (' ' + loaded)));
  						}
  					},
  					dfrd = $.Deferred(),
  					baseNode, spinner;
  				
  				if (!fm.navHash2Elm(cwdhash).length) {
  					syncing = true;
  					loadParents()
  					.done(function(res) {
  						done(res, dfrd);
  						rmSpinner();
  					})
  					.fail(function() { 
  						rmSpinner(true);
  						dfrd.reject();
  					})
  					.always(function() {
  						syncing = false;
  					});
  				} else {
  					done(void(0), dfrd);
  				}
  				
  				// trigger 'treesync' with my $.Deferred
  				fm.trigger('treesync', dfrd);
  
  				return dfrd;
  			},
  			
  			/**
  			 * Make writable and not root dirs droppable
  			 *
  			 * @return void
  			 */
  			updateDroppable = function(target, node) {
  				var limit = 100,
  					next;
  				
  				if (!target) {
  					if (!node || node.closest('div.'+wrapperRoot).hasClass(uploadable)) {
  						(node || tree.find('div.'+uploadable)).find(selNavdir+':not(.elfinder-ro,.elfinder-na)').addClass('native-droppable');
  					}
  					if (!node || node.closest('div.'+wrapperRoot).hasClass(pastable)) {
  						target = (node || tree.find('div.'+pastable)).find(selNavdir+':not(.'+droppable+')');
  					} else {
  						target = $();
  					}
  					if (node) {
  						// check leaf roots
  						node.children('div.'+wrapperRoot).each(function() {
  							updateDroppable(null, $(this));
  						});
  					}
  				}
  				
  				// make droppable on async
  				if (target.length) {
  					fm.asyncJob(function(elm) {
  						$(elm).droppable(droppableopts);
  					}, $.makeArray(target), {
  						interval : 20,
  						numPerOnce : 100
  					});
  				}
  			},
  			
  			/**
  			 * Check required folders for subfolders and update arrow classes
  			 *
  			 * @param  Array  folders to check
  			 * @param  String css class 
  			 * @return void
  			 */
  			updateArrows = function(dirs, cls) {
  				var sel = cls == loaded
  						? '.'+collapsed+':not(.'+loaded+')'
  						: ':not(.'+collapsed+')';
  				
  				$.each(dirs, function(i, dir) {
  					fm.navHash2Elm(dir.phash).filter(sel)
  						.filter(function() { return $.grep($(this).next('.'+subtree).children(), function(n) {
  							return ($(n).children().hasClass(root))? false : true;
  						}).length > 0; })
  						.addClass(cls);
  				});
  			},
  			
  			
  			
  			/**
  			 * Navigation tree
  			 *
  			 * @type JQuery
  			 */
  			tree = $(this).addClass(treeclass)
  				// make dirs draggable and toggle hover class
  				.on('mouseenter mouseleave', selNavdir, function(e) {
  					var enter = (e.type === 'mouseenter');
  					if (enter && scrolling) { return; }
  					var link  = $(this),
  						hash, dir; 
  					
  					if (!link.hasClass(dropover+' '+disabled)) {
  						if (!mobile && enter && !link.data('dragRegisted') && !link.hasClass(root+' '+draggable+' elfinder-na elfinder-wo')) {
  							link.data('dragRegisted', true);
  							if (fm.isCommandEnabled('copy', (hash = fm.navId2Hash(link.attr('id'))))) {
  								link.draggable(fm.draggable);
  							}
  						}
  						link.toggleClass(hover, enter);
  					}
  					// update title attr if necessary
  					if (enter && opts.attrTitle) {
  						dir = fm.file(hash || fm.navId2Hash(link.attr('id')));
  						if (!dir.isroot && link.attr('title') === (dir.i18 || dir.name)) {
  							link.attr('title', fm.path(hash, true));
  						}
  					}
  				})
  				// native drag enter
  				.on('dragenter', selNavdir, function(e) {
  					if (e.originalEvent.dataTransfer) {
  						var dst = $(this);
  						dst.addClass(hover);
  						if (dst.is('.'+collapsed+':not(.'+expanded+')')) {
  							dst.data('expandTimer', setTimeout(function() {
  								dst.is('.'+collapsed+'.'+hover) && dst.children('.'+arrow).trigger('click');
  							}, 500));
  						}
  					}
  				})
  				// native drag leave
  				.on('dragleave', selNavdir, function(e) {
  					if (e.originalEvent.dataTransfer) {
  						var dst = $(this);
  						dst.data('expandTimer') && clearTimeout(dst.data('expandTimer'));
  						dst.removeClass(hover);
  					}
  				})
  				// open dir or open subfolders in tree
  				.on('click', selNavdir, function(e) {
  					var link = $(this),
  						hash = fm.navId2Hash(link.attr('id')),
  						file = fm.file(hash);
  					
  					if (link.data('longtap')) {
  						link.removeData('longtap');
  						e.stopPropagation();
  						return;
  					}
  					
  					if (!link.hasClass(active)) {
  						tree.find(selNavdir+'.'+active).removeClass(active);
  						link.addClass(active);
  					}
  					if (hash != fm.cwd().hash && !link.hasClass(disabled)) {
  						fm.exec('open', hash).done(function() {
  							fm.one('opendone', function() {
  								fm.select({selected: [hash], origin: 'navbar'});
  							});
  						});
  					} else {
  						if (link.hasClass(collapsed)) {
  							link.children('.'+arrow).trigger('click');
  						}
  						fm.select({selected: [hash], origin: 'navbar'});
  					}
  				})
  				// for touch device
  				.on('touchstart', selNavdir, function(e) {
  					if (e.originalEvent.touches.length > 1) {
  						return;
  					}
  					var evt = e.originalEvent,
  						p;
  					
  					if (e.target.nodeName === 'INPUT') {
  						e.stopPropagation();
  						return;
  					}
  					
  					p = $(this).addClass(hover)
  					.removeData('longtap')
  					.data('tmlongtap', setTimeout(function(e){
  						// long tap
  						p.data('longtap', true);
  						fm.trigger('contextmenu', {
  							'type'    : 'navbar',
  							'targets' : [fm.navId2Hash(p.attr('id'))],
  							'x'       : evt.touches[0].pageX,
  							'y'       : evt.touches[0].pageY
  						});
  					}, 500));
  				})
  				.on('touchmove touchend', selNavdir, function(e) {
  					if (e.target.nodeName === 'INPUT') {
  						e.stopPropagation();
  						return;
  					}
  					clearTimeout($(this).data('tmlongtap'));
  					$(this).removeData('tmlongtap');
  					if (e.type == 'touchmove') {
  						$(this).removeClass(hover);
  					}
  				})
  				// toggle subfolders in tree
  				.on('click', selNavdir+'.'+collapsed+' .'+arrow, function(e) {
  					var arrow = $(this),
  						link  = arrow.parent(selNavdir),
  						stree = link.next('.'+subtree),
  						dfrd  = $.Deferred(),
  						slideTH = 30, cnt;
  
  					e.stopPropagation();
  
  					if (link.hasClass(loaded)) {
  						link.toggleClass(expanded);
  						fm.lazy(function() {
  							cnt = link.hasClass(expanded)? stree.children().length + stree.find('div.elfinder-navbar-subtree[style*=block]').children().length : stree.find('div:visible').length;
  							if (cnt > slideTH) {
  								stree.toggle();
  								fm.draggingUiHelper && fm.draggingUiHelper.data('refreshPositions', 1);
  								checkSubdirs();
  							} else {
  								stree.stop(true, true)[link.hasClass(expanded)? 'slideDown' : 'slideUp'](opts.durations.slideUpDown, function(){
  									fm.draggingUiHelper && fm.draggingUiHelper.data('refreshPositions', 1);
  									checkSubdirs();
  								});
  							}
  						}).always(function() {
  							dfrd.resolve();
  						});
  					} else {
  						spinner.insertBefore(arrow);
  						link.removeClass(collapsed);
  
  						fm.request({cmd : 'tree', target : fm.navId2Hash(link.attr('id'))})
  							.done(function(data) { 
  								updateTree(Object.assign([], filter(data.tree))); 
  								
  								if (stree.children().length) {
  									link.addClass(collapsed+' '+expanded);
  									if (stree.children().length > slideTH) {
  										stree.show();
  										fm.draggingUiHelper && fm.draggingUiHelper.data('refreshPositions', 1);
  										checkSubdirs();
  									} else {
  										stree.stop(true, true).slideDown(opts.durations.slideUpDown, function(){
  											fm.draggingUiHelper && fm.draggingUiHelper.data('refreshPositions', 1);
  											checkSubdirs();
  										});
  									}
  								} 
  							})
  							.always(function(data) {
  								spinner.remove();
  								link.addClass(loaded);
  								fm.one('treedone', function() {
  									dfrd.resolve();
  								});
  							});
  					}
  					arrow.data('dfrd', dfrd);
  				})
  				.on('contextmenu', selNavdir, function(e) {
  					var self = $(this);
  					
  					// now dirname editing
  					if (self.find('input:text').length) {
  						e.stopPropagation();
  						return;
  					}
  					
  					e.preventDefault();
  
  					if (!self.data('tmlongtap')) {
  						fm.trigger('contextmenu', {
  							'type'    : 'navbar',
  							'targets' : [fm.navId2Hash($(this).attr('id'))],
  							'x'       : e.pageX,
  							'y'       : e.pageY
  						});
  					}
  					self.addClass('ui-state-hover');
  					
  					fm.getUI('contextmenu').children().on('mouseenter', function() {
  						self.addClass('ui-state-hover');
  					});
  					
  					fm.bind('closecontextmenu', function() {
  						self.removeClass('ui-state-hover');
  					});
  				})
  				.on('scrolltoview', selNavdir, function(e, data) {
  					var self = $(this);
  					autoScroll(self.attr('id')).done(function() {
  						if (!data || data.blink === 'undefined' || data.blink) {
  							fm.resources.blink(self, 'lookme');
  						}
  					});
  				})
  				// prepend fake dir
  				.on('create.'+fm.namespace, function(e, item) {
  					var pdir = findSubtree(item.phash),
  						lock = item.move || false,
  						dir  = $(itemhtml(item)).addClass('elfinder-navbar-wrapper-tmp'),
  						selected = fm.selected();
  						
  					lock && selected.length && fm.trigger('lockfiles', {files: selected});
  					pdir.prepend(dir);
  				}),
  			scrolling = false,
  			navbarScrTm,
  			// move tree into navbar
  			navbar = fm.getUI('navbar').append(tree).show().on('scroll', function() {
  				scrolling = true;
  				navbarScrTm && cancelAnimationFrame(navbarScrTm);
  				navbarScrTm = requestAnimationFrame(function() {
  					scrolling = false;
  					checkSubdirs();
  				});
  			}),
  			
  			prevSortTreeview = fm.sortAlsoTreeview;
  			
  		fm.open(function(e) {
  			var data = e.data,
  				dirs = filter(data.files),
  				contextmenu = fm.getUI('contextmenu');
  
  			data.init && tree.empty();
  
  			if (fm.UA.iOS) {
  				navbar.removeClass('overflow-scrolling-touch').addClass('overflow-scrolling-touch');
  			}
  
  			if (dirs.length) {
  				fm.lazy(function() {
  					if (!contextmenu.data('cmdMaps')) {
  						contextmenu.data('cmdMaps', {});
  					}
  					updateTree(dirs);
  					updateArrows(dirs, loaded);
  					sync(dirs);
  				});
  			} else {
  				sync();
  			}
  		})
  		// add new dirs
  		.add(function(e) {
  			var dirs = filter(e.data.added);
  
  			if (dirs.length) {
  				updateTree(dirs);
  				updateArrows(dirs, collapsed);
  			}
  		})
  		// update changed dirs
  		.change(function(e) {
  			// do ot perfome while syncing
  			if (syncing) {
  				return;
  			}
  
  			var dirs = filter(e.data.changed, true),
  				length = dirs.length,
  				l    = length,
  				tgts = $(),
  				changed = {},
  				dir, phash, node, tmp, realParent, reqParent, realSibling, reqSibling, isExpanded, isLoaded, parent, subdirs;
  			
  			$.each(hasMoreDirs, function(h, node) {
  				node.trigger('update.'+fm.namespace, { change: 'prepare' });
  			});
  			
  			while (l--) {
  				dir = dirs[l];
  				phash = dir.phash;
  				if ((node = fm.navHash2Elm(dir.hash)).length) {
  					parent = node.parent();
  					if (phash) {
  						realParent  = node.closest('.'+subtree);
  						reqParent   = findSubtree(phash);
  						realSibling = node.parent().next();
  						reqSibling  = findSibling(reqParent, dir);
  						
  						if (!reqParent.length) {
  							continue;
  						}
  						
  						if (reqParent[0] !== realParent[0] || realSibling.get(0) !== reqSibling.get(0)) {
  							reqSibling.length ? reqSibling.before(parent) : reqParent.append(parent);
  						}
  					}
  					isExpanded = node.hasClass(expanded);
  					isLoaded   = node.hasClass(loaded);
  					tmp        = $(itemhtml(dir));
  					node.replaceWith(tmp.children(selNavdir));
  					! mobile && updateDroppable(null, parent);
  					
  					if (dir.dirs
  					&& (isExpanded || isLoaded) 
  					&& (node = fm.navHash2Elm(dir.hash))
  					&& node.next('.'+subtree).children().length) {
  						isExpanded && node.addClass(expanded);
  						isLoaded && node.addClass(loaded);
  					}
  					
  					subdirs |= dir.dirs == -1;
  				}
  			}
  			
  			// to check subdirs
  			if (subdirs) {
  				checkSubdirs();
  			}
  			
  			$.each(hasMoreDirs, function(h, node) {
  				node.trigger('update.'+fm.namespace, { change: 'done' });
  			});
  			
  			length && sync(void(0), false);
  		})
  		// remove dirs
  		.remove(function(e) {
  			var dirs = e.data.removed,
  				l    = dirs.length,
  				node, stree, removed;
  			
  			$.each(hasMoreDirs, function(h, node) {
  				node.trigger('update.'+fm.namespace, { removed : dirs });
  				node.trigger('update.'+fm.namespace, { change: 'prepare' });
  			});
  
  			while (l--) {
  				if ((node = fm.navHash2Elm(dirs[l])).length) {
  					removed = true;
  					stree = node.closest('.'+subtree);
  					node.parent().detach();
  					if (!stree.children().length) {
  						stree.hide().prev(selNavdir).removeClass(collapsed+' '+expanded+' '+loaded);
  					}
  				}
  			}
  			
  			removed && fm.getUI('navbar').children('.ui-resizable-handle').trigger('resize');
  			
  			$.each(hasMoreDirs, function(h, node) {
  				node.trigger('update.'+fm.namespace, { change: 'done' });
  			});
  		})
  		// lock/unlock dirs while moving
  		.bind('lockfiles unlockfiles', function(e) {
  			var lock = e.type == 'lockfiles',
  				helperLocked = e.data.helper? e.data.helper.data('locked') : false,
  				act  = (lock && !helperLocked) ? 'disable' : 'enable',
  				dirs = $.grep(e.data.files||[], function(h) {  
  					var dir = fm.file(h);
  					return dir && dir.mime == 'directory' ? true : false;
  				});
  				
  			$.each(dirs, function(i, hash) {
  				var dir = fm.navHash2Elm(hash);
  				
  				if (dir.length && !helperLocked) {
  					dir.hasClass(draggable) && dir.draggable(act);
  					dir.hasClass(droppable) && dir.droppable(act);
  					dir[lock ? 'addClass' : 'removeClass'](disabled);
  				}
  			});
  		})
  		.bind('sortchange', function() {
  			if (fm.sortAlsoTreeview || prevSortTreeview !== fm.sortAlsoTreeview) {
  				var dirs,
  					ends = [],
  					endsMap = {},
  					endsVid = {},
  					topVid = '',
  					single = false,
  					current;
  				
  				fm.lazy(function() {
  					dirs = filter(fm.files());
  					prevSortTreeview = fm.sortAlsoTreeview;
  					
  					tree.empty();
  					
  					// append volume roots at first
  					updateTree($.map(fm.roots, function(h) {
  						var dir = fm.file(h);
  						return dir && !dir.phash? dir : null;
  					}));
  					
  					if (!Object.keys(hasMoreDirs).length) {
  						updateTree(dirs);
  						current = selectPages();
  						updateArrows(dirs, loaded);
  					} else {
  						ends = getEnds();
  						if (ends.length > 1) {
  							$.each(ends, function(i, end) {
  								var vid = fm.file(fm.root(end)).volumeid; 
  								if (i === 0) {
  									topVid = vid;
  								}
  								endsVid[vid] = end;
  								endsMap[end] = [];
  							});
  							$.each(dirs, function(i, d) {
  								if (!d.volumeid) {
  									single = true;
  									return false;
  								}
  								endsMap[endsVid[d.volumeid] || endsVid[topVid]].push(d);
  							});
  						} else {
  							single = true;
  						}
  						if (single) {
  							$.each(ends, function(i, endHash) {
  								updateTree(dirs);
  								current = selectPages(fm.file(endHash));
  								updateArrows(dirs, loaded);
  							});
  						} else {
  							$.each(endsMap, function(endHash, dirs) {
  								updateTree(dirs);
  								current = selectPages(fm.file(endHash));
  								updateArrows(dirs, loaded);
  							});
  						}
  					}
  					
  					sync();
  				}, 100);
  			}
  		});
  
  	});
  	
  	return this;
  };
  