modxcms/revolution

View on GitHub
manager/assets/modext/widgets/core/tree/modx.tree.js

Summary

Maintainability
F
1 wk
Test Coverage
Ext.namespace('MODx.tree');
/**
 * Generates the Tree in Ext. All modTree classes extend this base class.
 *
 * @class MODx.tree.Tree
 * @extends Ext.tree.TreePanel
 * @constructor
 * @param {Object} config An object of options.
 * @xtype modx-tree
 */
MODx.tree.Tree = function (config) {
    config = config || {};
    Ext.applyIf(config,{
        baseParams: {}
        ,action: 'getNodes'
        ,loaderConfig: {}
    });
    if (config.action) {
        config.baseParams.action = config.action;
    }
    config.loaderConfig.dataUrl = config.url;
    config.loaderConfig.baseParams = config.baseParams;
    Ext.applyIf(config.loaderConfig,{
        preloadChildren: true
        ,clearOnLoad: true
    });

    this.config = config;
    var tl,root;
    if (this.config.url) {
        //@TODO extend TreeLoader here
        tl = new MODx.tree.TreeLoader(config.loaderConfig);
        tl.on('beforeload',function (l,node) {
            tl.dataUrl = this.config.url + '?action=' + this.config.action + '&id=' + node.attributes.id;
            if (node.attributes.type) {
                tl.dataUrl += '&type=' + node.attributes.type;
            }
        },this);
        tl.on('load',this.onLoad,this);
        root = {
            nodeType: 'async'
            ,text: config.root_name || config.rootName || ''
            ,qtip: config.root_qtip || config.rootQtip || ''
            ,draggable: false
            ,id: config.root_id || config.rootId || 'root'
            ,pseudoroot: true
            ,attributes: {
                pseudoroot: true
            }
            ,cls: 'tree-pseudoroot-node'
            ,iconCls: config.root_iconCls || config.rootIconCls || ''
        };
    } else {
        tl = new Ext.tree.TreeLoader({
            preloadChildren: true
            ,baseAttrs: {
                uiProvider: MODx.tree.CheckboxNodeUI
            }
        });
        root = new Ext.tree.TreeNode({
            text: this.config.rootName || ''
            ,draggable: false
            ,id: this.config.rootId || 'root'
            ,children: this.config.data || []
            ,pseudoroot: true
        });
    }
    Ext.applyIf(config,{
        useArrows: true
        ,autoScroll: true
        ,animate: true
        ,enableDD: true
        ,enableDrop: true
        ,ddAppendOnly: false
        ,containerScroll: true
        ,collapsible: true
        ,border: false
        ,autoHeight: true
        ,rootVisible: true
        ,loader: tl
        ,header: false
        ,hideBorders: true
        ,bodyBorder: false
        ,cls: 'modx-tree'
        ,root: root
        ,preventRender: false
        ,stateful: true
        ,menuConfig: {
            defaultAlign: 'tl-b?',
            enableScrolling: false,
            listeners: {
                show: function () {
                    var node = this.activeNode;
                    if (node)
                        node.ui.addClass('x-tree-selected');
                },
                hide: function () {
                    var node = this.activeNode;
                    if (node) {
                        node.isSelected() || (node.ui && node.ui.removeClass('x-tree-selected'));
                    }
                }
            }
        }
    });
    if (config.remoteToolbar === true && (config.tbar === undefined || config.tbar === null)) {
        Ext.Ajax.request({
            url: config.remoteToolbarUrl || config.url
            ,params: {
                action: config.remoteToolbarAction || 'getToolbar'
            }
            ,success: function (r) {
                r = Ext.decode(r.responseText);
                var itms = this._formatToolbar(r.object);
                var tb = this.getTopToolbar();
                if (tb) {
                    for (var i = 0; i < itms.length; i++) {
                        tb.add(itms[i]);
                    }
                    tb.doLayout();
                }
            }
            ,scope: this
        });
        config.tbar = {bodyStyle: 'padding: 0'};
    } else {
        var tb = this.getToolbar();
        if (config.tbar && config.useDefaultToolbar) {
            for (var i = 0; i < config.tbar.length; i++) {
                tb.push(config.tbar[i]);
            }
        } else if (config.tbar) {
            tb = config.tbar;
        }
        Ext.apply(config,{tbar: tb});
    }
    this.setup(config);
    this.config = config;

    this.on('append',this._onAppend,this)

};
Ext.extend(MODx.tree.Tree,Ext.tree.TreePanel,{
    menu: null
    ,options: {}
    ,disableHref: false

    ,onLoad: function (ldr,node,resp) {
        // add custom buttons to child nodes
        this.prepareNodes(node);

        // no select() here, just addClass, using Active Input Cookie Value to set focus
        var r = Ext.decode(resp.responseText);
        if (r.message) {
            var el = this.getTreeEl();
            el.addClass('modx-tree-load-msg');
            el.update(r.message);
            var w = 270;
            if (this.config.width > 150) {
                w = this.config.width;
            }
            el.setWidth(w);
            this.doLayout();
        }
    }

    /**
     * Sets up the tree and initializes it with the specified options.
     */
    ,setup: function (config) {
        config.listeners = config.listeners || {};
        config.listeners.render = {
            fn: function () {
                if (config.autoExpandRoot !== false || !config.hasOwnProperty('autoExpandRoot')) {
                    this.root.expand();
                }
                var tl = this.getLoader();
                Ext.apply(tl,{fullMask: new Ext.LoadMask(this.getEl())});
                tl.fullMask.removeMask = false;
                tl.on({
                    'load': function () {
                        this.fullMask.hide();
                    }
                    ,'loadexception': function () {
                        this.fullMask.hide();
                    }
                    ,'beforeload': function () {
                        this.fullMask.show();
                    }
                    ,scope: tl
                });
            }
            ,scope: this
        };
        MODx.tree.Tree.superclass.constructor.call(this,config);
        this.addEvents('afterSort','beforeSort');
        this.cm = new Ext.menu.Menu(config.menuConfig);
        this.on('contextmenu',this._showContextMenu,this);
        this.on('beforenodedrop',this._handleDrop,this);
        this.on('nodedragover',this._handleDrop,this);
        this.on('nodedrop',this._handleDrag,this);
        this.on('click',this._saveState,this);
        this.on('contextmenu',this._saveState,this);
        this.on('click',this._handleClick,this);

        this.treestate_id = this.config.id || Ext.id();
        this.on('load',this._initExpand,this,{single: true});
        this.on('expandnode',this._saveState,this);
        this.on('collapsenode',this._saveState,this);


        /* Absolute positionning fix  */
        this.on('expandnode',function () {
            var cnt = Ext.getCmp('modx-content');
            if (cnt) {
                cnt.doLayout();
            }
        },this);
    }

    /**
     * Expand the tree upon initialization.
     */
    ,_initExpand: function () {
        var treeState = Ext.state.Manager.get(this.treestate_id);
        if (Ext.isEmpty(treeState) && this.root) {
            this.root.expand();
            if (this.root.firstChild && this.config.expandFirst) {
                //this.root.firstChild.select();
                this.root.firstChild.expand();
            }
        } else {
            for (var i = 0; i < treeState.length; i++) {
                this.expandPath(treeState[i]);
            }
        }
    }

    /**
     * Add context menu items to the tree.
     * @param {Object, Array} items Either an Object config or array of Object configs.
     */
    ,addContextMenuItem: function (items) {
        var a = items,l = a.length;
        for (var i = 0; i < l; i++) {
            a[i].scope = a[i].scope || this;
            if (a[i].handler && typeof a[i].handler == 'string') {
                a[i].handler = eval(a[i].handler);
            }
            this.cm.add(a[i]);
        }
    }

    /**
     *
     *
     * @param node
     */
    ,prepareNodes: function(node) {
        var params = {};
        if (location.search) {
            var parts = location.search.substring(1).split('&');

            for (var i = 0; i < parts.length; i++) {
                var nv = parts[i].split('=');
                if (!nv[0]) continue;
                params[nv[0]] = nv[1] || true;
            }
        }
        var activeFile = params.file ? decodeURIComponent(params.file) : '' ;

        Ext.each(node.childNodes,function (node) {
            if (node.attributes.selected || node.id == activeFile) {
                node.ui.addClass('x-tree-selected');
            }

            // add the special buttons to node
            this.addNodeButtons(node);
        }, this);
    }

    /**
     * Adds direct access buttons to a node. Currently the only added button is
     * for directly creating a new child document.
     *
     * @param node
     */
    ,addNodeButtons: function(node) {
        var elId = node.ui.elNode.id + '_tools';
        var el = document.createElement('div');
        el.id = elId;
        el.className = 'modx-tree-node-btn-create';

        if (!node.attributes.pseudoroot && node.ui.hasClass('pnew_modDocument')) {
            node.ui.elNode.appendChild(el);

            var btn = MODx.load({
                xtype: 'modx-button',
                text: '',
                scope: this,
                tooltip: new Ext.ToolTip({
                    // TODO if childtemplate property is available, directly use that instead of "document"
                    title: _('create_document_inside')+" <strong>" + node.attributes.text + "</strong>"
                    ,target: this
                }),
                node: node,
                handler: function (btn,evt) {
                    evt.stopPropagation(evt);
                    node.getOwnerTree().handleDirectCreateClick(node);
                },
                iconCls: 'icon-plus-square',
                renderTo: elId,
                listeners: {
                    mouseover: function (button,e) {
                        button.tooltip.onTargetOver(e);
                    }
                    ,mouseout: function (button,e) {
                        button.tooltip.onTargetOut(e);
                    }
                }
            });
        }
    }

    /**
     * Shows the current context menu.
     * @param {Ext.tree.TreeNode} node The
     * @param {Ext.EventObject} e The event object run.
     */
    ,_showContextMenu: function (node,e) {
        this.cm.activeNode = node;
        this.cm.removeAll();
        var m;
        var handled = false;

        if (!Ext.isEmpty(node.attributes.treeHandler) || (node.isRoot && !Ext.isEmpty(node.childNodes[0].attributes.treeHandler))) {
            var h = Ext.getCmp(node.isRoot ? node.childNodes[0].attributes.treeHandler : node.attributes.treeHandler);
            if (h) {
                if (node.isRoot) {
                    node.attributes.type = 'root';
                }
                m = h.getMenu(this,node,e);
                handled = true;
            }
        }
        if (!handled) {
            if (this.getMenu) {
                m = this.getMenu(node,e);
            } else if (node.attributes.menu && node.attributes.menu.items) {
                m = node.attributes.menu.items;
            }
        }
        if (m && m.length > 0) {
            this.addContextMenuItem(m);
            this.cm.showAt(e.xy);
        }
        e.preventDefault();
        e.stopEvent();
    }

    /**
     * Checks to see if a node exists in a tree node's children.
     * @param {Object} t The parent node.
     * @param {Object} n The node to find.
     * @return {Boolean} True if the node exists in the parent's children.
     */
    ,hasNode: function (t,n) {
        return (t.findChild('id',n.id)) || (t.leaf === true && t.parentNode.findChild('id',n.id));
    }

    /**
     * Refreshes the tree and runs an optional func.
     * @param {Function} func The function to run.
     * @param {Object} scope The scope to run the function in.
     * @param {Array} args An array of arguments to run with.
     * @return {Boolean} True if successful.
     */
    ,refresh: function (func,scope,args) {
        var treeState = Ext.state.Manager.get(this.treestate_id);
        this.root.reload();
        if (treeState === undefined) {
            this.root.expand();
        } else {
            // Make sure we have a valid state array
            if (Ext.isArray(treeState)) {
                Ext.each(treeState,function (path,idx) {
                    this.expandPath(path);
                },this);
            }
        }
        if (func) {
            scope = scope || this;
            args = args || [];
            this.root.on('load',function () {
                Ext.callback(func,scope,args);
            },scope);
        }
        return true;
    }

    ,removeChildren: function (node) {
        while (node.firstChild) {
            var c = node.firstChild;
            node.removeChild(c);
            c.destroy();
        }
    }

    ,loadRemoteData: function (data) {
        this.removeChildren(this.getRootNode());
        for (var c in data) {
            if (typeof data[c] === 'object') {
                this.getRootNode().appendChild(data[c]);
            }
        }
    }

    ,reloadNode: function (n) {
        this.getLoader().load(n);
        n.expand();
    }

    /**
     * Abstracted remove function
     */
    ,remove: function (text,substr,split) {
        if (this.destroying) {
            return MODx.tree.Tree.superclass.remove.apply(this,arguments);
        }
        var node = this.cm.activeNode;
        var id = this._extractId(node.id,substr,split);
        var p = {action: this.config.removeAction || 'remove'};
        var pk = this.config.primaryKey || 'id';
        p[pk] = id;
        MODx.msg.confirm({
            title: this.config.removeTitle || _('warning')
            ,text: _(text)
            ,url: this.config.url
            ,params: p
            ,listeners: {
                'success': {fn: this.refresh,scope: this}
            }
        });
    }

    ,_extractId: function (id,substr,split) {
        substr = substr || false;
        split = split || false;
        if (substr !== false) {
            id = node.id.substr(substr);
        }
        if (split !== false) {
            id = node.id.split('_');
            id = id[split];
        }
        return id;
    }

    /**
     * Expand the tree and all children.
     */
    ,expandNodes: function () {
        if (this.root) {
            this.root.expand();
            this.root.expandChildNodes(true);
        }
    }

    /**
     * Completely collapse the tree.
     */
    ,collapseNodes: function () {
        if (this.root) {
            this.root.collapseChildNodes(true);
            this.root.collapse();
        }
    }

    /**
     * Save the state of the tree's open children.
     * @param {Ext.tree.TreeNode} n The most recent expanded or collapsed node.
     */
    ,_saveState: function (n) {
        if (!this.stateful) {
            return true;
        }
        var s = Ext.state.Manager.get(this.treestate_id);
        var p = n.getPath();
        var i;
        if (!Ext.isObject(s) && !Ext.isArray(s)) {
            s = [s];
            /* backwards compat */
        } else {
            s = s.slice();
        }
        if (Ext.isEmpty(p) || p == undefined) return;
        /* ignore invalid paths */
        if (n.expanded) { /* if expanding, add to state */
            if (Ext.isString(p) && s.indexOf(p) === -1) {
                var f = false;
                var sr;
                for (i = 0; i < s.length; i++) {
                    if (s[i] == undefined || typeof s[i] != 'string') {
                        s.splice(i,1);
                        continue;
                    }
                    sr = s[i].search(p);
                    if (sr !== -1 && s[sr]) { /* dont add if already in */
                        if (s[sr].length > s[i].length) {
                            f = true;
                        }
                    }
                }
                if (!f) { /* if not in, add */
                    s.push(p);
                }
            }
        } else { /* if collapsing, remove from state */
            s = s.remove(p);
            /* remove all children of node */
            for (i = 0; i < s.length; i++) {
                if (s[i] == undefined || typeof s[i] != 'string') {
                    s.splice(i,1);
                    continue;
                }
                if (s[i].search(p) !== -1) {
                    delete s[i];
                }
            }
        }
        /* clear out undefineds */
        for (i = 0; i < s.length; i++) {
            if (s[i] == undefined || typeof s[i] != 'string') {
                s.splice(i,1);
                continue;
            }
        }
        Ext.state.Manager.set(this.treestate_id,s);
    }

    /**
     * Handles tree clicks
     * @param {Object} n The node clicked
     * @param {Object} e The event object
     */
    ,_handleClick: function (n,e) {
        e.stopEvent();
        e.preventDefault();
        
        if (this.disableHref) {
            return true;
        }
        if (n.attributes.page && n.attributes.page !== '') {
            if (e.button == 1) return window.open(n.attributes.page,'_blank');
            else if (e.ctrlKey == 1 || e.metaKey == 1 || e.shiftKey == 1) return window.open(n.attributes.page);
            else if (e.target.tagName == 'SPAN') MODx.loadPage(n.attributes.page); // only open the edit page when clicking on the text and nothing else (e.g. icon/empty space)
            else if (n.isExpandable()) n.toggle(); // when clicking anything except the node-text, just open (if available) the node
            else MODx.loadPage(n.attributes.page); // for non container nodes, they can be edited by clicking anywhere on the node
        } else if (n.attributes.type && n.attributes.type === 'dir') {
            if (!n.expanded) {
                n.toggle();
            }
        } else if (n.isExpandable()) {
            n.toggle();
        }
        return true;
    }

    ,encode: function (node) {
        if (!node) {
            node = this.getRootNode();
        }
        var _encode = function (node) {
            var resultNode = {};
            var kids = node.childNodes;
            for (var i = 0; i < kids.length; i = i + 1) {
                var n = kids[i];
                resultNode[n.id] = {
                    id: n.id
                    ,checked: n.ui.isChecked()
                    ,type: n.attributes.type || ''
                    ,data: n.attributes.data || {}
                    ,children: _encode(n)
                };
            }
            return resultNode;
        };
        var nodes = _encode(node);
        return Ext.encode(nodes);
    }

    /**
     * Handles all drag events into the tree.
     * @param {Object} dropEvent The node dropped on the parent node.
     */
    ,_handleDrag: function (dropEvent) {
        function simplifyNodes(node) {
            var resultNode = {};
            var kids = node.childNodes;
            var len = kids.length;
            for (var i = 0; i < len; i++) {
                resultNode[kids[i].id] = simplifyNodes(kids[i]);
            }
            return resultNode;
        }

        var encNodes = Ext.encode(simplifyNodes(dropEvent.tree.root))
            ,source = dropEvent.dropNode;
        //var target = dropEvent.target;

        this.fireEvent('beforeSort',encNodes);
        MODx.Ajax.request({
            url: this.config.url
            ,params: {
                data: encodeURIComponent(encNodes)
                ,action: this.config.sortAction || 'sort'
                ,source_pk: source.attributes.pk
                ,source_type: source.attributes.type
            }
            ,listeners: {
                'success': {
                    fn: function (r) {
                        var el = dropEvent.dropNode.getUI().getTextEl();
                        if (el) {
                            Ext.get(el).frame();
                        }
                        this.fireEvent('afterSort',{event: dropEvent,result: r});
                    },scope: this
                }
                ,'failure': {
                    fn: function (r) {
                        MODx.form.Handler.errorJSON(r);
                        this.refresh();
                        return false;
                    },scope: this
                }
            }
        });
    }

    /**
     * Abstract definition to handle drop events.
     */
    ,_handleDrop: function (dropEvent) {
        var node = dropEvent.dropNode;
        if (node.isRoot) return false;

        if (!Ext.isEmpty(node.attributes.treeHandler)) {
            var h = Ext.getCmp(node.attributes.treeHandler);
            if (h) {
                return h.handleDrop(this,dropEvent);
            }
        }
    }

    /**
     * Semi unique ids across edits
     * @param {String} prefix Prefix the guid.
     * @return {String} The newly generated guid.
     */
    ,_guid: function (prefix) {
        return prefix + (new Date().getTime());
    }

    /**
     * Redirects the page or the content frame to the correct location.
     * @param {String} loc The URL to direct to.
     */
    ,redirect: function (loc) {
        MODx.loadPage(loc);
    }

    ,loadAction: function (p) {
        var id = '';
        if (this.cm.activeNode && this.cm.activeNode.id) {
            var pid = this.cm.activeNode.id.split('_');
            id = 'id=' + pid[1];
        }
        MODx.loadPage('?' + id + '&' + p);
    }
    /**
     * Loads the default toolbar for the tree.
     * @access private
     * @see Ext.Toolbar
     */
    ,_loadToolbar: function () {
    }

    /**
     * Refreshes a given tree node.
     * @access public
     * @param {String} id The ID of the node
     * @param {Boolean} self If true, will refresh self rather than parent.
     */
    ,refreshNode: function (id,self) {
        var node = this.getNodeById(id);
        if (node) {
            var n = self ? node : node.parentNode;
            this.getLoader().load(n,function () {
                n.expand();
            },this);
        }
    }

    /**
     * Refreshes selected active node
     * @access public
     */
    ,refreshActiveNode: function () {
        if (this.cm.activeNode) {
            this.getLoader().load(this.cm.activeNode,this.cm.activeNode.expand);
        } else {
            this.refresh();
        }
    }

    /**
     * Refreshes selected active node's parent
     * @access public
     */
    ,refreshParentNode: function () {
        if (this.cm.activeNode) {
            this.getLoader().load(this.cm.activeNode.parentNode || this.cm.activeNode, this.cm.activeNode.expand);
        } else {
            this.refresh();
        }
    }

    /**
     * Removes specified node
     * @param {String} id The node's ID
     */
    ,removeNode: function (id) {
        var node = this.getNodeById(id);
        if (node) {
            node.remove();
        }
    }

    /**
     * Dynamically removes active node
     */
    ,removeActiveNode: function () {
        this.cm.activeNode.remove();
    }

    /**
     * Gets a default toolbar setup
     */
    ,getToolbar: function () {
        var iu = MODx.config.manager_url + 'templates/default/images/restyle/icons/';
        return [{
            icon: iu + 'arrow_down.png'
            ,cls: 'x-btn-icon arrow_down'
            ,tooltip: {text: _('tree_expand')}
            ,handler: this.expandNodes
            ,scope: this
        },{
            icon: iu + 'arrow_up.png'
            ,cls: 'x-btn-icon arrow_up'
            ,tooltip: {text: _('tree_collapse')}
            ,handler: this.collapseNodes
            ,scope: this
        },{
            icon: iu + 'refresh.png'
            ,cls: 'x-btn-icon refresh'
            ,tooltip: {text: _('tree_refresh')}
            ,handler: this.refresh
            ,scope: this
        }];
    }

    /**
     * Add Items to the toolbar.
     */
    ,_formatToolbar: function (a) {
        var l = a.length;
        for (var i = 0; i < l; i++) {
            if (a[i].handler) {
                a[i].handler = eval(a[i].handler);
            }
            Ext.applyIf(a[i],{
                scope: this
                ,cls: this.config.toolbarItemCls || 'x-btn-icon'
            });
        }
        return a;
    }

    /**
     * Allow pseudoroot actions
     * @param tree {self}
     * @param parent {Ext.tree.TreeNode} Parent node
     * @param node {Ext.tree.TreeNode} Node to be inserted
     */
    ,_onAppend: function (tree,parent,node) {
        if (node.attributes.pseudoroot) {

            setTimeout((function (tree) {
                return function () {

                    var elId = node.ui.elNode.id + '_tools';
                    var el = document.createElement('div');
                    el.id = elId;
                    el.className = 'modx-tree-node-tool-ct';

                    node.ui.elNode.appendChild(el);

                    var inlineButtonsLang = tree.getInlineButtonsLang(node);

                    var btn = MODx.load({
                        xtype: 'modx-button',
                        text: '',
                        scope: this,
                        tooltip: new Ext.ToolTip({
                            title: inlineButtonsLang.add
                            ,target: this
                        }),
                        node: node,
                        handler: function (btn,evt) {
                            evt.stopPropagation(evt);
                            node.getOwnerTree().handleCreateClick(node);
                        },
                        iconCls: 'icon-plus-circle',
                        renderTo: elId,
                        listeners: {
                            mouseover: function (button,e) {
                                button.tooltip.onTargetOver(e);
                            }
                            ,mouseout: function (button,e) {
                                button.tooltip.onTargetOut(e);
                            }
                        }
                    });

                    var btn = MODx.load({
                        xtype: 'modx-button',
                        text: '',
                        scope: this,
                        tooltip: new Ext.ToolTip({
                            title: inlineButtonsLang.refresh
                            ,target: this
                        }),
                        node: node,
                        handler: function (btn,evt) {
                            evt.stopPropagation(evt);
                            node.reload();
                        },
                        iconCls: 'icon-refresh',
                        renderTo: elId,
                        listeners: {
                            mouseover: function (button,e) {
                                button.tooltip.onTargetOver(e);
                            }
                            ,mouseout: function (button,e) {
                                button.tooltip.onTargetOut(e);
                            }
                        }
                    });


                    window.BTNS.push(btn);


                }
            }(this)),200);

            return false;

            var btn = document.createElement('div');
            btn.innerHTML = 'H';

            node.el.appendChild(btn);
        }
    }

    /**
     * Handled inline add button click
     * Need to be extended in MODx.tree.Tree instances to work properly
     *
     * @param Ext.tree.AsyncTreeNode node
     */
    ,handleCreateClick: function (node) {
    }

    ,getInlineButtonsLang: function (node) {
        var langs = {};
        if (node.id != undefined) {
            var type = node.id.substr(2).split('_');
            if (type[0] == 'type') {
                langs.add = _('new_' + type[1]);
            } else if (type[0] == 'category') {
                langs.add = _('new_' + type[0]);
            } else {
                langs.add = _('new_document');
            }
        }

        langs.refresh = _('ext_refresh');
        return langs;
    }

});
Ext.reg('modx-tree',MODx.tree.Tree);


window.BTNS = [];