Merge branch 'MDL-32509' of git://github.com/danpoltawski/moodle
[moodle.git] / lib / yui / 3.5.0 / build / datatable-sort / datatable-sort.js
blobf70f8c5d19ce72304fd7ec331513554b444985bb
1 /*
2 YUI 3.5.0 (build 5089)
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) {
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         this.after(['sortableChange', 'sortByChange', 'columnsChange'],
370             Y.bind('_uiSetSortable', this));
372         if (this._theadNode) {
373             this._sortHandle = this.delegate(['click','keydown'],
374                 Y.rbind('_onUITriggerSort', this),
375                 '.' + this.getClassName('sortable', 'column'));
376         }
377     },
379     /**
380     Sets the `sortBy` attribute from the `sort` event's `e.sortBy` value.
382     @method _defSortFn
383     @param {EventFacade} e The `sort` event
384     @protected
385     @since 3.5.0
386     **/
387     _defSortFn: function (e) {
388         this.set.apply(this, ['sortBy', e.sortBy].concat(e.details));
389     },
391     /**
392     Removes the click subscription from the header for sorting.
394     @method destructor
395     @protected
396     @since 3.5.0
397     **/
398     destructor: function () {
399         if (this._sortHandle) {
400             this._sortHandle.detach();
401         }
402     },
404     /**
405     Getter for the `sortBy` attribute.
406     
407     Supports the special subattribute "sortBy.state" to get a normalized JSON
408     version of the current sort state.  Otherwise, returns the last assigned
409     value.
411     For example:
413     <pre><code>var table = new Y.DataTable({
414         columns: [ ... ],
415         data: [ ... ],
416         sortBy: 'username'
417     });
419     table.get('sortBy'); // 'username'
420     table.get('sortBy.state'); // { key: 'username', dir: 1 }
422     table.sort(['lastName', { firstName: "desc" }]);
423     table.get('sortBy'); // ['lastName', { firstName: "desc" }]
424     table.get('sortBy.state'); // [{ key: "lastName", dir: 1 }, { key: "firstName", dir: -1 }]
425     </code></pre>
427     @method _getSortBy
428     @param {String|String[]|Object|Object[]} val The current sortBy value
429     @param {String} detail String passed to `get(HERE)`. to parse subattributes
430     @protected
431     @since 3.5.0
432     **/
433     _getSortBy: function (val, detail) {
434         var state, i, len, col;
436         // "sortBy." is 7 characters. Used to catch 
437         detail = detail.slice(7);
439         // TODO: table.get('sortBy.asObject')? table.get('sortBy.json')?
440         if (detail === 'state') {
441             state = [];
443             for (i = 0, len = this._sortBy.length; i < len; ++i) {
444                 col = this._sortBy[i];
445                 state.push({
446                     column: col._id,
447                     dir: col.sortDir
448                 });
449             }
451             // TODO: Always return an array?
452             return { state: (state.length === 1) ? state[0] : state };
453         } else {
454             return val;
455         }
456     },
458     /**
459     Sets up the initial sort state and instance properties.  Publishes events
460     and subscribes to attribute change events to maintain internal state.
462     @method initializer
463     @protected
464     @since 3.5.0
465     **/
466     initializer: function () {
467         var boundParseSortable = Y.bind('_parseSortable', this);
469         this._parseSortable();
471         this._setSortBy();
473         this._initSortFn();
475         this._initSortStrings();
477         this.after({
478             renderHeader  : Y.bind('_renderSortable', this),
479             dataChange    : Y.bind('_afterSortDataChange', this),
480             sortByChange  : Y.bind('_afterSortByChange', this),
481             sortableChange: boundParseSortable,
482             columnsChange : boundParseSortable,
483             "*:change"    : Y.bind('_afterSortRecordChange', this)
484         });
486         this.publish('sort', {
487             defaultFn: Y.bind('_defSortFn', this)
488         });
489     },
491     /**
492     Creates a `_compare` function for the `data` ModelList to allow custom
493     sorting by multiple fields.
495     @method _initSortFn
496     @protected
497     @since 3.5.0
498     **/
499     _initSortFn: function () {
500         var self = this;
502         // TODO: This should be a ModelList extension.
503         // FIXME: Modifying a component of the host seems a little smelly
504         // FIXME: Declaring inline override to leverage closure vs
505         // compiling a new function for each column/sortable change or
506         // binding the _compare implementation to this, resulting in an
507         // extra function hop during sorting. Lesser of three evils?
508         this.data._compare = function (a, b) {
509             var cmp = 0,
510                 i, len, col, dir, aa, bb;
512             for (i = 0, len = self._sortBy.length; !cmp && i < len; ++i) {
513                 col = self._sortBy[i];
514                 dir = col.sortDir;
516                 if (col.sortFn) {
517                     cmp = col.sortFn(a, b, (dir === -1));
518                 } else {
519                     // FIXME? Requires columns without sortFns to have key
520                     aa = a.get(col.key);
521                     bb = b.get(col.key);
523                     cmp = (aa > bb) ? dir : ((aa < bb) ? -dir : 0);
524                 }
525             }
527             return cmp;
528         };
530         if (this._sortBy.length) {
531             this.data.comparator = this._sortComparator;
533             // TODO: is this necessary? Should it be elsewhere?
534             this.data.sort();
535         } else {
536             // Leave the _compare method in place to avoid having to set it
537             // up again.  Mistake?
538             delete this.data.comparator;
539         }
540     },
542     /**
543     Add the sort related strings to the `strings` map.
544     
545     @method _initSortStrings
546     @protected
547     @since 3.5.0
548     **/
549     _initSortStrings: function () {
550         // Not a valueFn because other class extensions will want to add to it
551         this.set('strings', Y.mix((this.get('strings') || {}), 
552             Y.Intl.get('datatable-sort')));
553     },
555     /**
556     Fires the `sort` event in response to user clicks on sortable column
557     headers.
559     @method _onUITriggerSort
560     @param {DOMEventFacade} e The `click` event
561     @protected
562     @since 3.5.0
563     **/
564     _onUITriggerSort: function (e) {
565         var id = e.currentTarget.getAttribute('data-yui3-col-id'),
566             sortBy = e.shiftKey ? this.get('sortBy') : [{}],
567             column = id && this.getColumn(id),
568             i, len;
570         if (e.type === 'keydown' && e.keyCode !== 32) {
571             return;
572         }
574         // In case a headerTemplate injected a link
575         // TODO: Is this overreaching?
576         e.preventDefault();
578         if (column) {
579             if (e.shiftKey) {
580                 for (i = 0, len = sortBy.length; i < len; ++i) {
581                     if (id === sortBy[i] || Math.abs(sortBy[i][id] === 1)) {
582                         if (!isObject(sortBy[i])) {
583                             sortBy[i] = {};
584                         }
586                         sortBy[i][id] = -(column.sortDir|0) || 1;
587                         break;
588                     }
589                 }
591                 if (i >= len) {
592                     sortBy.push(column._id);
593                 }
594             } else {
595                 sortBy[0][id] = -(column.sortDir|0) || 1;
596             }
598             this.fire('sort', {
599                 originEvent: e,
600                 sortBy: sortBy
601             });
602         }
603     },
605     /**
606     Normalizes the possible input values for the `sortable` attribute, storing
607     the results in the `_sortable` property.
609     @method _parseSortable
610     @protected
611     @since 3.5.0
612     **/
613     _parseSortable: function () {
614         var sortable = this.get('sortable'),
615             columns  = [],
616             i, len, col;
618         if (isArray(sortable)) {
619             for (i = 0, len = sortable.length; i < len; ++i) {
620                 col = sortable[i];
622                 // isArray is called because arrays are objects, but will rely
623                 // on getColumn to nullify them for the subsequent if (col)
624                 if (!isObject(col, true) || isArray(col)) {
625                     col = this.getColumn(col);
626                 }
628                 if (col) {
629                     columns.push(col);
630                 }
631             }
632         } else if (sortable) {
633             columns = this._displayColumns.slice();
635             if (sortable === 'auto') {
636                 for (i = columns.length - 1; i >= 0; --i) {
637                     if (!columns[i].sortable) {
638                         columns.splice(i, 1);
639                     }
640                 }
641             }
642         }
644         this._sortable = columns;
645     },
647     /**
648     Initial application of the sortable UI.
650     @method _renderSortable
651     @protected
652     @since 3.5.0
653     **/
654     _renderSortable: function () {
655         this._uiSetSortable();
657         this._bindSortUI();
658     },
660     /**
661     Parses the current `sortBy` attribute into a normalized structure for the
662     `data` ModelList's `_compare` method.  Also updates the column
663     configurations' `sortDir` properties.
665     @method _setSortBy
666     @protected
667     @since 3.5.0
668     **/
669     _setSortBy: function () {
670         var columns     = this._displayColumns,
671             sortBy      = this.get('sortBy') || [],
672             sortedClass = ' ' + this.getClassName('sorted'),
673             i, len, name, dir, field, column;
675         this._sortBy = [];
677         // Purge current sort state from column configs
678         for (i = 0, len = columns.length; i < len; ++i) {
679             column = columns[i];
681             delete column.sortDir;
683             if (column.className) {
684                 // TODO: be more thorough
685                 column.className = column.className.replace(sortedClass, '');
686             }
687         }
689         sortBy = toArray(sortBy);
691         for (i = 0, len = sortBy.length; i < len; ++i) {
692             name = sortBy[i];
693             dir  = 1;
695             if (isObject(name)) {
696                 field = name;
697                 // Have to use a for-in loop to process sort({ foo: -1 })
698                 for (name in field) {
699                     if (field.hasOwnProperty(name)) {
700                         dir = dirMap[field[name]];
701                         break;
702                     }
703                 }
704             }
706             if (name) {
707                 // Allow sorting of any model field and any column
708                 // FIXME: this isn't limited to model attributes, but there's no
709                 // convenient way to get a list of the attributes for a Model
710                 // subclass *including* the attributes of its superclasses.
711                 column = this.getColumn(name) || { _id: name, key: name };
713                 if (column) {
714                     column.sortDir = dir;
716                     if (!column.className) {
717                         column.className = '';
718                     }
720                     column.className += sortedClass;
722                     this._sortBy.push(column);
723                 }
724             }
725         }
726     },
728     /**
729     Array of column configuration objects of those columns that need UI setup
730     for user interaction.
732     @property _sortable
733     @type {Object[]}
734     @protected
735     @since 3.5.0
736     **/
737     //_sortable: null,
739     /**
740     Array of column configuration objects for those columns that are currently
741     being used to sort the data.  Fake column objects are used for fields that
742     are not rendered as columns.
744     @property _sortBy
745     @type {Object[]}
746     @protected
747     @since 3.5.0
748     **/
749     //_sortBy: null,
751     /**
752     Replacement `comparator` for the `data` ModelList that defers sorting logic
753     to the `_compare` method.  The deferral is accomplished by returning `this`.
755     @method _sortComparator
756     @param {Model} item The record being evaluated for sort position
757     @return {Model} The record
758     @protected
759     @since 3.5.0
760     **/
761     _sortComparator: function (item) {
762         // Defer sorting to ModelList's _compare
763         return item;
764     },
766     /**
767     Applies the appropriate classes to the `boundingBox` and column headers to
768     indicate sort state and sortability.
770     Also currently wraps the header content of sortable columns in a `<div>`
771     liner to give a CSS anchor for sort indicators.
773     @method _uiSetSortable
774     @protected
775     @since 3.5.0
776     **/
777     _uiSetSortable: function () {
778         var columns       = this._sortable || [],
779             sortableClass = this.getClassName('sortable', 'column'),
780             ascClass      = this.getClassName('sorted'),
781             descClass     = this.getClassName('sorted', 'desc'),
782             linerClass    = this.getClassName('sort', 'liner'),
783             indicatorClass= this.getClassName('sort', 'indicator'),
784             sortableCols  = {},
785             i, len, col, node, liner, title, desc;
787         this.get('boundingBox').toggleClass(
788             this.getClassName('sortable'),
789             columns.length);
791         for (i = 0, len = columns.length; i < len; ++i) {
792             sortableCols[columns[i].id] = columns[i];
793         }
795         // TODO: this.head.render() + decorate cells?
796         this._theadNode.all('.' + sortableClass).each(function (node) {
797             var col       = sortableCols[node.get('id')],
798                 liner     = node.one('.' + linerClass),
799                 indicator;
801             if (col) {
802                 if (!col.sortDir) {
803                     node.removeClass(ascClass)
804                         .removeClass(descClass);
805                 }
806             } else {
807                 node.removeClass(sortableClass)
808                     .removeClass(ascClass)
809                     .removeClass(descClass);
811                 if (liner) {
812                     liner.replace(liner.get('childNodes').toFrag());
813                 }
815                 indicator = node.one('.' + indicatorClass);
817                 if (indicator) {
818                     indicator.remove().destroy(true);
819                 }
820             }
821         });
823         for (i = 0, len = columns.length; i < len; ++i) {
824             col  = columns[i];
825             node = this._theadNode.one('#' + col.id);
826             desc = col.sortDir === -1;
828             if (node) {
829                 liner = node.one('.' + linerClass);
831                 node.addClass(sortableClass);
833                 if (col.sortDir) {
834                     node.addClass(ascClass);
836                     node.toggleClass(descClass, desc);
838                     node.setAttribute('aria-sort', desc ?
839                         'descending' : 'ascending');
840                 }
842                 if (!liner) {
843                     liner = Y.Node.create(Y.Lang.sub(
844                         this.SORTABLE_HEADER_TEMPLATE, {
845                             className: linerClass,
846                             indicatorClass: indicatorClass
847                         }));
849                     liner.prepend(node.get('childNodes').toFrag());
851                     node.append(liner);
852                 }
854                 title = sub(this.getString(
855                     (col.sortDir === 1) ? 'reverseSortBy' : 'sortBy'), {
856                         column: col.abbr || col.label ||
857                                 col.key  || ('column ' + i)
858                 });
860                 node.setAttribute('title', title);
861                 // To combat VoiceOver from reading the sort title as the
862                 // column header
863                 node.setAttribute('aria-labelledby', col.id);
864             }
865         }
866     },
868     /**
869     Allows values `true`, `false`, "auto", or arrays of column names through.
871     @method _validateSortable
872     @param {Any} val The input value to `set("sortable", VAL)`
873     @return {Boolean}
874     @protected
875     @since 3.5.0
876     **/
877     _validateSortable: function (val) {
878         return val === 'auto' || isBoolean(val) || isArray(val);
879     },
881     /**
882     Allows strings, arrays of strings, objects, or arrays of objects.
884     @method _validateSortBy
885     @param {String|String[]|Object|Object[]} val The new `sortBy` value
886     @return {Boolean}
887     @protected
888     @since 3.5.0
889     **/
890     _validateSortBy: function (val) {
891         return val === null ||
892                isString(val) ||
893                isObject(val, true) ||
894                (isArray(val) && (isString(val[0]) || isObject(val, true)));
895     }
897 }, true);
899 Y.DataTable.Sortable = Sortable;
901 Y.Base.mix(Y.DataTable, [Sortable]);
904 }, '3.5.0' ,{lang:['en'], requires:['datatable-base']});