Version 3.18.1
Show:

File: datatable/js/sort.js

            /**
            Adds support for sorting the table data by API methods `table.sort(...)` or
            `table.toggleSort(...)` or by clicking on column headers in the rendered UI.
            
            @module datatable
            @submodule datatable-sort
            @since 3.5.0
            **/
            var YLang     = Y.Lang,
                isBoolean = YLang.isBoolean,
                isString  = YLang.isString,
                isArray   = YLang.isArray,
                isObject  = YLang.isObject,
            
                toArray = Y.Array,
                sub     = YLang.sub,
            
                dirMap = {
                    asc : 1,
                    desc: -1,
                    "1" : 1,
                    "-1": -1
                };
            
            
            /**
            _API docs for this extension are included in the DataTable class._
            
            This DataTable class extension adds support for sorting the table data by API
            methods `table.sort(...)` or `table.toggleSort(...)` or by clicking on column
            headers in the rendered UI.
            
            Sorting by the API is enabled automatically when this module is `use()`d.  To
            enable UI triggered sorting, set the DataTable's `sortable` attribute to
            `true`.
            
            <pre><code>
            var table = new Y.DataTable({
                columns: [ 'id', 'username', 'name', 'birthdate' ],
                data: [ ... ],
                sortable: true
            });
            
            table.render('#table');
            </code></pre>
            
            Setting `sortable` to `true` will enable UI sorting for all columns.  To enable
            UI sorting for certain columns only, set `sortable` to an array of column keys,
            or just add `sortable: true` to the respective column configuration objects.
            This uses the default setting of `sortable: auto` for the DataTable instance.
            
            <pre><code>
            var table = new Y.DataTable({
                columns: [
                    'id',
                    { key: 'username',  sortable: true },
                    { key: 'name',      sortable: true },
                    { key: 'birthdate', sortable: true }
                ],
                data: [ ... ]
                // sortable: 'auto' is the default
            });
            
            // OR
            var table = new Y.DataTable({
                columns: [ 'id', 'username', 'name', 'birthdate' ],
                data: [ ... ],
                sortable: [ 'username', 'name', 'birthdate' ]
            });
            </code></pre>
            
            To disable UI sorting for all columns, set `sortable` to `false`.  This still
            permits sorting via the API methods.
            
            As new records are inserted into the table's `data` ModelList, they will be inserted at the correct index to preserve the sort order.
            
            The current sort order is stored in the `sortBy` attribute.  Assigning this value at instantiation will automatically sort your data.
            
            Sorting is done by a simple value comparison using &lt; and &gt; on the field
            value.  If you need custom sorting, add a sort function in the column's
            `sortFn` property.  Columns whose content is generated by formatters, but don't
            relate to a single `key`, require a `sortFn` to be sortable.
            
            <pre><code>
            function nameSort(a, b, desc) {
                var aa = a.get('lastName') + a.get('firstName'),
                    bb = a.get('lastName') + b.get('firstName'),
                    order = (aa > bb) ? 1 : -(aa < bb);
            
                return desc ? -order : order;
            }
            
            var table = new Y.DataTable({
                columns: [ 'id', 'username', { key: name, sortFn: nameSort }, 'birthdate' ],
                data: [ ... ],
                sortable: [ 'username', 'name', 'birthdate' ]
            });
            </code></pre>
            
            See the user guide for more details.
            
            @class DataTable.Sortable
            @for DataTable
            @since 3.5.0
            **/
            function Sortable() {}
            
            Sortable.ATTRS = {
                // Which columns in the UI should suggest and respond to sorting interaction
                // pass an empty array if no UI columns should show sortable, but you want the
                // table.sort(...) API
                /**
                Controls which column headers can trigger sorting by user clicks.
            
                Acceptable values are:
            
                 * "auto" - (default) looks for `sortable: true` in the column configurations
                 * `true` - all columns are enabled
                 * `false - no UI sortable is enabled
                 * {String[]} - array of key names to give sortable headers
            
                @attribute sortable
                @type {String|String[]|Boolean}
                @default "auto"
                @since 3.5.0
                **/
                sortable: {
                    value: 'auto',
                    validator: '_validateSortable'
                },
            
                /**
                The current sort configuration to maintain in the data.
            
                Accepts column `key` strings or objects with a single property, the column
                `key`, with a value of 1, -1, "asc", or "desc".  E.g. `{ username: 'asc'
                }`.  String values are assumed to be ascending.
            
                Example values would be:
            
                 * `"username"` - sort by the data's `username` field or the `key`
                   associated to a column with that `name`.
                 * `{ username: "desc" }` - sort by `username` in descending order.
                   Alternately, use values "asc", 1 (same as "asc"), or -1 (same as "desc").
                 * `["lastName", "firstName"]` - ascending sort by `lastName`, but for
                   records with the same `lastName`, ascending subsort by `firstName`.
                   Array can have as many items as you want.
                 * `[{ lastName: -1 }, "firstName"]` - descending sort by `lastName`,
                   ascending subsort by `firstName`. Mixed types are ok.
            
                @attribute sortBy
                @type {String|String[]|Object|Object[]}
                @since 3.5.0
                **/
                sortBy: {
                    validator: '_validateSortBy',
                    getter: '_getSortBy'
                },
            
                /**
                Strings containing language for sorting tooltips.
            
                @attribute strings
                @type {Object}
                @default (strings for current lang configured in the YUI instance config)
                @since 3.5.0
                **/
                strings: {}
            };
            
            Y.mix(Sortable.prototype, {
            
                /**
                Sort the data in the `data` ModelList and refresh the table with the new
                order.
            
                Acceptable values for `fields` are `key` strings or objects with a single
                property, the column `key`, with a value of 1, -1, "asc", or "desc".  E.g.
                `{ username: 'asc' }`.  String values are assumed to be ascending.
            
                Example values would be:
            
                 * `"username"` - sort by the data's `username` field or the `key`
                   associated to a column with that `name`.
                 * `{ username: "desc" }` - sort by `username` in descending order.
                   Alternately, use values "asc", 1 (same as "asc"), or -1 (same as "desc").
                 * `["lastName", "firstName"]` - ascending sort by `lastName`, but for
                   records with the same `lastName`, ascending subsort by `firstName`.
                   Array can have as many items as you want.
                 * `[{ lastName: -1 }, "firstName"]` - descending sort by `lastName`,
                   ascending subsort by `firstName`. Mixed types are ok.
            
                @method sort
                @param {String|String[]|Object|Object[]} fields The field(s) to sort by
                @param {Object} [payload] Extra `sort` event payload you want to send along
                @return {DataTable}
                @chainable
                @since 3.5.0
                **/
                sort: function (fields, payload) {
                    /**
                    Notifies of an impending sort, either from clicking on a column
                    header, or from a call to the `sort` or `toggleSort` method.
            
                    The requested sort is available in the `sortBy` property of the event.
            
                    The default behavior of this event sets the table's `sortBy` attribute.
            
                    @event sort
                    @param {String|String[]|Object|Object[]} sortBy The requested sort
                    @preventable _defSortFn
                    **/
                    return this.fire('sort', Y.merge((payload || {}), {
                        sortBy: fields || this.get('sortBy')
                    }));
                },
            
                /**
                Template for the node that will wrap the header content for sortable
                columns.
            
                @property SORTABLE_HEADER_TEMPLATE
                @type {String}
                @value '<div class="{className}" tabindex="0"><span class="{indicatorClass}"></span></div>'
                @since 3.5.0
                **/
                SORTABLE_HEADER_TEMPLATE: '<div class="{className}" tabindex="0" unselectable="on"><span class="{indicatorClass}"></span></div>',
            
                /**
                Reverse the current sort direction of one or more fields currently being
                sorted by.
            
                Pass the `key` of the column or columns you want the sort order reversed
                for.
            
                @method toggleSort
                @param {String|String[]} fields The field(s) to reverse sort order for
                @param {Object} [payload] Extra `sort` event payload you want to send along
                @return {DataTable}
                @chainable
                @since 3.5.0
                **/
                toggleSort: function (columns, payload) {
                    var current = this._sortBy,
                        sortBy = [],
                        i, len, j, col, index;
            
                    // To avoid updating column configs or sortBy directly
                    for (i = 0, len = current.length; i < len; ++i) {
                        col = {};
                        col[current[i]._id] = current[i].sortDir;
                        sortBy.push(col);
                    }
            
                    if (columns) {
                        columns = toArray(columns);
            
                        for (i = 0, len = columns.length; i < len; ++i) {
                            col = columns[i];
                            index = -1;
            
                            for (j = sortBy.length - 1; i >= 0; --i) {
                                if (sortBy[j][col]) {
                                    sortBy[j][col] *= -1;
                                    break;
                                }
                            }
                        }
                    } else {
                        for (i = 0, len = sortBy.length; i < len; ++i) {
                            for (col in sortBy[i]) {
                                if (sortBy[i].hasOwnProperty(col)) {
                                    sortBy[i][col] *= -1;
                                    break;
                                }
                            }
                        }
                    }
            
                    return this.fire('sort', Y.merge((payload || {}), {
                        sortBy: sortBy
                    }));
                },
            
                //--------------------------------------------------------------------------
                // Protected properties and methods
                //--------------------------------------------------------------------------
                /**
                Sorts the `data` ModelList based on the new `sortBy` configuration.
            
                @method _afterSortByChange
                @param {EventFacade} e The `sortByChange` event
                @protected
                @since 3.5.0
                **/
                _afterSortByChange: function () {
                    // Can't use a setter because it's a chicken and egg problem. The
                    // columns need to be set up to translate, but columns are initialized
                    // from Core's initializer.  So construction-time assignment would
                    // fail.
                    this._setSortBy();
            
                    // Don't sort unless sortBy has been set
                    if (this._sortBy.length) {
                        if (!this.data.comparator) {
                             this.data.comparator = this._sortComparator;
                        }
            
                        this.data.sort();
                    }
                },
            
                /**
                Applies the sorting logic to the new ModelList if the `newVal` is a new
                ModelList.
            
                @method _afterSortDataChange
                @param {EventFacade} e the `dataChange` event
                @protected
                @since 3.5.0
                **/
                _afterSortDataChange: function (e) {
                    // object values always trigger a change event, but we only want to
                    // call _initSortFn if the value passed to the `data` attribute was a
                    // new ModelList, not a set of new data as an array, or even the same
                    // ModelList.
                    if (e.prevVal !== e.newVal || e.newVal.hasOwnProperty('_compare')) {
                        this._initSortFn();
                    }
                },
            
                /**
                Checks if any of the fields in the modified record are fields that are
                currently being sorted by, and if so, resorts the `data` ModelList.
            
                @method _afterSortRecordChange
                @param {EventFacade} e The Model's `change` event
                @protected
                @since 3.5.0
                **/
                _afterSortRecordChange: function (e) {
                    var i, len;
            
                    for (i = 0, len = this._sortBy.length; i < len; ++i) {
                        if (e.changed[this._sortBy[i].key]) {
                            this.data.sort();
                            break;
                        }
                    }
                },
            
                /**
                Subscribes to state changes that warrant updating the UI, and adds the
                click handler for triggering the sort operation from the UI.
            
                @method _bindSortUI
                @protected
                @since 3.5.0
                **/
                _bindSortUI: function () {
                    var handles = this._eventHandles;
            
                    if (!handles.sortAttrs) {
                        handles.sortAttrs = this.after(
                            ['sortableChange', 'sortByChange', 'columnsChange'],
                            Y.bind('_uiSetSortable', this));
                    }
            
                    if (!handles.sortUITrigger && this._theadNode) {
                        handles.sortUITrigger = this.delegate(['click','keydown'],
                            Y.rbind('_onUITriggerSort', this),
                            '.' + this.getClassName('sortable', 'column'));
                    }
                },
            
                /**
                Sets the `sortBy` attribute from the `sort` event's `e.sortBy` value.
            
                @method _defSortFn
                @param {EventFacade} e The `sort` event
                @protected
                @since 3.5.0
                **/
                _defSortFn: function (e) {
                    this.set.apply(this, ['sortBy', e.sortBy].concat(e.details));
                },
            
                /**
                Getter for the `sortBy` attribute.
            
                Supports the special subattribute "sortBy.state" to get a normalized JSON
                version of the current sort state.  Otherwise, returns the last assigned
                value.
            
                For example:
            
                <pre><code>var table = new Y.DataTable({
                    columns: [ ... ],
                    data: [ ... ],
                    sortBy: 'username'
                });
            
                table.get('sortBy'); // 'username'
                table.get('sortBy.state'); // { key: 'username', dir: 1 }
            
                table.sort(['lastName', { firstName: "desc" }]);
                table.get('sortBy'); // ['lastName', { firstName: "desc" }]
                table.get('sortBy.state'); // [{ key: "lastName", dir: 1 }, { key: "firstName", dir: -1 }]
                </code></pre>
            
                @method _getSortBy
                @param {String|String[]|Object|Object[]} val The current sortBy value
                @param {String} detail String passed to `get(HERE)`. to parse subattributes
                @protected
                @since 3.5.0
                **/
                _getSortBy: function (val, detail) {
                    var state, i, len, col;
            
                    // "sortBy." is 7 characters. Used to catch
                    detail = detail.slice(7);
            
                    // TODO: table.get('sortBy.asObject')? table.get('sortBy.json')?
                    if (detail === 'state') {
                        state = [];
            
                        for (i = 0, len = this._sortBy.length; i < len; ++i) {
                            col = this._sortBy[i];
                            state.push({
                                column: col._id,
                                dir: col.sortDir
                            });
                        }
            
                        // TODO: Always return an array?
                        return { state: (state.length === 1) ? state[0] : state };
                    } else {
                        return val;
                    }
                },
            
                /**
                Sets up the initial sort state and instance properties.  Publishes events
                and subscribes to attribute change events to maintain internal state.
            
                @method initializer
                @protected
                @since 3.5.0
                **/
                initializer: function () {
                    var boundParseSortable = Y.bind('_parseSortable', this);
            
                    this._parseSortable();
            
                    this._setSortBy();
            
                    this._initSortFn();
            
                    this._initSortStrings();
            
                    this.after({
                        'table:renderHeader': Y.bind('_renderSortable', this),
                        dataChange          : Y.bind('_afterSortDataChange', this),
                        sortByChange        : Y.bind('_afterSortByChange', this),
                        sortableChange      : boundParseSortable,
                        columnsChange       : boundParseSortable
                    });
                    this.data.after(this.data.model.NAME + ":change",
                        Y.bind('_afterSortRecordChange', this));
            
                    // TODO: this event needs magic, allowing async remote sorting
                    this.publish('sort', {
                        defaultFn: Y.bind('_defSortFn', this)
                    });
                },
            
                /**
                Creates a `_compare` function for the `data` ModelList to allow custom
                sorting by multiple fields.
            
                @method _initSortFn
                @protected
                @since 3.5.0
                **/
                _initSortFn: function () {
                    var self = this;
            
                    // TODO: This should be a ModelList extension.
                    // FIXME: Modifying a component of the host seems a little smelly
                    // FIXME: Declaring inline override to leverage closure vs
                    // compiling a new function for each column/sortable change or
                    // binding the _compare implementation to this, resulting in an
                    // extra function hop during sorting. Lesser of three evils?
                    this.data._compare = function (a, b) {
                        var cmp = 0,
                            i, len, col, dir, cs, aa, bb;
            
                        for (i = 0, len = self._sortBy.length; !cmp && i < len; ++i) {
                            col = self._sortBy[i];
                            dir = col.sortDir,
                            cs = col.caseSensitive;
            
                            if (col.sortFn) {
                                cmp = col.sortFn(a, b, (dir === -1));
                            } else {
                                // FIXME? Requires columns without sortFns to have key
                                aa = a.get(col.key) || '';
                                bb = b.get(col.key) || '';
                                if (!cs && typeof(aa) === "string" && typeof(bb) === "string"){// Not case sensitive
                                    aa = aa.toLowerCase();
                                    bb = bb.toLowerCase();
                                }
                                cmp = (aa > bb) ? dir : ((aa < bb) ? -dir : 0);
                            }
                        }
            
                        return cmp;
                    };
            
                    if (this._sortBy.length) {
                        this.data.comparator = this._sortComparator;
            
                        // TODO: is this necessary? Should it be elsewhere?
                        this.data.sort();
                    } else {
                        // Leave the _compare method in place to avoid having to set it
                        // up again.  Mistake?
                        delete this.data.comparator;
                    }
                },
            
                /**
                Add the sort related strings to the `strings` map.
            
                @method _initSortStrings
                @protected
                @since 3.5.0
                **/
                _initSortStrings: function () {
                    // Not a valueFn because other class extensions will want to add to it
                    this.set('strings', Y.mix((this.get('strings') || {}),
                        Y.Intl.get('datatable-sort')));
                },
            
                /**
                Fires the `sort` event in response to user clicks on sortable column
                headers.
            
                @method _onUITriggerSort
                @param {DOMEventFacade} e The `click` event
                @protected
                @since 3.5.0
                **/
                _onUITriggerSort: function (e) {
                    var id = e.currentTarget.getAttribute('data-yui3-col-id'),
                        column = id && this.getColumn(id),
                        sortBy, i, len;
            
                    if (e.type === 'keydown' && e.keyCode !== 32) {
                        return;
                    }
            
                    // In case a headerTemplate injected a link
                    // TODO: Is this overreaching?
                    e.preventDefault();
            
                    if (column) {
                        if (e.shiftKey) {
                            sortBy = this.get('sortBy') || [];
            
                            for (i = 0, len = sortBy.length; i < len; ++i) {
                                if (id === sortBy[i]  || Math.abs(sortBy[i][id]) === 1) {
                                    if (!isObject(sortBy[i])) {
                                        sortBy[i] = {};
                                    }
            
                                    sortBy[i][id] = -(column.sortDir||0) || 1;
                                    break;
                                }
                            }
            
                            if (i >= len) {
                                sortBy.push(column._id);
                            }
                        } else {
                            sortBy = [{}];
            
                            sortBy[0][id] = -(column.sortDir||0) || 1;
                        }
            
                        this.fire('sort', {
                            originEvent: e,
                            sortBy: sortBy
                        });
                    }
                },
            
                /**
                Normalizes the possible input values for the `sortable` attribute, storing
                the results in the `_sortable` property.
            
                @method _parseSortable
                @protected
                @since 3.5.0
                **/
                _parseSortable: function () {
                    var sortable = this.get('sortable'),
                        columns  = [],
                        i, len, col;
            
                    if (isArray(sortable)) {
                        for (i = 0, len = sortable.length; i < len; ++i) {
                            col = sortable[i];
            
                            // isArray is called because arrays are objects, but will rely
                            // on getColumn to nullify them for the subsequent if (col)
                            if (!isObject(col, true) || isArray(col)) {
                                col = this.getColumn(col);
                            }
            
                            if (col) {
                                columns.push(col);
                            }
                        }
                    } else if (sortable) {
                        columns = this._displayColumns.slice();
            
                        if (sortable === 'auto') {
                            for (i = columns.length - 1; i >= 0; --i) {
                                if (!columns[i].sortable) {
                                    columns.splice(i, 1);
                                }
                            }
                        }
                    }
            
                    this._sortable = columns;
                },
            
                /**
                Initial application of the sortable UI.
            
                @method _renderSortable
                @protected
                @since 3.5.0
                **/
                _renderSortable: function () {
                    this._uiSetSortable();
            
                    this._bindSortUI();
                },
            
                /**
                Parses the current `sortBy` attribute into a normalized structure for the
                `data` ModelList's `_compare` method.  Also updates the column
                configurations' `sortDir` properties.
            
                @method _setSortBy
                @protected
                @since 3.5.0
                **/
                _setSortBy: function () {
                    var columns     = this._displayColumns,
                        sortBy      = this.get('sortBy') || [],
                        sortedClass = ' ' + this.getClassName('sorted'),
                        i, len, name, dir, field, column;
            
                    this._sortBy = [];
            
                    // Purge current sort state from column configs
                    for (i = 0, len = columns.length; i < len; ++i) {
                        column = columns[i];
            
                        delete column.sortDir;
            
                        if (column.className) {
                            // TODO: be more thorough
                            column.className = column.className.replace(sortedClass, '');
                        }
                    }
            
                    sortBy = toArray(sortBy);
            
                    for (i = 0, len = sortBy.length; i < len; ++i) {
                        name = sortBy[i];
                        dir  = 1;
            
                        if (isObject(name)) {
                            field = name;
                            // Have to use a for-in loop to process sort({ foo: -1 })
                            for (name in field) {
                                if (field.hasOwnProperty(name)) {
                                    dir = dirMap[field[name]];
                                    break;
                                }
                            }
                        }
            
                        if (name) {
                            // Allow sorting of any model field and any column
                            // FIXME: this isn't limited to model attributes, but there's no
                            // convenient way to get a list of the attributes for a Model
                            // subclass *including* the attributes of its superclasses.
                            column = this.getColumn(name) || { _id: name, key: name };
            
                            if (column) {
                                column.sortDir = dir;
            
                                if (!column.className) {
                                    column.className = '';
                                }
            
                                column.className += sortedClass;
            
                                this._sortBy.push(column);
                            }
                        }
                    }
                },
            
                /**
                Array of column configuration objects of those columns that need UI setup
                for user interaction.
            
                @property _sortable
                @type {Object[]}
                @protected
                @since 3.5.0
                **/
                //_sortable: null,
            
                /**
                Array of column configuration objects for those columns that are currently
                being used to sort the data.  Fake column objects are used for fields that
                are not rendered as columns.
            
                @property _sortBy
                @type {Object[]}
                @protected
                @since 3.5.0
                **/
                //_sortBy: null,
            
                /**
                Replacement `comparator` for the `data` ModelList that defers sorting logic
                to the `_compare` method.  The deferral is accomplished by returning `this`.
            
                @method _sortComparator
                @param {Model} item The record being evaluated for sort position
                @return {Model} The record
                @protected
                @since 3.5.0
                **/
                _sortComparator: function (item) {
                    // Defer sorting to ModelList's _compare
                    return item;
                },
            
                /**
                Applies the appropriate classes to the `boundingBox` and column headers to
                indicate sort state and sortability.
            
                Also currently wraps the header content of sortable columns in a `<div>`
                liner to give a CSS anchor for sort indicators.
            
                @method _uiSetSortable
                @protected
                @since 3.5.0
                **/
                _uiSetSortable: function () {
                    var columns       = this._sortable || [],
                        sortableClass = this.getClassName('sortable', 'column'),
                        ascClass      = this.getClassName('sorted'),
                        descClass     = this.getClassName('sorted', 'desc'),
                        linerClass    = this.getClassName('sort', 'liner'),
                        indicatorClass= this.getClassName('sort', 'indicator'),
                        sortableCols  = {},
                        i, len, col, node, liner, title, desc;
            
                    this.get('boundingBox').toggleClass(
                        this.getClassName('sortable'),
                        columns.length);
            
                    for (i = 0, len = columns.length; i < len; ++i) {
                        sortableCols[columns[i].id] = columns[i];
                    }
            
                    // TODO: this.head.render() + decorate cells?
                    this._theadNode.all('.' + sortableClass).each(function (node) {
                        var col       = sortableCols[node.get('id')],
                            liner     = node.one('.' + linerClass),
                            indicator;
            
                        if (col) {
                            if (!col.sortDir) {
                                node.removeClass(ascClass)
                                    .removeClass(descClass);
                            }
                        } else {
                            node.removeClass(sortableClass)
                                .removeClass(ascClass)
                                .removeClass(descClass);
            
                            if (liner) {
                                liner.replace(liner.get('childNodes').toFrag());
                            }
            
                            indicator = node.one('.' + indicatorClass);
            
                            if (indicator) {
                                indicator.remove().destroy(true);
                            }
                        }
                    });
            
                    for (i = 0, len = columns.length; i < len; ++i) {
                        col  = columns[i];
                        node = this._theadNode.one('#' + col.id);
                        desc = col.sortDir === -1;
            
                        if (node) {
                            liner = node.one('.' + linerClass);
            
                            node.addClass(sortableClass);
            
                            if (col.sortDir) {
                                node.addClass(ascClass);
            
                                node.toggleClass(descClass, desc);
            
                                node.setAttribute('aria-sort', desc ?
                                    'descending' : 'ascending');
                            }
            
                            if (!liner) {
                                liner = Y.Node.create(Y.Lang.sub(
                                    this.SORTABLE_HEADER_TEMPLATE, {
                                        className: linerClass,
                                        indicatorClass: indicatorClass
                                    }));
            
                                liner.prepend(node.get('childNodes').toFrag());
            
                                node.append(liner);
                            }
            
                            title = sub(this.getString(
                                (col.sortDir === 1) ? 'reverseSortBy' : 'sortBy'), // get string
                                {
                                    title:  col.title || '',
                                    key:    col.key || '',
                                    abbr:   col.abbr || '',
                                    label:  col.label || '',
                                    column: col.abbr || col.label ||
                                            col.key  || ('column ' + i)
                                }
                            );
            
                            node.setAttribute('title', title);
                            // To combat VoiceOver from reading the sort title as the
                            // column header
                            node.setAttribute('aria-labelledby', col.id);
                        }
                    }
                },
            
                /**
                Allows values `true`, `false`, "auto", or arrays of column names through.
            
                @method _validateSortable
                @param {Any} val The input value to `set("sortable", VAL)`
                @return {Boolean}
                @protected
                @since 3.5.0
                **/
                _validateSortable: function (val) {
                    return val === 'auto' || isBoolean(val) || isArray(val);
                },
            
                /**
                Allows strings, arrays of strings, objects, or arrays of objects.
            
                @method _validateSortBy
                @param {String|String[]|Object|Object[]} val The new `sortBy` value
                @return {Boolean}
                @protected
                @since 3.5.0
                **/
                _validateSortBy: function (val) {
                    return val === null ||
                           isString(val) ||
                           isObject(val, true) ||
                           (isArray(val) && (isString(val[0]) || isObject(val, true)));
                }
            
            }, true);
            
            Y.DataTable.Sortable = Sortable;
            /**
            Used when the instance's `sortable` attribute is set to
            "auto" (the default) to determine which columns will support
            user sorting by clicking on the header.
            
            If the instance's `key` attribute is not set, this
            configuration is ignored.
            
                { key: 'lastLogin', sortable: true }
            
            @property sortable
            @type Boolean
            @for DataTable.Column
             */
            /**
            When the instance's `caseSensitive` attribute is set to
            `true` the sort order is case sensitive (relevant to string columns only).
            
            Case sensitive sort is marginally more efficient and should be considered
            for large data sets when case insensitive sort is not required.
            
                { key: 'lastLogin', sortable: true, caseSensitive: true }
            
            @property caseSensitive
            @type Boolean
            @for DataTable.Column
             */
            /**
            Allows a column to be sorted using a custom algorithm.  The
            function receives three parameters, the first two being the
            two record Models to compare, and the third being a boolean
            `true` if the sort order should be descending.
            
            The function should return `1` to sort `a` above `b`, `-1`
            to sort `a` below `b`, and `0` if they are equal.  Keep in
            mind that the order should be reversed when `desc` is
            `true`.
            
            The `desc` parameter is provided to allow `sortFn`s to
            always sort certain values above or below others, such as
            always sorting `null`s on top.
            
                {
                  label: 'Name',
                  sortFn: function (a, b, desc) {
                    var an = a.get('lname') + b.get('fname'),
                        bn = a.get('lname') + b.get('fname'),
                        order = (an > bn) ? 1 : -(an < bn);
            
                    return desc ? -order : order;
                  },
                  formatter: function (o) {
                    return o.data.lname + ', ' + o.data.fname;
                  }
                }
            
            @property sortFn
            @type Function
            @for DataTable.Column
            */
            /**
            (__read-only__) If a column is sorted, this
            will be set to `1` for ascending order or `-1` for
            descending.  This configuration is public for inspection,
            but can't be used during DataTable instantiation to set the
            sort direction of the column.  Use the table's
            [sortBy](DataTable.html#attr_sortBy)
            attribute for that.
            
            @property sortDir
            @type {Number}
            @readOnly
            @for DataTable.Column
            */
            
            Y.Base.mix(Y.DataTable, [Sortable]);