Blame | Last modification | View Log | RSS feed
// vim: ts=4:sw=4:nu:fdc=4:nospell
/**
* Ext.ux.FileTreePanel
*
* @author Ing. Jozef Sakáloš
* @version $Id: Ext.ux.FileTreePanel.js 112 2008-03-28 21:11:17Z jozo $
* @date 13. March 2008
*
* @license Ext.ux.FileTreePanel is licensed under the terms of
* the Open Source LGPL 3.0 license. Commercial use is permitted to the extent
* that the code/component(s) do NOT become part of another Open Source or Commercially
* licensed development library or toolkit without explicit permission.
*
* License details: http://www.gnu.org/licenses/lgpl.html
*/
/*global Ext, window, document, setTimeout */
/**
* @class Ext.ux.FileTreePanel
* @extends Ext.tree.TreePanel
*/
Ext.ux.FileTreePanel = Ext.extend(Ext.tree.TreePanel, {
// config variables overridable from outside
// {{{
/**
* @cfg {Object} baseParams This object is not used directly by FileTreePanel but it is
* propagated to lower level objects instead. Included here for convenience.
*/
/**
* @cfg {String} confirmText Text to display as title of confirmation message box
*/
confirmText:'Confirm'
/**
* @cfg {Boolean} containerScroll true to register
* this container with ScrollManager (defaults to true)
*/
,containerScroll:true
/**
* @cfg {String} deleteText Delete text (for message box title or other displayed texts)
*/
,deleteText:'Delete'
/**
* @cfg {String} deleteUrl URL to use when deleting; this.url is used if not set (defaults to undefined)
*/
/**
* @cfg {String} downloadUrl URL to use when downloading; this.url is used if not set (defaults to undefined)
*/
/**
* @cfg {Boolean} enableDD true to enable drag & drop of files and folders (defaults to true)
*/
,enableDD:true
/**
* @cfg {Boolean) enableDelete true to enable to delete files and directories.
* If false context menu item is not shown (defaults to true)
*/
,enableDelete:true
/**
* @cfg {Boolean) enableNewDir true to enable to create new directory.
* If false context menu item is not shown (defaults to true)
*/
,enableNewDir:true
/**
* @cfg {Boolean) enableOpen true to enable open submenu
* If false context menu item is not shown (defaults to true)
*/
,enableOpen:true
/**
* @cfg {Boolean} enableProgress true to enable querying server for progress information
* Passed to underlying uploader. Included here for convenience.
*/
,enableProgress:true
/**
* @cfg {Boolean) enableRename true to enable to rename files and directories.
* If false context menu item is not shown (defaults to true)
*/
,enableRename:true
/**
* @cfg {Boolean} enableSort true to enable sorting of tree. See also folderSort (defaults to true)
*/
,enableSort:true
/**
* @cfg {Boolean) enableUpload true to enable to upload files.
* If false context menu item is not shown (defaults to true)
*/
,enableUpload:true
/**
* @cfg {String} errorText Text to display for an error
*/
,errorText:'Error'
/**
* @cfg {String} existsText Text to display in message box if file exists
*/
,existsText:'File <b>{0}</b> already exists'
/**
* @cfg {Boolean} true to expand root node on FileTreePanel render (defaults to true)
*/
,expandOnRender:true
/**
* @cfg {String} fileCls class prefix to add to nodes. "-extension" is appended to
* this prefix to form filetype class, for example: file-odt, file-pdf. These classes
* are used to display correct filetype icons in the tree. css file and icons must
* exist of course.
*/
,fileCls:'file'
/**
* @cfg {String} fileText
*/
,fileText:'File'
/**
* @cfg {Boolean} focusPopup true to focus new browser popup window for 'popup' openMode
* (defaults to true)
*/
,focusPopup:true
/**
* @cfg {Boolean} folderSort true to place directories at the top of the tree (defaults to true)
*/
,folderSort:true
/**
* @cfg {String} hrefPrefix Text to prepend before file href for file open command.
* (defaults to '')
*/
,hrefPrefix:''
/**
* @cfg {String} hrefSuffix Text to append to file href for file open command.
* (defaults to '')
*/
,hrefSuffix:''
/**
* @cfg {String} layout Layout to use for this panel (defaults to 'fit')
*/
,layout:'fit'
/**
* @cfg {String} loadingText Text to use for load mask msg
*/
,loadingText:'Loading'
/**
* @cfg {Boolean} loadMask True to mask tree panel while loading
*/
,loadMask:false
/**
* @cfg {Number} maxFileSize Maximum upload file size in bytes
* This config property is propagated down to uploader for convenience
*/
,maxFileSize:524288
/**
* @cfg {Number} maxMsgLen Maximum message length for message box (defaults to 2000).
* If message is longer Ext.util.Format.ellipsis is used to truncate it and append ...
*/
,maxMsgLen:2000
/**
* @cfg {String} method Method to use when posting to server. Other valid value is 'get'
* (defaults to 'post')
*/
,method:'post'
/**
* @cfg {String} newdirText Default name for new directories (defaults to 'New Folder')
*/
,newdirText:'New Folder'
/**
* @cfg {String} newdirUrl URL to use when creating new directory;
* this.url is used if not set (defaults to undefined)
*/
/**
* @cfg {String} openMode Default file open mode. This mode is used when user dblclicks
* a file. Other valid values are '_self', '_blank' and 'download' (defaults to 'popup')
*/
,openMode:'popup'
/**
* @cfg {String} overwriteText Text to use in overwrite confirmation message box
*/
,overwriteText:'Do you want to overwrite it?'
/**
* @cfg {String} popupFeatures Features for new browser window opened by popup open mode
*/
,popupFeatures:'width=800,height=600,dependent=1,scrollbars=1,resizable=1,toolbar=1'
/**
* @cfg {Boolean} readOnly true to disable write operations. treeEditor and context menu
* are not created if true (defaults to false)
*/
,readOnly:false
/**
* @cfg {String} reallyWantText Text to display for that question
*/
,reallyWantText:'Do you really want to'
/**
* @cfg {String} renameUrl URL to use when renaming; this.url is used if not set (defaults to undefined)
*/
/**
* @cfg {String} rootPath Relative path pointing to the directory that is root of this tree (defaults to 'root')
*/
,rootPath:'root'
/**
* @cfg {String} rootText Text to display for root node (defaults to 'Tree Root')
*/
,rootText:'Tree Root'
/**
* @cfg {Boolean} selectOnEdit true to select the edited text on edit start (defaults to true)
*/
,selectOnEdit:true
/**
* @cfg {Boolean} singleUpload true to upload files in one form, false to upload one by one
* This config property is propagated down to uploader for convenience
*/
,singleUpload:false
/**
* @cfg {Boolean} topMenu true to create top toolbar with menu in addition to contextmenu
*/
,topMenu:false
/**
* @cfg {String} url URL to use when communicating with server
*/
,url:'filetree.php'
// }}}
// overrides
// {{{
/**
* called by Ext when instantiating
* @private
* @param {Object} config Configuration object
*/
,initComponent:function() {
// {{{
Ext.apply(this, {
// create root node
root:new Ext.tree.AsyncTreeNode({
text:this.rootText
,path:this.rootPath
,allowDrag:false
})
// create treeEditor
,treeEditor:!this.readOnly ? new Ext.tree.TreeEditor(this, {
allowBlank:false
,cancelOnEsc:true
,completeOnEnter:true
,ignoreNoChange:true
,selectOnFocus:this.selectOnEdit
}) : undefined
// drop config
,dropConfig:this.dropConfig ? this.dropConfig : {
ddGroup:this.ddGroup || 'TreeDD'
,appendOnly:this.enableSort
,expandDelay:3600000 // do not expand on drag over node
}
// create treeSorter
,treeSorter:this.enableSort ? new Ext.tree.TreeSorter(this, {folderSort:this.folderSort}) : undefined
// {{{
,keys:[{
// Enter = open
key:Ext.EventObject.ENTER, scope:this
,fn:function(key, e) {
var sm = this.getSelectionModel();
var node = sm.getSelectedNode();
if(node && 0 !== node.getDepth() && node.isLeaf()) {
this.openNode(node);
}
}},{
// F2 = edit
key:113, scope:this
,fn:function(key, e) {
var sm = this.getSelectionModel();
var node = sm.getSelectedNode();
if(node && 0 !== node.getDepth() && this.enableRename && this.readOnly !== true) {
this.treeEditor.triggerEdit(node);
}
}},{
// Delete Key = Delete
key:46, stopEvent:true, scope:this
,fn:function(key, e) {
var sm = this.getSelectionModel();
var node = sm.getSelectedNode();
if(node && 0 !== node.getDepth() && this.enableDelete && this.readOnly !== true) {
this.deleteNode(node);
}
}},{
// Ctrl + E = reload
key:69, ctrl:true, stopEvent:true, scope:this
,fn:function(key, e) {
var sm = this.getSelectionModel();
var node = sm.getSelectedNode();
if(node) {
node = node.isLeaf() ? node.parentNode : node;
sm.select(node);
node.reload();
}
}},{
// Ctrl + -> = expand deep
key:39, ctrl:true, stopEvent:true, scope:this
,fn:function(key, e) {
var sm = this.getSelectionModel();
var node = sm.getSelectedNode();
if(node && !node.isLeaf()) {
sm.select(node);
node.expand.defer(1, node, [true]);
}
}},{
// Ctrl + <- = collapse deep
key:37, ctrl:true, scope:this, stopEvent:true
,fn:function(key, e) {
var sm = this.getSelectionModel();
var node = sm.getSelectedNode();
if(node && !node.isLeaf()) {
sm.select(node);
node.collapse.defer(1, node, [true]);
}
}},{
// Ctrl + N = New Directory
key:78, ctrl:true, scope:this, stopEvent:true
,fn:function(key, e) {
var sm, node;
sm = this.getSelectionModel();
node = sm.getSelectedNode();
if(node && this.enableNewDir && this.readOnly !== true) {
node = node.isLeaf() ? node.parentNode : node;
this.createNewDir(node);
}
}}]
// }}}
}); // eo apply
// }}}
// {{{
// create loader
if(!this.loader) {
this.loader = new Ext.tree.TreeLoader({
url:this.url
,baseParams:{cmd:'get'}
,listeners:{
beforeload:{scope:this, fn:function(loader, node) {
loader.baseParams.path = this.getPath(node);
}}
}
});
}
// }}}
// {{{
// install top menu if configured
if(true === this.topMenu) {
this.tbar = [{
text:this.fileText
,disabled:true
,scope:this
,menu:this.getContextMenu()
}];
}
// }}}
// call parent
Ext.ux.FileTreePanel.superclass.initComponent.apply(this, arguments);
// {{{
// install treeEditor event handlers
if(this.treeEditor) {
// do not enter edit mode on selected node click
this.treeEditor.beforeNodeClick = function(node,e){return true;};
// treeEditor event handlers
this.treeEditor.on({
complete:{scope:this, fn:this.onEditComplete}
,beforecomplete:{scope:this, fn:this.onBeforeEditComplete}
});
}
// }}}
// {{{
// install event handlers
this.on({
contextmenu:{scope:this, fn:this.onContextMenu, stopEvent:true}
,dblclick:{scope:this, fn:this.onDblClick}
,beforenodedrop:{scope:this, fn:this.onBeforeNodeDrop}
,nodedrop:{scope:this, fn:this.onNodeDrop}
,nodedragover:{scope:this, fn:this.onNodeDragOver}
});
// }}}
// {{{
// add events
this.addEvents(
/**
* @event beforeopen
* Fires before file open. Return false to cancel the event
* @param {Ext.ux.FileTreePanel} this
* @param {String} fileName name of the file being opened
* @param {String} url url of the file being opened
* @param {String} mode open mode
*/
'beforeopen'
/**
* @event open
* Fires after file open has been initiated
* @param {Ext.ux.FileTreePanel} this
* @param {String} fileName name of the file being opened
* @param {String} url url of the file being opened
* @param {String} mode open mode
*/
,'open'
/**
* @event beforerename
* Fires after the user completes file name editing
* but before the file is renamed. Return false to cancel the event
* @param {Ext.ux.FileTreePanel} this
* @param {Ext.tree.AsyncTreeNode} node being renamed
* @param {String} newPath including file name
* @param {String} oldPath including file name
*/
,'beforerename'
/**
* @event rename
* Fires after the file has been successfully renamed
* @param {Ext.ux.FileTreePanel} this
* @param {Ext.tree.AsyncTreeNode} node that has been renamed
* @param {String} newPath including file name
* @param {String} oldPath including file name
*/
,'rename'
/**
* @event renamefailure
* Fires after a failure when renaming file
* @param {Ext.ux.FileTreePanel} this
* @param {Ext.tree.AsyncTreeNode} node rename of which failed
* @param {String} newPath including file name
* @param {String} oldPath including file name
*/
,'renamefailure'
/**
* @event beforedelete
* Fires before a file or directory is deleted. Return false to cancel the event.
* @param {Ext.ux.FileTreePanel} this
* @param {Ext.tree.AsyncTreeNode} node being deleted
*/
,'beforedelete'
/**
* @event delete
* Fires after a file or directory has been deleted
* @param {Ext.ux.FileTreePanel} this
* @param {String} path including file name that has been deleted
*/
,'delete'
/**
* @event deletefailure
* Fires if node delete failed
* @param {Ext.ux.FileTreePanel} this
* @param {Ext.tree.AsyncTreeNode} node delete of which failed
*/
,'deletefailure'
/**
* @event beforenewdir
* Fires before new directory is created. Return false to cancel the event
* @param {Ext.ux.FileTreePanel} this
* @param {Ext.tree.AsyncTreeNode} node under which the new directory is being created
*/
,'beforenewdir'
/**
* @event newdir
* Fires after the new directory has been successfully created
* @param {Ext.ux.FileTreePanel} this
* @param {Ext.tree.AsyncTreeNode} new node/directory that has been created
*/
,'newdir'
/**
* @event newdirfailure
* Fires if creation of new directory failed
* @param {Ext.ux.FileTreePanel} this
* @param {String} path creation of which failed
*/
,'newdirfailure'
); // eo addEvents
// }}}
} // eo function initComponent
// }}}
// {{{
/**
* onRender override - just expands root node if configured
* @private
*/
,onRender:function() {
// call parent
Ext.ux.FileTreePanel.superclass.onRender.apply(this, arguments);
if(true === this.topMenu) {
this.topMenu = Ext.getCmp(this.getTopToolbar().items.itemAt(0).id);
this.getSelectionModel().on({
scope:this
,selectionchange:function(sm, node) {
var disable = node ? false : true;
disable = disable || this.readOnly;
this.topMenu.setDisabled(disable);
}
});
Ext.apply(this.topMenu, {
showMenu:function() {
this.showContextMenu(false);
}.createDelegate(this)
// ,menu:this.getContextMenu()
});
}
// expand root node if so configured
if(this.expandOnRender) {
this.root.expand();
}
// prevent default browser context menu to appear
this.el.on({
contextmenu:{fn:function(){return false;},stopEvent:true}
});
// setup loading mask if configured
if(true === this.loadMask) {
this.loader.on({
scope:this.el
,beforeload:this.el.mask.createDelegate(this.el, [this.loadingText + '...'])
,load:this.el.unmask
,loadexception:this.el.unmask
});
}
} // eo function onRender
// }}}
// new methods
// {{{
/**
* runs after an Ajax requested command has completed/failed
* @private
* @param {Object} options Options used for the request
* @param {Boolean} success true if ajax call was successful (cmd may have failed)
* @param {Object} response ajax call response object
*/
,cmdCallback:function(options, success, response) {
var i, o, node;
var showMsg = true;
// process Ajax success
if(true === success) {
// try to decode JSON response
try {
o = Ext.decode(response.responseText);
}
catch(ex) {
this.showError(response.responseText);
}
// process command success
if(true === o.success) {
switch(options.params.cmd) {
case 'delete':
if(true !== this.eventsSuspended) {
this.fireEvent('delete', this, this.getPath(options.node));
}
options.node.parentNode.removeChild(options.node);
break;
case 'newdir':
if(true !== this.eventsSuspended) {
this.fireEvent('newdir', this, options.node);
}
break;
case 'rename':
this.updateCls(options.node, options.params.oldname);
if(true !== this.eventsSuspended) {
this.fireEvent('rename', this, options.node, options.params.newname, options.params.oldname);
}
break;
}
} // eo process command success
// process command failure
else {
switch(options.params.cmd) {
case 'rename':
// handle drag & drop rename error
if(options.oldParent) {
options.oldParent.appendChild(options.node);
}
// handle simple rename error
else {
options.node.setText(options.oldName);
}
// signal failure to onNodeDrop
if(options.e) {
options.e.failure = true;
}
if(true !== this.eventsSuspended) {
this.fireEvent('renamefailure', this, options.node, options.params.newname, options.params.oldname);
}
break;
case 'newdir':
if(false !== this.eventsSuspended) {
this.fireEvent('newdirfailure', this, options.params.dir);
}
options.node.parentNode.removeChild(options.node);
break;
case 'delete':
if(true !== this.eventsSuspended) {
this.fireEvent('deletefailure', this, options.node);
}
options.node.parentNode.reload.defer(1, options.node.parentNode);
break;
default:
this.root.reload();
break;
}
// show default message box with server error
this.showError(o.error || response.responseText);
} // eo process command failure
} // eo process Ajax success
// process Ajax failure
else {
this.showError(response.responseText);
}
} // eo function cmdCallback
// }}}
// {{{
/**
* displays overwrite confirm msg box and runs passed callback if response is yes
* @private
* @param {String} filename File to overwrite
* @param {Function} callback Function to call on yes response
* @param {Object} scope Scope for callback (defaults to this)
*/
,confirmOverwrite:function(filename, callback, scope) {
Ext.Msg.show({
title:this.confirmText
,msg:String.format(this.existsText, filename) + '. ' + this.overwriteText
,icon:Ext.Msg.QUESTION
,buttons:Ext.Msg.YESNO
,fn:callback.createDelegate(scope || this)
});
}
// }}}
// {{{
/**
* creates new directory (node)
* @private
* @param {Ext.tree.AsyncTreeNode} node
*/
,createNewDir:function(node) {
// fire beforenewdir event
if(true !== this.eventsSuspended && false === this.fireEvent('beforenewdir', this, node)) {
return;
}
var treeEditor = this.treeEditor;
var newNode;
// get node to append the new directory to
var appendNode = node.isLeaf() ? node.parentNode : node;
// create new folder after the appendNode is expanded
appendNode.expand(false, false, function(n) {
// create new node
newNode = n.appendChild(new Ext.tree.AsyncTreeNode({text:this.newdirText, iconCls:'folder'}));
// setup one-shot event handler for editing completed
treeEditor.on({
complete:{
scope:this
,single:true
,fn:this.onNewDir
}}
);
// creating new directory flag
treeEditor.creatingNewDir = true;
// start editing after short delay
(function(){treeEditor.triggerEdit(newNode);}.defer(10));
// expand callback needs to run in this context
}.createDelegate(this));
} // eo function creatingNewDir
// }}}
// {{{
/**
* deletes the passed node
* @private
* @param {Ext.tree.AsyncTreeNode} node
*/
,deleteNode:function(node) {
// fire beforedelete event
if(true !== this.eventsSuspended && false === this.fireEvent('beforedelete', this, node)) {
return;
}
Ext.Msg.show({
title:this.deleteText
,msg:this.reallyWantText + ' ' + this.deleteText.toLowerCase() + ' <b>' + node.text + '</b>?'
,icon:Ext.Msg.WARNING
,buttons:Ext.Msg.YESNO
,scope:this
,fn:function(response) {
// do nothing if answer is not yes
if('yes' !== response) {
this.getEl().dom.focus();
return;
}
// setup request options
var options = {
url:this.deleteUrl || this.url
,method:this.method
,scope:this
,callback:this.cmdCallback
,node:node
,params:{
cmd:'delete'
,file:this.getPath(node)
}
};
Ext.Ajax.request(options);
}
});
} // eo function deleteNode
// }}}
// {{{
/**
* requests file download from server
* @private
* @param {String} path Full path including file name but relative to server root path
*/
,downloadFile:function(path) {
// create hidden target iframe
var id = Ext.id();
var frame = document.createElement('iframe');
frame.id = id;
frame.name = id;
frame.className = 'x-hidden';
if(Ext.isIE) {
frame.src = Ext.SSL_SECURE_URL;
}
document.body.appendChild(frame);
if(Ext.isIE) {
document.frames[id].name = id;
}
var form = Ext.DomHelper.append(document.body, {
tag:'form'
,method:'post'
,action:this.downloadUrl || this.url
,target:id
});
document.body.appendChild(form);
var hidden;
// append cmd to form
hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'cmd';
hidden.value = 'download';
form.appendChild(hidden);
// append path to form
hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'path';
hidden.value = path;
form.appendChild(hidden);
var callback = function() {
Ext.EventManager.removeListener(frame, 'load', callback, this);
setTimeout(function() {document.body.removeChild(form);}, 100);
setTimeout(function() {document.body.removeChild(frame);}, 110);
};
Ext.EventManager.on(frame, 'load', callback, this);
form.submit();
}
// }}}
// {{{
/**
* returns (and lazy create) the context menu
* @private
*/
,getContextMenu:function() {
// lazy create context menu
if(!this.contextmenu) {
var config = {
singleUpload:this.singleUpload
,maxFileSize:this.maxFileSize
,enableProgress:this.enableProgress
};
if(this.baseParams) {
config.baseParams = this.baseParams;
}
this.contextmenu = new Ext.ux.FileTreeMenu(config);
this.contextmenu.on({click:{scope:this, fn:this.onContextClick}});
this.uploadPanel = this.contextmenu.getItemByCmd('upload-panel').component;
this.uploadPanel.on({
beforeupload:{scope:this, fn:this.onBeforeUpload}
,allfinished:{scope:this, fn:this.onAllFinished}
});
this.uploadPanel.setUrl(this.uploadUrl || this.url);
}
return this.contextmenu;
} // eo function getContextMenu
// }}}
// {{{
/**
* returns file class based on name extension
* @private
* @param {String} name File name to get class of
*/
,getFileCls:function(name) {
var atmp = name.split('.');
if(1 === atmp.length) {
return this.fileCls;
}
else {
return this.fileCls + '-' + atmp.pop().toLowerCase();
}
}
// }}}
// {{{
/**
* returns path of node (file/directory)
* @private
*/
,getPath:function(node) {
var path, p, a;
// get path for non-root node
if(node !== this.root) {
p = node.parentNode;
a = [node.text];
while(p && p !== this.root) {
a.unshift(p.text);
p = p.parentNode;
}
a.unshift(this.root.attributes.path || '');
path = a.join(this.pathSeparator);
}
// path for root node is it's path attribute
else {
path = node.attributes.path || '';
}
// a little bit of security: strip leading / or .
// full path security checking has to be implemented on server
path = path.replace(/^[\/\.]*/, '');
return path;
} // eo function getPath
// }}}
// {{{
/**
* returns true if node has child with the specified name (text)
* @private
* @param {Ext.data.Node} node
* @param {String} childName
*/
,hasChild:function(node, childName) {
return (node.isLeaf() ? node.parentNode : node).findChild('text', childName) !== null;
}
// }}}
// {{{
/**
* Hides context menu
* @return {Ext.ux.FileTreeMenu} this
*/
,hideContextMenu:function() {
if(this.contextmenu && this.contextmenu.isVisible()) {
this.contextmenu.hide();
}
return this;
} // eo function hideContextMenu
// }}}
// {{{
/**
* called before editing is completed - allows edit cancellation
* @private
* @param {TreeEditor} editor
* @param {String} newName
* @param {String} oldName
*/
,onBeforeEditComplete:function(editor, newName, oldName) {
if(editor.cancellingEdit) {
editor.cancellingEdit = false;
return;
}
var oldPath = this.getPath(editor.editNode);
var newPath = oldPath.replace(/\/[^\\]+$/, '/' + newName);
if(false === this.fireEvent('beforerename', this, editor.editNode, newPath, oldPath)) {
editor.cancellingEdit = true;
editor.cancelEdit();
return false;
}
}
// }}}
// {{{
/**
* runs before node is dropped
* @private
* @param {Object} e dropEvent object
*/
,onBeforeNodeDrop:function(e) {
// source node, node being dragged
var s = e.dropNode;
// destination node (dropping on this node)
var d = e.target.leaf ? e.target.parentNode : e.target;
// node has been dropped within the same parent
if(s.parentNode === d) {
return false;
}
// check if same name exists in the destination
// this works only if destination node is loaded
if(this.hasChild(d, s.text) && undefined === e.confirmed) {
this.confirmOverwrite(s.text, function(response) {
e.confirmed = 'yes' === response;
this.onBeforeNodeDrop(e);
});
return false;
}
if(false === e.confirmed) {
return false;
}
e.confirmed = undefined;
e.oldParent = s.parentNode;
var oldName = this.getPath(s);
var newName = this.getPath(d) + '/' + s.text;
// fire beforerename event
if(true !== this.eventsSuspended && false === this.fireEvent('beforerename', this, s, newName, oldName)) {
return false;
}
var options = {
url:this.renameUrl || this.url
,method:this.method
,scope:this
,callback:this.cmdCallback
,node:s
,oldParent:s.parentNode
,e:e
,params:{
cmd:'rename'
,oldname:oldName
,newname:newName
}
};
Ext.Ajax.request(options);
return true;
}
// }}}
// {{{
/**
* sets uploadPanel's destination path
* @private
*/
,onBeforeUpload:function(uploadPanel) {
var menu = this.getContextMenu();
var path = this.getPath(menu.node);
if(menu.node.isLeaf()) {
path = path.replace(/\/[^\/]+$/, '', path);
}
uploadPanel.setPath(path);
} // eo function onBeforeUpload
// }}}
// {{{
/**
* reloads tree node on upload finish
* @private
*/
,onAllFinished:function(uploader) {
var menu = this.getContextMenu();
(menu.node.isLeaf() ? menu.node.parentNode : menu.node).reload();
} // eo function onAllFinished
// }}}
// {{{
/**
* @private
* context menu click handler
* @param {Ext.menu.Menu} context menu
* @param {Ext.menu.Item} item clicked
* @param {Ext.EventObject} raw event
*/
,onContextClick:function(menu, item, e) {
if(item.disabled) {
return;
}
var node = menu.node;
if(!node) {
node = menu.parentMenu.node;
}
switch(item.cmd) {
case 'reload':
node.reload();
break;
case 'expand':
node.expand(true);
break;
case 'collapse':
node.collapse(true);
break;
case 'open':
this.openNode(node);
break;
case 'open-self':
this.openNode(node, '_self');
break;
case 'open-popup':
this.openNode(node, 'popup');
break;
case 'open-blank':
this.openNode(node, '_blank');
break;
case 'open-dwnld':
this.openNode(node, 'download');
break;
case 'rename':
this.treeEditor.triggerEdit(node);
break;
case 'delete':
this.deleteNode(node);
break;
case 'newdir':
this.createNewDir(node);
break;
default:
break;
}
} // eo function onContextClick
// }}}
// {{{
/**
* contextmenu event handler
* @private
*/
,onContextMenu:function(node, e) {
if(this.readOnly) {
return false;
}
this.showContextMenu(node);
return false;
} // eo function onContextMenu
// }}}
// {{{
/**
* dblclick handlers
* @private
*/
,onDblClick:function(node, e) {
this.openNode(node);
} // eo function onDblClick
// }}}
// {{{
/**
* Destroys the FileTreePanel and sub-components
* @private
*/
,onDestroy:function() {
// destroy contextmenu
if(this.contextmenu) {
this.contextmenu.purgeListeners();
this.contextmenu.destroy();
this.contextmenu = null;
}
// destroy treeEditor
if(this.treeEditor) {
this.treeEditor.purgeListeners();
this.treeEditor.destroy();
this.treeEditor = null;
}
// remover reference to treeSorter
if(this.treeSorter) {
this.treeSorter = null;
}
// call parent
Ext.ux.FileTreePanel.superclass.onDestroy.call(this);
} // eo function onDestroy
// }}}
// {{{
/**
* runs when editing of a node (rename) is completed
* @private
* @param {Ext.Editor} editor
* @param {String} newName
* @param {String} oldName
*/
,onEditComplete:function(editor, newName, oldName) {
var node = editor.editNode;
if(newName === oldName || editor.creatingNewDir) {
editor.creatingNewDir = false;
return;
}
var path = this.getPath(node.parentNode);
var options = {
url:this.renameUrl || this.url
,method:this.method
,scope:this
,callback:this.cmdCallback
,node:node
,oldName:oldName
,params:{
cmd:'rename'
,oldname:path + '/' + oldName
,newname:path + '/' + newName
}
};
Ext.Ajax.request(options);
}
// }}}
// {{{
/**
* create new directory handler
* @private
* runs after editing of new directory name is completed
* @param {Ext.Editor} editor
*/
,onNewDir:function(editor) {
var path = this.getPath(editor.editNode);
var options = {
url:this.newdirUrl || this.url
,method:this.method
,scope:this
,node:editor.editNode
,callback:this.cmdCallback
,params:{
cmd:'newdir'
,dir:path
}
};
Ext.Ajax.request(options);
}
// }}}
// {{{
/**
* called while dragging over, decides if drop is allowed
* @private
* @param {Object} dd event
*/
,onNodeDragOver:function(e) {
e.cancel = e.target.disabled || e.dropNode.parentNode === e.target.parentNode && e.target.isLeaf();
} // eo function onNodeDragOver
// }}}
// {{{
/**
* called when node is dropped
* @private
* @param {Object} dd event
*/
,onNodeDrop:function(e) {
// failure can be signalled by cmdCallback
// put drop node to the original parent in that case
if(true === e.failure) {
e.oldParent.appendChild(e.dropNode);
return;
}
// if we already have node with the same text, remove the duplicate
var sameNode = e.dropNode.parentNode.findChild('text', e.dropNode.text);
if(sameNode && sameNode !== e.dropNode) {
sameNode.parentNode.removeChild(sameNode);
}
}
// }}}
// {{{
/**
* Opens node
* @param {Ext.tree.AsyncTreeNode} node
* @param {String} mode Can be "_self", "_blank", or "popup". Defaults to (this.openMode)
*/
,openNode:function(node, mode) {
if(!this.enableOpen) {
return;
}
mode = mode || this.openMode;
var url;
var path;
if(node.isLeaf()) {
path = this.getPath(node);
url = this.hrefPrefix + path + this.hrefSuffix;
// fire beforeopen event
if(true !== this.eventsSuspended && false === this.fireEvent('beforeopen', this, node.text, url, mode)) {
return;
}
switch(mode) {
case 'popup':
if(!this.popup || this.popup.closed) {
this.popup = window.open(url, this.hrefTarget, this.popupFeatures);
}
this.popup.location = url;
if(this.focusPopup) {
this.popup.focus();
}
break;
case '_self':
window.location = url;
break;
case '_blank':
window.open(url);
break;
case 'download':
this.downloadFile(path);
break;
}
// fire open event
if(true !== this.eventsSuspended) {
this.fireEvent('open', this, node.text, url, mode);
}
}
}
// }}}
// {{{
/**
* Sets/Unsets delete of files/directories disabled/enabled
* @param {Boolean} disabled
* @return {Ext.ux.FileTreePanel} this
*/
,setDeleteDisabled:function(disabled) {
disabled = !(!disabled);
if(!this.enableDelete === disabled) {
return this;
}
this.hideContextMenu();
this.enableDelete = !disabled;
} // eo function setDeleteDisabled
// }}}
// {{{
/**
* Sets/Unsets creation of new directory disabled/enabled
* @param {Boolean} disabled
* @return {Ext.ux.FileTreePanel} this
*/
,setNewdirDisabled:function(disabled) {
disabled = !(!disabled);
if(!this.enableNewDir === disabled) {
return this;
}
this.hideContextMenu();
this.enableNewDir = !disabled;
} // eo function setNewdirDisabled
// }}}
// {{{
/**
* Sets/Unsets open files disabled/enabled
* @param {Boolean} disabled
* @return {Ext.ux.FileTreePanel} this
*/
,setOpenDisabled:function(disabled) {
disabled = !(!disabled);
if(!this.enableOpen === disabled) {
return this;
}
this.hideContextMenu();
this.enableOpen = !disabled;
return this;
} // eo function setOpenDisabled
// }}}
// {{{
/**
* Sets/Unsets this tree to/from readOnly state
* @param {Boolean} readOnly
* @return {Ext.ux.FileTreePanel} this
*/
,setReadOnly:function(readOnly) {
readOnly = !(!readOnly);
if(this.readOnly === readOnly) {
return this;
}
this.hideContextMenu();
if(this.dragZone) {
this.dragZone.locked = readOnly;
}
this.readOnly = readOnly;
return this;
} // eo function setReadOnly
// }}}
// {{{
/**
* Sets/Unsets rename of files/directories disabled/enabled
* @param {Boolean} disabled
* @return {Ext.ux.FileTreePanel} this
*/
,setRenameDisabled:function(disabled) {
disabled = !(!disabled);
if(!this.enableRename === disabled) {
return this;
}
this.hideContextMenu();
if(this.dragZone) {
this.dragZone.locked = disabled;
}
this.enableRename = !disabled;
return this;
} // eo function setRenameDisabled
// }}}
// {{{
/**
* Sets/Unsets uploading of files disabled/enabled
* @param {Boolean} disabled
* @return {Ext.ux.FileTreePanel} this
*/
,setUploadDisabled:function(disabled) {
disabled = !(!disabled);
if(!this.enableUpload === disabled) {
return this;
}
this.hideContextMenu();
this.enableUpload = !disabled;
return this;
} // of function setUploadDisabled
// }}}
// {{{
/**
* adjusts context menu depending on many things and shows it
* @private
* @param {Ext.tree.AsyncTreeNode} node Node on which was right-clicked
*/
,showContextMenu:function(node) {
// setup node alignment
var topAlign = false;
var alignEl = this.topMenu ? this.topMenu.getEl() : this.body;
if(!node) {
node = this.getSelectionModel().getSelectedNode();
topAlign = true;
}
else {
alignEl = node.getUI().getEl();
}
if(!node) {
return;
}
var menu = this.getContextMenu();
menu.node = node;
// set node name
menu.getItemByCmd('nodename').setText(Ext.util.Format.ellipsis(node.text, 22));
// enable/disable items depending on node clicked
menu.setItemDisabled('open', !node.isLeaf());
menu.setItemDisabled('reload', node.isLeaf());
menu.setItemDisabled('expand', node.isLeaf());
menu.setItemDisabled('collapse', node.isLeaf());
menu.setItemDisabled('delete', node === this.root || node.disabled);
menu.setItemDisabled('rename', this.readOnly || node === this.root || node.disabled);
menu.setItemDisabled('newdir', this.readOnly || (node.isLeaf() ? node.parentNode.disabled : node.disabled));
menu.setItemDisabled('upload', node.isLeaf() ? node.parentNode.disabled : node.disabled);
menu.setItemDisabled('upload-panel', node.isLeaf() ? node.parentNode.disabled : node.disabled);
// show/hide logic
menu.getItemByCmd('open').setVisible(this.enableOpen);
menu.getItemByCmd('delete').setVisible(this.enableDelete);
menu.getItemByCmd('newdir').setVisible(this.enableNewDir);
menu.getItemByCmd('rename').setVisible(this.enableRename);
menu.getItemByCmd('upload').setVisible(this.enableUpload);
menu.getItemByCmd('upload-panel').setVisible(this.enableUpload);
menu.getItemByCmd('sep-upload').setVisible(this.enableUpload);
menu.getItemByCmd('sep-collapse').setVisible(this.enableNewDir || this.enableDelete || this.enableRename);
// select node
node.select();
// show menu
if(topAlign) {
menu.showAt(menu.getEl().getAlignToXY(alignEl, 'tl-bl?'));
}
else {
menu.showAt(menu.getEl().getAlignToXY(alignEl, 'tl-tl?', [0, 18]));
}
} // eo function
// }}}
// {{{
/**
* universal show error function
* @private
* @param {String} msg message
* @param {String} title title
*/
,showError:function(msg, title) {
Ext.Msg.show({
title:title || this.errorText
,msg:Ext.util.Format.ellipsis(msg, this.maxMsgLen)
,fixCursor:true
,icon:Ext.Msg.ERROR
,buttons:Ext.Msg.OK
,minWidth:1200 > String(msg).length ? 360 : 600
});
} // eo function showError
// }}}
// {{{
/**
* updates class of leaf after rename
* @private
* @param {Ext.tree.AsyncTreeNode} node Node to update class of
* @param {String} oldName Name the node had before
*/
,updateCls:function(node, oldName) {
if(node.isLeaf()) {
Ext.fly(node.getUI().iconNode).removeClass(this.getFileCls(oldName));
Ext.fly(node.getUI().iconNode).addClass(this.getFileCls(node.text));
}
}
// }}}
}); // eo extend
// register xtype
Ext.reg('filetreepanel', Ext.ux.FileTreePanel);
// eof