Subversion Repositories eFlore/Applications.cel

Rev

Go to most recent revision | 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