MDL-35616 import YUI 3.7.2
[moodle.git] / lib / yuilib / 3.7.2 / build / datatable-sort / datatable-sort-debug.js
blobf791d643b8a5f4a6bd264fd344d2fcc5b128e745
1 /*
2 YUI 3.7.2 (build 5639)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('datatable-sort', function (Y, NAME) {
9 /**
10 Adds support for sorting the table data by API methods `table.sort(...)` or
11 `table.toggleSort(...)` or by clicking on column headers in the rendered UI.
13 @module datatable
14 @submodule datatable-sort
15 @since 3.5.0
16 **/
17 var YLang     = Y.Lang,
18     isBoolean = YLang.isBoolean,
19     isString  = YLang.isString,
20     isArray   = YLang.isArray,
21     isObject  = YLang.isObject,
23     toArray = Y.Array,
24     sub     = YLang.sub,
26     dirMap = {
27         asc : 1,
28         desc: -1,
29         "1" : 1,
30         "-1": -1
31     };
34 /**
35 _API docs for this extension are included in the DataTable class._
37 This DataTable class extension adds support for sorting the table data by API
38 methods `table.sort(...)` or `table.toggleSort(...)` or by clicking on column
39 headers in the rendered UI.
41 Sorting by the API is enabled automatically when this module is `use()`d.  To
42 enable UI triggered sorting, set the DataTable's `sortable` attribute to
43 `true`.
45 <pre><code>
46 var table = new Y.DataTable({
47     columns: [ 'id', 'username', 'name', 'birthdate' ],
48     data: [ ... ],
49     sortable: true
50 });
52 table.render('#table');
53 </code></pre>
55 Setting `sortable` to `true` will enable UI sorting for all columns.  To enable
56 UI sorting for certain columns only, set `sortable` to an array of column keys,
57 or just add `sortable: true` to the respective column configuration objects.
58 This uses the default setting of `sortable: auto` for the DataTable instance.
60 <pre><code>
61 var table = new Y.DataTable({
62     columns: [
63         'id',
64         { key: 'username',  sortable: true },
65         { key: 'name',      sortable: true },
66         { key: 'birthdate', sortable: true }
67     ],
68     data: [ ... ]
69     // sortable: 'auto' is the default
70 });
72 // OR
73 var table = new Y.DataTable({
74     columns: [ 'id', 'username', 'name', 'birthdate' ],
75     data: [ ... ],
76     sortable: [ 'username', 'name', 'birthdate' ]
77 });
78 </code></pre>
80 To disable UI sorting for all columns, set `sortable` to `false`.  This still
81 permits sorting via the API methods.
83 As new records are inserted into the table's `data` ModelList, they will be inserted at the correct index to preserve the sort order.
85 The current sort order is stored in the `sortBy` attribute.  Assigning this value at instantiation will automatically sort your data.
87 Sorting is done by a simple value comparison using &lt; and &gt; on the field
88 value.  If you need custom sorting, add a sort function in the column's
89 `sortFn` property.  Columns whose content is generated by formatters, but don't
90 relate to a single `key`, require a `sortFn` to be sortable.
92 <pre><code>
93 function nameSort(a, b, desc) {
94     var aa = a.get('lastName') + a.get('firstName'),
95         bb = a.get('lastName') + b.get('firstName'),
96         order = (aa > bb) ? 1 : -(aa < bb);
97         
98     return desc ? -order : order;
101 var table = new Y.DataTable({
102     columns: [ 'id', 'username', { key: name, sortFn: nameSort }, 'birthdate' ],
103     data: [ ... ],
104     sortable: [ 'username', 'name', 'birthdate' ]
106 </code></pre>
108 See the user guide for more details.
110 @class DataTable.Sortable
111 @for DataTable
112 @since 3.5.0
114 function Sortable() {}
116 Sortable.ATTRS = {
117     // Which columns in the UI should suggest and respond to sorting interaction
118     // pass an empty array if no UI columns should show sortable, but you want the
119     // table.sort(...) API
120     /**
121     Controls which column headers can trigger sorting by user clicks.
123     Acceptable values are:
125      * "auto" - (default) looks for `sortable: true` in the column configurations
126      * `true` - all columns are enabled
127      * `false - no UI sortable is enabled
128      * {String[]} - array of key names to give sortable headers
130     @attribute sortable
131     @type {String|String[]|Boolean}
132     @default "auto"
133     @since 3.5.0
134     **/
135     sortable: {
136         value: 'auto',
137         validator: '_validateSortable'
138     },
140     /**
141     The current sort configuration to maintain in the data.
143     Accepts column `key` strings or objects with a single property, the column
144     `key`, with a value of 1, -1, "asc", or "desc".  E.g. `{ username: 'asc'
145     }`.  String values are assumed to be ascending.
147     Example values would be:
149      * `"username"` - sort by the data's `username` field or the `key`
150        associated to a column with that `name`.
151      * `{ username: "desc" }` - sort by `username` in descending order.
152        Alternately, use values "asc", 1 (same as "asc"), or -1 (same as "desc").
153      * `["lastName", "firstName"]` - ascending sort by `lastName`, but for
154        records with the same `lastName`, ascending subsort by `firstName`.
155        Array can have as many items as you want.
156      * `[{ lastName: -1 }, "firstName"]` - descending sort by `lastName`,
157        ascending subsort by `firstName`. Mixed types are ok.
159     @attribute sortBy
160     @type {String|String[]|Object|Object[]}
161     @since 3.5.0
162     **/
163     sortBy: {
164         validator: '_validateSortBy',
165         getter: '_getSortBy'
166     },
168     /**
169     Strings containing language for sorting tooltips.
171     @attribute strings
172     @type {Object}
173     @default (strings for current lang configured in the YUI instance config)
174     @since 3.5.0
175     **/
176     strings: {}
179 Y.mix(Sortable.prototype, {
181     /**
182     Sort the data in the `data` ModelList and refresh the table with the new
183     order.
185     Acceptable values for `fields` are `key` strings or objects with a single
186     property, the column `key`, with a value of 1, -1, "asc", or "desc".  E.g.
187     `{ username: 'asc' }`.  String values are assumed to be ascending.
189     Example values would be:
191      * `"username"` - sort by the data's `username` field or the `key`
192        associated to a column with that `name`.
193      * `{ username: "desc" }` - sort by `username` in descending order.
194        Alternately, use values "asc", 1 (same as "asc"), or -1 (same as "desc").
195      * `["lastName", "firstName"]` - ascending sort by `lastName`, but for
196        records with the same `lastName`, ascending subsort by `firstName`.
197        Array can have as many items as you want.
198      * `[{ lastName: -1 }, "firstName"]` - descending sort by `lastName`,
199        ascending subsort by `firstName`. Mixed types are ok.
201     @method sort
202     @param {String|String[]|Object|Object[]} fields The field(s) to sort by
203     @param {Object} [payload] Extra `sort` event payload you want to send along
204     @return {DataTable}
205     @chainable
206     @since 3.5.0
207     **/
208     sort: function (fields, payload) {
209         /**
210         Notifies of an impending sort, either from clicking on a column
211         header, or from a call to the `sort` or `toggleSort` method.
213         The requested sort is available in the `sortBy` property of the event.
215         The default behavior of this event sets the table's `sortBy` attribute.
217         @event sort
218         @param {String|String[]|Object|Object[]} sortBy The requested sort
219         @preventable _defSortFn
220         **/
221         return this.fire('sort', Y.merge((payload || {}), {
222             sortBy: fields || this.get('sortBy')
223         }));
224     },
226     /**
227     Template for the node that will wrap the header content for sortable
228     columns.
230     @property SORTABLE_HEADER_TEMPLATE
231     @type {HTML}
232     @value '<div class="{className}" tabindex="0"><span class="{indicatorClass}"></span></div>'
233     @since 3.5.0
234     **/
235     SORTABLE_HEADER_TEMPLATE: '<div class="{className}" tabindex="0"><span class="{indicatorClass}"></span></div>',
237     /**
238     Reverse the current sort direction of one or more fields currently being
239     sorted by.
241     Pass the `key` of the column or columns you want the sort order reversed
242     for.
244     @method toggleSort
245     @param {String|String[]} fields The field(s) to reverse sort order for
246     @param {Object} [payload] Extra `sort` event payload you want to send along
247     @return {DataTable}
248     @chainable
249     @since 3.5.0
250     **/
251     toggleSort: function (columns, payload) {
252         var current = this._sortBy,
253             sortBy = [],
254             i, len, j, col, index;
256         // To avoid updating column configs or sortBy directly
257         for (i = 0, len = current.length; i < len; ++i) {
258             col = {};
259             col[current[i]._id] = current[i].sortDir;
260             sortBy.push(col);
261         }
263         if (columns) {
264             columns = toArray(columns);
266             for (i = 0, len = columns.length; i < len; ++i) {
267                 col = columns[i];
268                 index = -1;
270                 for (j = sortBy.length - 1; i >= 0; --i) {
271                     if (sortBy[j][col]) {
272                         sortBy[j][col] *= -1;
273                         break;
274                     }
275                 }
276             }
277         } else {
278             for (i = 0, len = sortBy.length; i < len; ++i) {
279                 for (col in sortBy[i]) {
280                     if (sortBy[i].hasOwnProperty(col)) {
281                         sortBy[i][col] *= -1;
282                         break;
283                     }
284                 }
285             }
286         }
288         return this.fire('sort', Y.merge((payload || {}), {
289             sortBy: sortBy
290         }));
291     },
293     //--------------------------------------------------------------------------
294     // Protected properties and methods
295     //--------------------------------------------------------------------------
296     /**
297     Sorts the `data` ModelList based on the new `sortBy` configuration.
299     @method _afterSortByChange
300     @param {EventFacade} e The `sortByChange` event
301     @protected
302     @since 3.5.0
303     **/
304     _afterSortByChange: function (e) {
305         // Can't use a setter because it's a chicken and egg problem. The
306         // columns need to be set up to translate, but columns are initialized
307         // from Core's initializer.  So construction-time assignment would
308         // fail.
309         this._setSortBy();
311         // Don't sort unless sortBy has been set
312         if (this._sortBy.length) {
313             if (!this.data.comparator) {
314                  this.data.comparator = this._sortComparator;
315             }
317             this.data.sort();
318         }
319     },
321     /**
322     Applies the sorting logic to the new ModelList if the `newVal` is a new
323     ModelList.
325     @method _afterSortDataChange
326     @param {EventFacade} e the `dataChange` event
327     @protected
328     @since 3.5.0
329     **/
330     _afterSortDataChange: function (e) {
331         // object values always trigger a change event, but we only want to
332         // call _initSortFn if the value passed to the `data` attribute was a
333         // new ModelList, not a set of new data as an array, or even the same
334         // ModelList.
335         if (e.prevVal !== e.newVal || e.newVal.hasOwnProperty('_compare')) {
336             this._initSortFn();
337         }
338     },
340     /**
341     Checks if any of the fields in the modified record are fields that are
342     currently being sorted by, and if so, resorts the `data` ModelList.
344     @method _afterSortRecordChange
345     @param {EventFacade} e The Model's `change` event
346     @protected
347     @since 3.5.0
348     **/
349     _afterSortRecordChange: function (e) {
350         var i, len;
352         for (i = 0, len = this._sortBy.length; i < len; ++i) {
353             if (e.changed[this._sortBy[i].key]) {
354                 this.data.sort();
355                 break;
356             }
357         }
358     },
360     /**
361     Subscribes to state changes that warrant updating the UI, and adds the
362     click handler for triggering the sort operation from the UI.
364     @method _bindSortUI
365     @protected
366     @since 3.5.0
367     **/
368     _bindSortUI: function () {
369         var handles = this._eventHandles;
370         
371         if (!handles.sortAttrs) {
372             handles.sortAttrs = this.after(
373                 ['sortableChange', 'sortByChange', 'columnsChange'],
374                 Y.bind('_uiSetSortable', this));
375         }
377         if (!handles.sortUITrigger && this._theadNode) {
378             handles.sortUITrigger = this.delegate(['click','keydown'],
379                 Y.rbind('_onUITriggerSort', this),
380                 '.' + this.getClassName('sortable', 'column'));
381         }
382     },
384     /**
385     Sets the `sortBy` attribute from the `sort` event's `e.sortBy` value.
387     @method _defSortFn
388     @param {EventFacade} e The `sort` event
389     @protected
390     @since 3.5.0
391     **/
392     _defSortFn: function (e) {
393         this.set.apply(this, ['sortBy', e.sortBy].concat(e.details));
394     },
396     /**
397     Getter for the `sortBy` attribute.
398     
399     Supports the special subattribute "sortBy.state" to get a normalized JSON
400     version of the current sort state.  Otherwise, returns the last assigned
401     value.
403     For example:
405     <pre><code>var table = new Y.DataTable({
406         columns: [ ... ],
407         data: [ ... ],
408         sortBy: 'username'
409     });
411     table.get('sortBy'); // 'username'
412     table.get('sortBy.state'); // { key: 'username', dir: 1 }
414     table.sort(['lastName', { firstName: "desc" }]);
415     table.get('sortBy'); // ['lastName', { firstName: "desc" }]
416     table.get('sortBy.state'); // [{ key: "lastName", dir: 1 }, { key: "firstName", dir: -1 }]
417     </code></pre>
419     @method _getSortBy
420     @param {String|String[]|Object|Object[]} val The current sortBy value
421     @param {String} detail String passed to `get(HERE)`. to parse subattributes
422     @protected
423     @since 3.5.0
424     **/
425     _getSortBy: function (val, detail) {
426         var state, i, len, col;
428         // "sortBy." is 7 characters. Used to catch 
429         detail = detail.slice(7);
431         // TODO: table.get('sortBy.asObject')? table.get('sortBy.json')?
432         if (detail === 'state') {
433             state = [];
435             for (i = 0, len = this._sortBy.length; i < len; ++i) {
436                 col = this._sortBy[i];
437                 state.push({
438                     column: col._id,
439                     dir: col.sortDir
440                 });
441             }
443             // TODO: Always return an array?
444             return { state: (state.length === 1) ? state[0] : state };
445         } else {
446             return val;
447         }
448     },
450     /**
451     Sets up the initial sort state and instance properties.  Publishes events
452     and subscribes to attribute change events to maintain internal state.
454     @method initializer
455     @protected
456     @since 3.5.0
457     **/
458     initializer: function () {
459         var boundParseSortable = Y.bind('_parseSortable', this);
461         this._parseSortable();
463         this._setSortBy();
465         this._initSortFn();
467         this._initSortStrings();
469         this.after({
470             'table:renderHeader': Y.bind('_renderSortable', this),
471             dataChange          : Y.bind('_afterSortDataChange', this),
472             sortByChange        : Y.bind('_afterSortByChange', this),
473             sortableChange      : boundParseSortable,
474             columnsChange       : boundParseSortable
475         });
476         this.data.after(this.data.model.NAME + ":change",
477             Y.bind('_afterSortRecordChange', this));
479         // TODO: this event needs magic, allowing async remote sorting
480         this.publish('sort', {
481             defaultFn: Y.bind('_defSortFn', this)
482         });
483     },
485     /**
486     Creates a `_compare` function for the `data` ModelList to allow custom
487     sorting by multiple fields.
489     @method _initSortFn
490     @protected
491     @since 3.5.0
492     **/
493     _initSortFn: function () {
494         var self = this;
496         // TODO: This should be a ModelList extension.
497         // FIXME: Modifying a component of the host seems a little smelly
498         // FIXME: Declaring inline override to leverage closure vs
499         // compiling a new function for each column/sortable change or
500         // binding the _compare implementation to this, resulting in an
501         // extra function hop during sorting. Lesser of three evils?
502         this.data._compare = function (a, b) {
503             var cmp = 0,
504                 i, len, col, dir, aa, bb;
506             for (i = 0, len = self._sortBy.length; !cmp && i < len; ++i) {
507                 col = self._sortBy[i];
508                 dir = col.sortDir;
510                 if (col.sortFn) {
511                     cmp = col.sortFn(a, b, (dir === -1));
512                 } else {
513                     // FIXME? Requires columns without sortFns to have key
514                     aa = a.get(col.key) || '';
515                     bb = b.get(col.key) || '';
517                     cmp = (aa > bb) ? dir : ((aa < bb) ? -dir : 0);
518                 }
519             }
521             return cmp;
522         };
524         if (this._sortBy.length) {
525             this.data.comparator = this._sortComparator;
527             // TODO: is this necessary? Should it be elsewhere?
528             this.data.sort();
529         } else {
530             // Leave the _compare method in place to avoid having to set it
531             // up again.  Mistake?
532             delete this.data.comparator;
533         }
534     },
536     /**
537     Add the sort related strings to the `strings` map.
538     
539     @method _initSortStrings
540     @protected
541     @since 3.5.0
542     **/
543     _initSortStrings: function () {
544         // Not a valueFn because other class extensions will want to add to it
545         this.set('strings', Y.mix((this.get('strings') || {}), 
546             Y.Intl.get('datatable-sort')));
547     },
549     /**
550     Fires the `sort` event in response to user clicks on sortable column
551     headers.
553     @method _onUITriggerSort
554     @param {DOMEventFacade} e The `click` event
555     @protected
556     @since 3.5.0
557     **/
558     _onUITriggerSort: function (e) {
559         var id = e.currentTarget.getAttribute('data-yui3-col-id'),
560             sortBy = e.shiftKey ? this.get('sortBy') : [{}],
561             column = id && this.getColumn(id),
562             i, len;
564         if (e.type === 'keydown' && e.keyCode !== 32) {
565             return;
566         }
568         // In case a headerTemplate injected a link
569         // TODO: Is this overreaching?
570         e.preventDefault();
572         if (column) {
573             if (e.shiftKey) {
574                 for (i = 0, len = sortBy.length; i < len; ++i) {
575                     if (id === sortBy[i] || Math.abs(sortBy[i][id] === 1)) {
576                         if (!isObject(sortBy[i])) {
577                             sortBy[i] = {};
578                         }
580                         sortBy[i][id] = -(column.sortDir|0) || 1;
581                         break;
582                     }
583                 }
585                 if (i >= len) {
586                     sortBy.push(column._id);
587                 }
588             } else {
589                 sortBy[0][id] = -(column.sortDir|0) || 1;
590             }
592             this.fire('sort', {
593                 originEvent: e,
594                 sortBy: sortBy
595             });
596         }
597     },
599     /**
600     Normalizes the possible input values for the `sortable` attribute, storing
601     the results in the `_sortable` property.
603     @method _parseSortable
604     @protected
605     @since 3.5.0
606     **/
607     _parseSortable: function () {
608         var sortable = this.get('sortable'),
609             columns  = [],
610             i, len, col;
612         if (isArray(sortable)) {
613             for (i = 0, len = sortable.length; i < len; ++i) {
614                 col = sortable[i];
616                 // isArray is called because arrays are objects, but will rely
617                 // on getColumn to nullify them for the subsequent if (col)
618                 if (!isObject(col, true) || isArray(col)) {
619                     col = this.getColumn(col);
620                 }
622                 if (col) {
623                     columns.push(col);
624                 }
625             }
626         } else if (sortable) {
627             columns = this._displayColumns.slice();
629             if (sortable === 'auto') {
630                 for (i = columns.length - 1; i >= 0; --i) {
631                     if (!columns[i].sortable) {
632                         columns.splice(i, 1);
633                     }
634                 }
635             }
636         }
638         this._sortable = columns;
639     },
641     /**
642     Initial application of the sortable UI.
644     @method _renderSortable
645     @protected
646     @since 3.5.0
647     **/
648     _renderSortable: function () {
649         this._uiSetSortable();
651         this._bindSortUI();
652     },
654     /**
655     Parses the current `sortBy` attribute into a normalized structure for the
656     `data` ModelList's `_compare` method.  Also updates the column
657     configurations' `sortDir` properties.
659     @method _setSortBy
660     @protected
661     @since 3.5.0
662     **/
663     _setSortBy: function () {
664         var columns     = this._displayColumns,
665             sortBy      = this.get('sortBy') || [],
666             sortedClass = ' ' + this.getClassName('sorted'),
667             i, len, name, dir, field, column;
669         this._sortBy = [];
671         // Purge current sort state from column configs
672         for (i = 0, len = columns.length; i < len; ++i) {
673             column = columns[i];
675             delete column.sortDir;
677             if (column.className) {
678                 // TODO: be more thorough
679                 column.className = column.className.replace(sortedClass, '');
680             }
681         }
683         sortBy = toArray(sortBy);
685         for (i = 0, len = sortBy.length; i < len; ++i) {
686             name = sortBy[i];
687             dir  = 1;
689             if (isObject(name)) {
690                 field = name;
691                 // Have to use a for-in loop to process sort({ foo: -1 })
692                 for (name in field) {
693                     if (field.hasOwnProperty(name)) {
694                         dir = dirMap[field[name]];
695                         break;
696                     }
697                 }
698             }
700             if (name) {
701                 // Allow sorting of any model field and any column
702                 // FIXME: this isn't limited to model attributes, but there's no
703                 // convenient way to get a list of the attributes for a Model
704                 // subclass *including* the attributes of its superclasses.
705                 column = this.getColumn(name) || { _id: name, key: name };
707                 if (column) {
708                     column.sortDir = dir;
710                     if (!column.className) {
711                         column.className = '';
712                     }
714                     column.className += sortedClass;
716                     this._sortBy.push(column);
717                 }
718             }
719         }
720     },
722     /**
723     Array of column configuration objects of those columns that need UI setup
724     for user interaction.
726     @property _sortable
727     @type {Object[]}
728     @protected
729     @since 3.5.0
730     **/
731     //_sortable: null,
733     /**
734     Array of column configuration objects for those columns that are currently
735     being used to sort the data.  Fake column objects are used for fields that
736     are not rendered as columns.
738     @property _sortBy
739     @type {Object[]}
740     @protected
741     @since 3.5.0
742     **/
743     //_sortBy: null,
745     /**
746     Replacement `comparator` for the `data` ModelList that defers sorting logic
747     to the `_compare` method.  The deferral is accomplished by returning `this`.
749     @method _sortComparator
750     @param {Model} item The record being evaluated for sort position
751     @return {Model} The record
752     @protected
753     @since 3.5.0
754     **/
755     _sortComparator: function (item) {
756         // Defer sorting to ModelList's _compare
757         return item;
758     },
760     /**
761     Applies the appropriate classes to the `boundingBox` and column headers to
762     indicate sort state and sortability.
764     Also currently wraps the header content of sortable columns in a `<div>`
765     liner to give a CSS anchor for sort indicators.
767     @method _uiSetSortable
768     @protected
769     @since 3.5.0
770     **/
771     _uiSetSortable: function () {
772         var columns       = this._sortable || [],
773             sortableClass = this.getClassName('sortable', 'column'),
774             ascClass      = this.getClassName('sorted'),
775             descClass     = this.getClassName('sorted', 'desc'),
776             linerClass    = this.getClassName('sort', 'liner'),
777             indicatorClass= this.getClassName('sort', 'indicator'),
778             sortableCols  = {},
779             i, len, col, node, liner, title, desc;
781         this.get('boundingBox').toggleClass(
782             this.getClassName('sortable'),
783             columns.length);
785         for (i = 0, len = columns.length; i < len; ++i) {
786             sortableCols[columns[i].id] = columns[i];
787         }
789         // TODO: this.head.render() + decorate cells?
790         this._theadNode.all('.' + sortableClass).each(function (node) {
791             var col       = sortableCols[node.get('id')],
792                 liner     = node.one('.' + linerClass),
793                 indicator;
795             if (col) {
796                 if (!col.sortDir) {
797                     node.removeClass(ascClass)
798                         .removeClass(descClass);
799                 }
800             } else {
801                 node.removeClass(sortableClass)
802                     .removeClass(ascClass)
803                     .removeClass(descClass);
805                 if (liner) {
806                     liner.replace(liner.get('childNodes').toFrag());
807                 }
809                 indicator = node.one('.' + indicatorClass);
811                 if (indicator) {
812                     indicator.remove().destroy(true);
813                 }
814             }
815         });
817         for (i = 0, len = columns.length; i < len; ++i) {
818             col  = columns[i];
819             node = this._theadNode.one('#' + col.id);
820             desc = col.sortDir === -1;
822             if (node) {
823                 liner = node.one('.' + linerClass);
825                 node.addClass(sortableClass);
827                 if (col.sortDir) {
828                     node.addClass(ascClass);
830                     node.toggleClass(descClass, desc);
832                     node.setAttribute('aria-sort', desc ?
833                         'descending' : 'ascending');
834                 }
836                 if (!liner) {
837                     liner = Y.Node.create(Y.Lang.sub(
838                         this.SORTABLE_HEADER_TEMPLATE, {
839                             className: linerClass,
840                             indicatorClass: indicatorClass
841                         }));
843                     liner.prepend(node.get('childNodes').toFrag());
845                     node.append(liner);
846                 }
848                 title = sub(this.getString(
849                     (col.sortDir === 1) ? 'reverseSortBy' : 'sortBy'), {
850                         column: col.abbr || col.label ||
851                                 col.key  || ('column ' + i)
852                 });
854                 node.setAttribute('title', title);
855                 // To combat VoiceOver from reading the sort title as the
856                 // column header
857                 node.setAttribute('aria-labelledby', col.id);
858             }
859         }
860     },
862     /**
863     Allows values `true`, `false`, "auto", or arrays of column names through.
865     @method _validateSortable
866     @param {Any} val The input value to `set("sortable", VAL)`
867     @return {Boolean}
868     @protected
869     @since 3.5.0
870     **/
871     _validateSortable: function (val) {
872         return val === 'auto' || isBoolean(val) || isArray(val);
873     },
875     /**
876     Allows strings, arrays of strings, objects, or arrays of objects.
878     @method _validateSortBy
879     @param {String|String[]|Object|Object[]} val The new `sortBy` value
880     @return {Boolean}
881     @protected
882     @since 3.5.0
883     **/
884     _validateSortBy: function (val) {
885         return val === null ||
886                isString(val) ||
887                isObject(val, true) ||
888                (isArray(val) && (isString(val[0]) || isObject(val, true)));
889     }
891 }, true);
893 Y.DataTable.Sortable = Sortable;
895 Y.Base.mix(Y.DataTable, [Sortable]);
898 }, '3.7.2', {"requires": ["datatable-base"], "lang": ["en"], "skinnable": true});