The source code

    /*!
     * Ext JS Library 3.2.1
     * Copyright(c) 2006-2010 Ext JS, Inc.
     * licensing@extjs.com
     * http://www.extjs.com/license
     */
    /**
     * @class Ext.data.GroupingStore
     * @extends Ext.data.Store
     * A specialized store implementation that provides for grouping records by one of the available fields. This
     * is usually used in conjunction with an {@link Ext.grid.GroupingView} to provide the data model for
     * a grouped GridPanel.
     *
     * Internally, GroupingStore is simply a normal Store with multi sorting enabled from the start. The grouping field
     * and direction are always injected as the first sorter pair. GroupingView picks up on the configured groupField and
     * builds grid rows appropriately.
     *
     * @constructor
     * Creates a new GroupingStore.
     * @param {Object} config A config object containing the objects needed for the Store to access data,
     * and read the data into Records.
     * @xtype groupingstore
     */
    Ext.data.GroupingStore = Ext.extend(Ext.data.Store, {

        //inherit docs
        constructor: function(config) {
            config = config || {};

            //We do some preprocessing here to massage the grouping + sorting options into a single
            //multi sort array. If grouping and sorting options are both presented to the constructor,
            //the sorters array consists of the grouping sorter object followed by the sorting sorter object
            //see Ext.data.Store's sorting functions for details about how multi sorting works
            this.hasMultiSort  = true;
            this.multiSortInfo = this.multiSortInfo || {sorters: []};

            var sorters    = this.multiSortInfo.sorters,
                groupField = config.groupField || this.groupField,
                sortInfo   = config.sortInfo || this.sortInfo,
                groupDir   = config.groupDir || this.groupDir;

            //add the grouping sorter object first
            if(groupField){
                sorters.push({
                    field    : groupField,
                    direction: groupDir
                });
            }

            //add the sorting sorter object if it is present
            if (sortInfo) {
                sorters.push(sortInfo);
            }

            Ext.data.GroupingStore.superclass.constructor.call(this, config);

            this.addEvents(
              /**
               * @event groupchange
               * Fired whenever a call to store.groupBy successfully changes the grouping on the store
               * @param {Ext.data.GroupingStore} store The grouping store
               * @param {String} groupField The field that the store is now grouped by
               */
              'groupchange'
            );

            this.applyGroupField();
        },

        /**
         * @cfg {String} groupField
         * The field name by which to sort the store's data (defaults to '').
         */
        /**
         * @cfg {Boolean} remoteGroup
         * True if the grouping should apply on the server side, false if it is local only (defaults to false).  If the
         * grouping is local, it can be applied immediately to the data.  If it is remote, then it will simply act as a
         * helper, automatically sending the grouping field name as the 'groupBy' param with each XHR call.
         */
        remoteGroup : false,
        /**
         * @cfg {Boolean} groupOnSort
         * True to sort the data on the grouping field when a grouping operation occurs, false to sort based on the
         * existing sort info (defaults to false).
         */
        groupOnSort:false,

        groupDir : 'ASC',

        /**
         * Clears any existing grouping and refreshes the data using the default sort.
         */
        clearGrouping : function(){
            this.groupField = false;

            if(this.remoteGroup){
                if(this.baseParams){
                    delete this.baseParams.groupBy;
                    delete this.baseParams.groupDir;
                }
                var lo = this.lastOptions;
                if(lo && lo.params){
                    delete lo.params.groupBy;
                    delete lo.params.groupDir;
                }

                this.reload();
            }else{
                this.sort();
                this.fireEvent('datachanged', this);
            }
        },

        /**
         * Groups the data by the specified field.
         * @param {String} field The field name by which to sort the store's data
         * @param {Boolean} forceRegroup (optional) True to force the group to be refreshed even if the field passed
         * in is the same as the current grouping field, false to skip grouping on the same field (defaults to false)
         */
        groupBy : function(field, forceRegroup, direction) {
            direction = direction ? (String(direction).toUpperCase() == 'DESC' ? 'DESC' : 'ASC') : this.groupDir;

            if (this.groupField == field && this.groupDir == direction && !forceRegroup) {
                return; // already grouped by this field
            }

            //check the contents of the first sorter. If the field matches the CURRENT groupField (before it is set to the new one),
            //remove the sorter as it is actually the grouper. The new grouper is added back in by this.sort
            sorters = this.multiSortInfo.sorters;
            if (sorters.length > 0 && sorters[0].field == this.groupField) {
                sorters.shift();
            }

            this.groupField = field;
            this.groupDir = direction;
            this.applyGroupField();

            var fireGroupEvent = function() {
                this.fireEvent('groupchange', this, this.getGroupState());
            };

            if (this.groupOnSort) {
                this.sort(field, direction);
                fireGroupEvent.call(this);
                return;
            }

            if (this.remoteGroup) {
                this.on('load', fireGroupEvent, this, {single: true});
                this.reload();
            } else {
                this.sort(sorters);
                fireGroupEvent.call(this);
            }
        },

        //GroupingStore always uses multisorting so we intercept calls to sort here to make sure that our grouping sorter object
        //is always injected first.
        sort : function(fieldName, dir) {
            if (this.remoteSort) {
                return Ext.data.GroupingStore.superclass.sort.call(this, fieldName, dir);
            }

            var sorters = [];

            //cater for any existing valid arguments to this.sort, massage them into an array of sorter objects
            if (Ext.isArray(arguments[0])) {
                sorters = arguments[0];
            } else if (fieldName == undefined) {
                //we preserve the existing sortInfo here because this.sort is called after
                //clearGrouping and there may be existing sorting
                sorters = this.sortInfo ? [this.sortInfo] : [];
            } else {
                //TODO: this is lifted straight from Ext.data.Store's singleSort function. It should instead be
                //refactored into a common method if possible
                var field = this.fields.get(fieldName);
                if (!field) return false;

                var name       = field.name,
                    sortInfo   = this.sortInfo || null,
                    sortToggle = this.sortToggle ? this.sortToggle[name] : null;

                if (!dir) {
                    if (sortInfo && sortInfo.field == name) { // toggle sort dir
                        dir = (this.sortToggle[name] || 'ASC').toggle('ASC', 'DESC');
                    } else {
                        dir = field.sortDir;
                    }
                }

                this.sortToggle[name] = dir;
                this.sortInfo = {field: name, direction: dir};

                sorters = [this.sortInfo];
            }

            //add the grouping sorter object as the first multisort sorter
            if (this.groupField) {
                sorters.unshift({direction: this.groupDir, field: this.groupField});
            }

            return this.multiSort.call(this, sorters, dir);
        },

        /**
         * @private
         * Saves the current grouping field and direction to this.baseParams and this.lastOptions.params
         * if we're using remote grouping. Does not actually perform any grouping - just stores values
         */
        applyGroupField: function(){
            if (this.remoteGroup) {
                if(!this.baseParams){
                    this.baseParams = {};
                }

                Ext.apply(this.baseParams, {
                    groupBy : this.groupField,
                    groupDir: this.groupDir
                });

                var lo = this.lastOptions;
                if (lo && lo.params) {
                    lo.params.groupDir = this.groupDir;

                    //this is deleted because of a bug reported at http://www.extjs.com/forum/showthread.php?t=82907
                    delete lo.params.groupBy;
                }
            }
        },

        /**
         * @private
         * TODO: This function is apparently never invoked anywhere in the framework. It has no documentation
         * and should be considered for deletion
         */
        applyGrouping : function(alwaysFireChange){
            if(this.groupField !== false){
                this.groupBy(this.groupField, true, this.groupDir);
                return true;
            }else{
                if(alwaysFireChange === true){
                    this.fireEvent('datachanged', this);
                }
                return false;
            }
        },

        /**
         * @private
         * Returns the grouping field that should be used. If groupOnSort is used this will be sortInfo's field,
         * otherwise it will be this.groupField
         * @return {String} The group field
         */
        getGroupState : function(){
            return this.groupOnSort && this.groupField !== false ?
                   (this.sortInfo ? this.sortInfo.field : undefined) : this.groupField;
        }
    });
    Ext.reg('groupingstore', Ext.data.GroupingStore);