Daily Archives: October 22, 2013

Implementing an Editable Auto-Sorting Ext JS 4.2 Tree

taxonomy

Ext JS 4 has some fantastic components, although it’s not always clear as to how to configure a particular component to achieve a desired result.

Recently we were tasked with implementing a custom metadata/taxonomy editing GUI for a customer and decided to create an editable, drag-and-drop tree control to accomplish this task.

Here’s the code, which was cobbled together from various blog posts, forum messages, and a little bit of ingenuity on my part. Note that it was developed using Sencha Architect 2.x, which is, in itself, noteworthy:

Here’s the code for the Tree Panel:

Ext.define('MyApp.view.admin.taxonomy.Taxonomy', {
    extend: 'Ext.tree.Panel',
    alias: 'widget.edittaxonomy',

    requires: [
        'MyApp.view.override.admin.taxonomy.Taxonomy',
        'Ext.grid.plugin.CellEditing'
    ],
    plugins: [
       Ext.create('Ext.grid.plugin.CellEditing', {clicksToEdit:2})
    ]

    height: 500,
    width: 200,
    autoScroll: true,
    header: false,
    title: 'Taxonomy',
    hideHeaders: true,
    store: 'Taxonomy',

    initComponent: function() {
        var me = this;

        Ext.applyIf(me, {
            viewConfig: {
                toggleOnDblClick: false,
                plugins: [
                    Ext.create('Ext.tree.plugin.TreeViewDragDrop', {
                        appendOnly: true,
                        nodeHighlightOnDrop: true,
                        nodeHighlightOnRepair: true
                    })
                ],
                listeners: {
                    drop: {
                        fn: me.onTreeViewDragDropDrop,
                        scope: me
                    }
                }
            },
            dockedItems: [
                {
                    xtype: 'toolbar',
                    dock: 'top',
                    items: [
                        {
                            xtype: 'button',
                            handler: function(button, event) {
                                var tree = this.up('treepanel');
                                var selModel = tree.getSelectionModel();

                                // Could also use:
                                // var node = selModel.getSelection()[0];
                                var node = selModel.getLastSelected();

                                if (!node) {
                                    node = tree.getStore().getRootNode();
                                }

                                // Feels like this should happen automatically
                                node.set('leaf', false);

                                node.appendChild({
                                    leaf: true,
                                    text: 'New Child'
                                });

                                // Not strictly required but...
                                node.expand();
                            },
                            flex: 1,
                            text: 'Add Term'
                        },
                        {
                            xtype: 'button',
                            handler: function(button, event) {
                                var tree = this.up('treepanel');
                                var selModel = tree.getSelectionModel();

                                // Could also use:
                                // var node = selModel.getSelection()[0];
                                var node = selModel.getLastSelected();
                                node.destroy();
                            },
                            flex: 1,
                            text: 'Del Term'
                        }
                    ]
                },
                {
                    xtype: 'toolbar',
                    dock: 'bottom',
                    items: [
                        {
                            xtype: 'button',
                            handler: function(button, event) {
                                Ext.getStore('Taxonomy').sync();
                            },
                            flex: 1,
                            text: 'Save'
                        }
                    ]
                }
            ],
            columns: [
                {
                    xtype: 'treecolumn',
                    dataIndex: 'text',
                    text: 'Categories/Keywords',
                    flex: 1,
                    editor: {
                        xtype: 'textfield',
                        allowBlank: false,
                        allowOnlyWhitespace: false,
                        maxLength: 25
                    }
                }
            ],
            listeners: {
                beforerender: {
                    fn: me.onTreepanelBeforeRender,
                    scope: me
                }
            }
        });

        me.callParent(arguments);
    },

    onTreeViewDragDropDrop: function(node, data, overModel, dropPosition, eOpts) {

        this.getStore().sortChildren(overModel);

    },

    onTreepanelBeforeRender: function(component, eOpts) {
        component.getStore().load();
    }

});

And here’s the code for the Store:

Ext.define('MyApp.store.Taxonomy', {
    extend: 'Ext.data.TreeStore',

    constructor: function(cfg) {
        var me = this;
        cfg = cfg || {};
        me.callParent([Ext.apply({
            lazyFill: true,
            proxy: {
                type: 'ajax',
                url: 'resources/sampledata/taxonomy.json'
            },
            fields: [
                {
                    name: 'text'
                },
                {
                    name: 'bitmask'
                }
            ],
            listeners: {
                update: {
                    fn: me.onTreeStoreUpdate,
                    scope: me
                }
            }
        }, cfg)]);
    },

    onTreeStoreUpdate: function(store, record, operation, modifiedFieldNames, eOpts) {

        if (modifiedFieldNames && modifiedFieldNames[0] === 'text') {
            this.sortChildren(record.parentNode);
        }
    },

    sortChildren: function(parentNode) {
        parentNode.sort(function(n1, n2) {
            n1 = n1.get('text');
            n2 = n2.get('text');
            if (n1 < n2) {
                return -1;
            } else if (n1 === n2) {
                return 0;
            }
            return 1;
        });
    }
});

And here’s the sample data:

[{
    "text": "Category 1", 
    "id" : 1,
    "cls": "folder",
    "children": [{
        "text": "Keyword 1",
        "leaf": true,
         "id" : 2,
    },{
        "text": "Keyword 2",
        "leaf": true,
         "id" : 3,
    },{
        "text": "Keyword 3",
        "leaf": true,
        "id" : 4,
    }]
},{
    "text": "Category 2",
    "cls": "folder",
    "children": [{
        "text": "Keyword A",
        "leaf": true,
        "id" : 5,
    },{
        "text": "Keyword B",
        "leaf": true,
        "id" : 6,
    },{
        "text": "Keyword C",
        "leaf": true,
        "id" : 7
    }]
}]