mirror of
https://github.com/Studio-42/elFinder
synced 2026-06-08 12:37:09 +00:00
550 lines
13 KiB
JavaScript
550 lines
13 KiB
JavaScript
"use strict";
|
|
/**
|
|
* @class elFinder folders tree
|
|
*
|
|
* @author Dmitry (dio) Levashov
|
|
**/
|
|
$.fn.elfindertree = function(fm, opts) {
|
|
var treeclass = fm.res('class', 'tree');
|
|
|
|
this.not('.'+treeclass).each(function() {
|
|
|
|
var c = 'class',
|
|
|
|
/**
|
|
* Root directory class name
|
|
*
|
|
* @type String
|
|
*/
|
|
root = fm.res(c, 'treeroot'),
|
|
|
|
/**
|
|
* Open root dir if not opened yet
|
|
*
|
|
* @type Boolean
|
|
*/
|
|
openRoot = opts.openRootOnLoad,
|
|
|
|
/**
|
|
* Subtree class name
|
|
*
|
|
* @type String
|
|
*/
|
|
subtree = fm.res(c, 'navsubtree'),
|
|
|
|
/**
|
|
* Directory class name
|
|
*
|
|
* @type String
|
|
*/
|
|
navdir = fm.res(c, 'treedir'),
|
|
|
|
/**
|
|
* 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',
|
|
|
|
/**
|
|
* 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'),
|
|
|
|
insideNavbar = function(x) {
|
|
var left = navbar.offset().left;
|
|
|
|
return left <= x && x <= left + navbar.width();
|
|
},
|
|
|
|
drop = fm.droppable.drop,
|
|
|
|
/**
|
|
* Droppable options
|
|
*
|
|
* @type Object
|
|
*/
|
|
droppableopts = $.extend(true, {}, fm.droppable, {
|
|
// show subfolders on dropover
|
|
over : function(e) {
|
|
var link = $(this),
|
|
cl = hover+' '+dropover;
|
|
|
|
if (insideNavbar(e.clientX)) {
|
|
link.addClass(cl)
|
|
if (link.is('.'+collapsed+':not(.'+expanded+')')) {
|
|
setTimeout(function() {
|
|
link.is('.'+dropover) && link.children('.'+arrow).click();
|
|
}, 500);
|
|
}
|
|
} else {
|
|
link.removeClass(cl);
|
|
}
|
|
},
|
|
out : function() { $(this).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'),
|
|
|
|
/**
|
|
* Symlink marker html template
|
|
*
|
|
* @type String
|
|
*/
|
|
stpl = fm.res('tpl', 'symlink'),
|
|
|
|
/**
|
|
* Html template replacement methods
|
|
*
|
|
* @type Object
|
|
*/
|
|
replace = {
|
|
id : function(dir) { return fm.navHash2Id(dir.hash) },
|
|
cssclass : function(dir) { return (dir.phash ? '' : root)+' '+navdir+' '+fm.perms2class(dir)+' '+(dir.dirs && !dir.link ? collapsed : ''); },
|
|
permissions : function(dir) { return !dir.read || !dir.write ? ptpl : ''; },
|
|
symlink : function(dir) { return dir.alias ? stpl : ''; }
|
|
},
|
|
|
|
/**
|
|
* Return html for given dir
|
|
*
|
|
* @param Object directory
|
|
* @return String
|
|
*/
|
|
itemhtml = function(dir) {
|
|
dir.name = fm.escape(dir.i18 || dir.name);
|
|
|
|
return tpl.replace(/(?:\{([a-z]+)\})/ig, function(m, key) {
|
|
return dir[key] || (replace[key] ? replace[key](dir) : '');
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Return only dirs from files list
|
|
*
|
|
* @param Array files list
|
|
* @return Array
|
|
*/
|
|
filter = function(files) {
|
|
return $.map(files||[], function(f) { return f.mime == 'directory' ? f : null });
|
|
},
|
|
|
|
/**
|
|
* Find parent subtree for required directory
|
|
*
|
|
* @param String dir hash
|
|
* @return jQuery
|
|
*/
|
|
findSubtree = function(hash) {
|
|
return hash ? tree.find('#'+fm.navHash2Id(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'))))
|
|
&& dir.name.toLowerCase().localeCompare(info.name.toLowerCase()) < 0) {
|
|
return node;
|
|
}
|
|
node = node.next();
|
|
}
|
|
return $('');
|
|
},
|
|
|
|
/**
|
|
* Add new dirs in tree
|
|
*
|
|
* @param Array dirs list
|
|
* @return void
|
|
*/
|
|
updateTree = function(dirs) {
|
|
var length = dirs.length,
|
|
orphans = [],
|
|
i = dirs.length,
|
|
dir, html, parent, sibling;
|
|
|
|
var firstVol = true; // check for netmount volume
|
|
while (i--) {
|
|
dir = dirs[i];
|
|
|
|
if (tree.find('#'+fm.navHash2Id(dir.hash)).length) {
|
|
continue;
|
|
}
|
|
|
|
if ((parent = findSubtree(dir.phash)).length) {
|
|
html = itemhtml(dir);
|
|
if (dir.phash && (sibling = findSibling(parent, dir)).length) {
|
|
sibling.before(html);
|
|
} else {
|
|
parent[firstVol || dir.phash ? 'append' : 'prepend'](html);
|
|
firstVol = false;
|
|
}
|
|
} else {
|
|
orphans.push(dir);
|
|
}
|
|
}
|
|
|
|
if (orphans.length && orphans.length < length) {
|
|
return updateTree(orphans);
|
|
}
|
|
|
|
setTimeout(function() {
|
|
updateDroppable();
|
|
}, 10);
|
|
|
|
},
|
|
|
|
/**
|
|
* Mark current directory as active
|
|
* If current directory is not in tree - load it and its parents
|
|
*
|
|
* @param {Boolean} do not recursive call
|
|
* @return void
|
|
*/
|
|
sync = function(stopRec) {
|
|
var cwd = fm.cwd().hash,
|
|
current = tree.find('#'+fm.navHash2Id(cwd)),
|
|
rootNode, dir;
|
|
|
|
if (openRoot) {
|
|
rootNode = tree.find('#'+fm.navHash2Id(fm.root()));
|
|
rootNode.is('.'+loaded) && rootNode.addClass(expanded).next('.'+subtree).show();
|
|
openRoot = false;
|
|
}
|
|
|
|
if (!current.is('.'+active)) {
|
|
tree.find('.'+navdir+'.'+active).removeClass(active);
|
|
current.addClass(active);
|
|
}
|
|
|
|
if (opts.syncTree) {
|
|
if (current.length) {
|
|
return current.parentsUntil('.'+root).filter('.'+subtree).show().prev('.'+navdir).addClass(expanded);
|
|
}
|
|
if (fm.newAPI) {
|
|
dir = fm.file(cwd);
|
|
if (dir && dir.phash && tree.find('#'+fm.navHash2Id(dir.phash)).length) {
|
|
updateTree([dir]);
|
|
return sync();
|
|
}
|
|
fm.request({
|
|
data : {cmd : 'parents', target : cwd},
|
|
preventFail : true
|
|
})
|
|
.done(function(data) {
|
|
var dirs = filter(data.tree);
|
|
updateTree(dirs);
|
|
updateArrows(dirs, loaded);
|
|
cwd == fm.cwd().hash && sync(true);
|
|
})
|
|
;
|
|
}
|
|
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Make writable and not root dirs droppable
|
|
*
|
|
* @return void
|
|
*/
|
|
updateDroppable = function() {
|
|
tree.find('.'+navdir+':not(.'+droppable+',.elfinder-ro,.elfinder-na)').droppable(droppableopts);
|
|
},
|
|
|
|
/**
|
|
* 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+')';
|
|
|
|
|
|
//tree.find('.'+subtree+':has(*)').prev(':not(.'+collapsed+')').addClass(collapsed)
|
|
|
|
$.each(dirs, function(i, dir) {
|
|
tree.find('#'+fm.navHash2Id(dir.phash)+sel)
|
|
.filter(function() { return $(this).next('.'+subtree).children().length > 0 })
|
|
.addClass(cls);
|
|
})
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
* Navigation tree
|
|
*
|
|
* @type JQuery
|
|
*/
|
|
tree = $(this).addClass(treeclass)
|
|
// make dirs draggable and toggle hover class
|
|
.delegate('.'+navdir, 'hover', function(e) {
|
|
var link = $(this),
|
|
enter = e.type == 'mouseenter';
|
|
|
|
if (!link.is('.'+dropover+' ,.'+disabled)) {
|
|
enter && !link.is('.'+root+',.'+draggable+',.elfinder-na,.elfinder-wo') && link.draggable(fm.draggable);
|
|
link.toggleClass(hover, enter);
|
|
}
|
|
})
|
|
// add/remove dropover css class
|
|
.delegate('.'+navdir, 'dropover dropout drop', function(e) {
|
|
$(this)[e.type == 'dropover' ? 'addClass' : 'removeClass'](dropover+' '+hover);
|
|
})
|
|
// open dir or open subfolders in tree
|
|
.delegate('.'+navdir, 'click', function(e) {
|
|
var link = $(this),
|
|
hash = fm.navId2Hash(link.attr('id')),
|
|
file = fm.file(hash);
|
|
|
|
fm.trigger('searchend');
|
|
|
|
if (hash != fm.cwd().hash && !link.is('.'+disabled)) {
|
|
fm.exec('open', file.thash || hash);
|
|
} else if (link.is('.'+collapsed)) {
|
|
link.children('.'+arrow).click();
|
|
}
|
|
})
|
|
// toggle subfolders in tree
|
|
.delegate('.'+navdir+'.'+collapsed+' .'+arrow, 'click', function(e) {
|
|
var arrow = $(this),
|
|
link = arrow.parent('.'+navdir),
|
|
stree = link.next('.'+subtree);
|
|
|
|
e.stopPropagation();
|
|
|
|
if (link.is('.'+loaded)) {
|
|
link.toggleClass(expanded);
|
|
stree.slideToggle()
|
|
} else {
|
|
spinner.insertBefore(arrow);
|
|
link.removeClass(collapsed);
|
|
|
|
fm.request({cmd : 'tree', target : fm.navId2Hash(link.attr('id'))})
|
|
.done(function(data) {
|
|
updateTree(filter(data.tree));
|
|
|
|
if (stree.children().length) {
|
|
link.addClass(collapsed+' '+expanded);
|
|
stree.slideDown();
|
|
}
|
|
sync();
|
|
})
|
|
.always(function(data) {
|
|
spinner.remove();
|
|
link.addClass(loaded);
|
|
});
|
|
}
|
|
})
|
|
.delegate('.'+navdir, 'contextmenu', function(e) {
|
|
e.preventDefault();
|
|
|
|
fm.trigger('contextmenu', {
|
|
'type' : 'navbar',
|
|
'targets' : [fm.navId2Hash($(this).attr('id'))],
|
|
'x' : e.clientX,
|
|
'y' : e.clientY
|
|
});
|
|
}),
|
|
// move tree into navbar
|
|
navbar = fm.getUI('navbar').append(tree).show()
|
|
|
|
;
|
|
|
|
fm.open(function(e) {
|
|
var data = e.data,
|
|
dirs = filter(data.files);
|
|
|
|
data.init && tree.empty();
|
|
|
|
if (dirs.length) {
|
|
updateTree(dirs);
|
|
updateArrows(dirs, loaded);
|
|
}
|
|
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) {
|
|
var dirs = filter(e.data.changed),
|
|
l = dirs.length,
|
|
dir, node, tmp, realParent, reqParent, realSibling, reqSibling, isExpanded, isLoaded;
|
|
|
|
while (l--) {
|
|
dir = dirs[l];
|
|
if ((node = tree.find('#'+fm.navHash2Id(dir.hash))).length) {
|
|
if (dir.phash) {
|
|
realParent = node.closest('.'+subtree);
|
|
reqParent = findSubtree(dir.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(node) : reqParent.append(node);
|
|
}
|
|
}
|
|
isExpanded = node.is('.'+expanded);
|
|
isLoaded = node.is('.'+loaded);
|
|
tmp = $(itemhtml(dir));
|
|
node.replaceWith(tmp.children('.'+navdir));
|
|
|
|
if (dir.dirs
|
|
&& (isExpanded || isLoaded)
|
|
&& (node = tree.find('#'+fm.navHash2Id(dir.hash)))
|
|
&& node.next('.'+subtree).children().length) {
|
|
isExpanded && node.addClass(expanded);
|
|
isLoaded && node.addClass(loaded);
|
|
}
|
|
}
|
|
}
|
|
|
|
sync();
|
|
updateDroppable();
|
|
})
|
|
// remove dirs
|
|
.remove(function(e) {
|
|
var dirs = e.data.removed,
|
|
l = dirs.length,
|
|
node, stree;
|
|
|
|
while (l--) {
|
|
if ((node = tree.find('#'+fm.navHash2Id(dirs[l]))).length) {
|
|
stree = node.closest('.'+subtree);
|
|
node.parent().detach();
|
|
if (!stree.children().length) {
|
|
stree.hide().prev('.'+navdir).removeClass(collapsed+' '+expanded+' '+loaded);
|
|
}
|
|
}
|
|
}
|
|
})
|
|
// add/remove active class for current dir
|
|
.bind('search searchend', function(e) {
|
|
tree.find('#'+fm.navHash2Id(fm.cwd().hash))[e.type == 'search' ? 'removeClass' : 'addClass'](active);
|
|
})
|
|
// lock/unlock dirs while moving
|
|
.bind('lockfiles unlockfiles', function(e) {
|
|
var lock = e.type == 'lockfiles',
|
|
act = lock ? 'disable' : 'enable',
|
|
dirs = $.map(e.data.files||[], function(h) {
|
|
var dir = fm.file(h);
|
|
return dir && dir.mime == 'directory' ? h : null;
|
|
})
|
|
|
|
$.each(dirs, function(i, hash) {
|
|
var dir = tree.find('#'+fm.navHash2Id(hash));
|
|
|
|
if (dir.length) {
|
|
dir.is('.'+draggable) && dir.draggable(act);
|
|
dir.is('.'+droppable) && dir.droppable(active);
|
|
dir[lock ? 'addClass' : 'removeClass'](disabled);
|
|
}
|
|
});
|
|
})
|
|
|
|
});
|
|
|
|
return this;
|
|
}
|