API Docs for: 3.8.0
Show:

File: datatable/js/sort.js

  1. /**
  2. Adds support for sorting the table data by API methods `table.sort(...)` or
  3. `table.toggleSort(...)` or by clicking on column headers in the rendered UI.

  4. @module datatable
  5. @submodule datatable-sort
  6. @since 3.5.0
  7. **/
  8. var YLang     = Y.Lang,
  9.     isBoolean = YLang.isBoolean,
  10.     isString  = YLang.isString,
  11.     isArray   = YLang.isArray,
  12.     isObject  = YLang.isObject,

  13.     toArray = Y.Array,
  14.     sub     = YLang.sub,

  15.     dirMap = {
  16.         asc : 1,
  17.         desc: -1,
  18.         "1" : 1,
  19.         "-1": -1
  20.     };


  21. /**
  22. _API docs for this extension are included in the DataTable class._

  23. This DataTable class extension adds support for sorting the table data by API
  24. methods `table.sort(...)` or `table.toggleSort(...)` or by clicking on column
  25. headers in the rendered UI.

  26. Sorting by the API is enabled automatically when this module is `use()`d.  To
  27. enable UI triggered sorting, set the DataTable's `sortable` attribute to
  28. `true`.

  29. <pre><code>
  30. var table = new Y.DataTable({
  31.     columns: [ 'id', 'username', 'name', 'birthdate' ],
  32.     data: [ ... ],
  33.     sortable: true
  34. });

  35. table.render('#table');
  36. </code></pre>

  37. Setting `sortable` to `true` will enable UI sorting for all columns.  To enable
  38. UI sorting for certain columns only, set `sortable` to an array of column keys,
  39. or just add `sortable: true` to the respective column configuration objects.
  40. This uses the default setting of `sortable: auto` for the DataTable instance.

  41. <pre><code>
  42. var table = new Y.DataTable({
  43.     columns: [
  44.         'id',
  45.         { key: 'username',  sortable: true },
  46.         { key: 'name',      sortable: true },
  47.         { key: 'birthdate', sortable: true }
  48.     ],
  49.     data: [ ... ]
  50.     // sortable: 'auto' is the default
  51. });

  52. // OR
  53. var table = new Y.DataTable({
  54.     columns: [ 'id', 'username', 'name', 'birthdate' ],
  55.     data: [ ... ],
  56.     sortable: [ 'username', 'name', 'birthdate' ]
  57. });
  58. </code></pre>

  59. To disable UI sorting for all columns, set `sortable` to `false`.  This still
  60. permits sorting via the API methods.

  61. As new records are inserted into the table's `data` ModelList, they will be inserted at the correct index to preserve the sort order.

  62. The current sort order is stored in the `sortBy` attribute.  Assigning this value at instantiation will automatically sort your data.

  63. Sorting is done by a simple value comparison using &lt; and &gt; on the field
  64. value.  If you need custom sorting, add a sort function in the column's
  65. `sortFn` property.  Columns whose content is generated by formatters, but don't
  66. relate to a single `key`, require a `sortFn` to be sortable.

  67. <pre><code>
  68. function nameSort(a, b, desc) {
  69.     var aa = a.get('lastName') + a.get('firstName'),
  70.         bb = a.get('lastName') + b.get('firstName'),
  71.         order = (aa > bb) ? 1 : -(aa < bb);
  72.        
  73.     return desc ? -order : order;
  74. }

  75. var table = new Y.DataTable({
  76.     columns: [ 'id', 'username', { key: name, sortFn: nameSort }, 'birthdate' ],
  77.     data: [ ... ],
  78.     sortable: [ 'username', 'name', 'birthdate' ]
  79. });
  80. </code></pre>

  81. See the user guide for more details.

  82. @class DataTable.Sortable
  83. @for DataTable
  84. @since 3.5.0
  85. **/
  86. function Sortable() {}

  87. Sortable.ATTRS = {
  88.     // Which columns in the UI should suggest and respond to sorting interaction
  89.     // pass an empty array if no UI columns should show sortable, but you want the
  90.     // table.sort(...) API
  91.     /**
  92.     Controls which column headers can trigger sorting by user clicks.

  93.     Acceptable values are:

  94.      * "auto" - (default) looks for `sortable: true` in the column configurations
  95.      * `true` - all columns are enabled
  96.      * `false - no UI sortable is enabled
  97.      * {String[]} - array of key names to give sortable headers

  98.     @attribute sortable
  99.     @type {String|String[]|Boolean}
  100.     @default "auto"
  101.     @since 3.5.0
  102.     **/
  103.     sortable: {
  104.         value: 'auto',
  105.         validator: '_validateSortable'
  106.     },

  107.     /**
  108.     The current sort configuration to maintain in the data.

  109.     Accepts column `key` strings or objects with a single property, the column
  110.     `key`, with a value of 1, -1, "asc", or "desc".  E.g. `{ username: 'asc'
  111.     }`.  String values are assumed to be ascending.

  112.     Example values would be:

  113.      * `"username"` - sort by the data's `username` field or the `key`
  114.        associated to a column with that `name`.
  115.      * `{ username: "desc" }` - sort by `username` in descending order.
  116.        Alternately, use values "asc", 1 (same as "asc"), or -1 (same as "desc").
  117.      * `["lastName", "firstName"]` - ascending sort by `lastName`, but for
  118.        records with the same `lastName`, ascending subsort by `firstName`.
  119.        Array can have as many items as you want.
  120.      * `[{ lastName: -1 }, "firstName"]` - descending sort by `lastName`,
  121.        ascending subsort by `firstName`. Mixed types are ok.

  122.     @attribute sortBy
  123.     @type {String|String[]|Object|Object[]}
  124.     @since 3.5.0
  125.     **/
  126.     sortBy: {
  127.         validator: '_validateSortBy',
  128.         getter: '_getSortBy'
  129.     },

  130.     /**
  131.     Strings containing language for sorting tooltips.

  132.     @attribute strings
  133.     @type {Object}
  134.     @default (strings for current lang configured in the YUI instance config)
  135.     @since 3.5.0
  136.     **/
  137.     strings: {}
  138. };

  139. Y.mix(Sortable.prototype, {

  140.     /**
  141.     Sort the data in the `data` ModelList and refresh the table with the new
  142.     order.

  143.     Acceptable values for `fields` are `key` strings or objects with a single
  144.     property, the column `key`, with a value of 1, -1, "asc", or "desc".  E.g.
  145.     `{ username: 'asc' }`.  String values are assumed to be ascending.

  146.     Example values would be:

  147.      * `"username"` - sort by the data's `username` field or the `key`
  148.        associated to a column with that `name`.
  149.      * `{ username: "desc" }` - sort by `username` in descending order.
  150.        Alternately, use values "asc", 1 (same as "asc"), or -1 (same as "desc").
  151.      * `["lastName", "firstName"]` - ascending sort by `lastName`, but for
  152.        records with the same `lastName`, ascending subsort by `firstName`.
  153.        Array can have as many items as you want.
  154.      * `[{ lastName: -1 }, "firstName"]` - descending sort by `lastName`,
  155.        ascending subsort by `firstName`. Mixed types are ok.

  156.     @method sort
  157.     @param {String|String[]|Object|Object[]} fields The field(s) to sort by
  158.     @param {Object} [payload] Extra `sort` event payload you want to send along
  159.     @return {DataTable}
  160.     @chainable
  161.     @since 3.5.0
  162.     **/
  163.     sort: function (fields, payload) {
  164.         /**
  165.         Notifies of an impending sort, either from clicking on a column
  166.         header, or from a call to the `sort` or `toggleSort` method.

  167.         The requested sort is available in the `sortBy` property of the event.

  168.         The default behavior of this event sets the table's `sortBy` attribute.

  169.         @event sort
  170.         @param {String|String[]|Object|Object[]} sortBy The requested sort
  171.         @preventable _defSortFn
  172.         **/
  173.         return this.fire('sort', Y.merge((payload || {}), {
  174.             sortBy: fields || this.get('sortBy')
  175.         }));
  176.     },

  177.     /**
  178.     Template for the node that will wrap the header content for sortable
  179.     columns.

  180.     @property SORTABLE_HEADER_TEMPLATE
  181.     @type {HTML}
  182.     @value '<div class="{className}" tabindex="0"><span class="{indicatorClass}"></span></div>'
  183.     @since 3.5.0
  184.     **/
  185.     SORTABLE_HEADER_TEMPLATE: '<div class="{className}" tabindex="0"><span class="{indicatorClass}"></span></div>',

  186.     /**
  187.     Reverse the current sort direction of one or more fields currently being
  188.     sorted by.

  189.     Pass the `key` of the column or columns you want the sort order reversed
  190.     for.

  191.     @method toggleSort
  192.     @param {String|String[]} fields The field(s) to reverse sort order for
  193.     @param {Object} [payload] Extra `sort` event payload you want to send along
  194.     @return {DataTable}
  195.     @chainable
  196.     @since 3.5.0
  197.     **/
  198.     toggleSort: function (columns, payload) {
  199.         var current = this._sortBy,
  200.             sortBy = [],
  201.             i, len, j, col, index;

  202.         // To avoid updating column configs or sortBy directly
  203.         for (i = 0, len = current.length; i < len; ++i) {
  204.             col = {};
  205.             col[current[i]._id] = current[i].sortDir;
  206.             sortBy.push(col);
  207.         }

  208.         if (columns) {
  209.             columns = toArray(columns);

  210.             for (i = 0, len = columns.length; i < len; ++i) {
  211.                 col = columns[i];
  212.                 index = -1;

  213.                 for (j = sortBy.length - 1; i >= 0; --i) {
  214.                     if (sortBy[j][col]) {
  215.                         sortBy[j][col] *= -1;
  216.                         break;
  217.                     }
  218.                 }
  219.             }
  220.         } else {
  221.             for (i = 0, len = sortBy.length; i < len; ++i) {
  222.                 for (col in sortBy[i]) {
  223.                     if (sortBy[i].hasOwnProperty(col)) {
  224.                         sortBy[i][col] *= -1;
  225.                         break;
  226.                     }
  227.                 }
  228.             }
  229.         }

  230.         return this.fire('sort', Y.merge((payload || {}), {
  231.             sortBy: sortBy
  232.         }));
  233.     },

  234.     //--------------------------------------------------------------------------
  235.     // Protected properties and methods
  236.     //--------------------------------------------------------------------------
  237.     /**
  238.     Sorts the `data` ModelList based on the new `sortBy` configuration.

  239.     @method _afterSortByChange
  240.     @param {EventFacade} e The `sortByChange` event
  241.     @protected
  242.     @since 3.5.0
  243.     **/
  244.     _afterSortByChange: function (e) {
  245.         // Can't use a setter because it's a chicken and egg problem. The
  246.         // columns need to be set up to translate, but columns are initialized
  247.         // from Core's initializer.  So construction-time assignment would
  248.         // fail.
  249.         this._setSortBy();

  250.         // Don't sort unless sortBy has been set
  251.         if (this._sortBy.length) {
  252.             if (!this.data.comparator) {
  253.                  this.data.comparator = this._sortComparator;
  254.             }

  255.             this.data.sort();
  256.         }
  257.     },

  258.     /**
  259.     Applies the sorting logic to the new ModelList if the `newVal` is a new
  260.     ModelList.

  261.     @method _afterSortDataChange
  262.     @param {EventFacade} e the `dataChange` event
  263.     @protected
  264.     @since 3.5.0
  265.     **/
  266.     _afterSortDataChange: function (e) {
  267.         // object values always trigger a change event, but we only want to
  268.         // call _initSortFn if the value passed to the `data` attribute was a
  269.         // new ModelList, not a set of new data as an array, or even the same
  270.         // ModelList.
  271.         if (e.prevVal !== e.newVal || e.newVal.hasOwnProperty('_compare')) {
  272.             this._initSortFn();
  273.         }
  274.     },

  275.     /**
  276.     Checks if any of the fields in the modified record are fields that are
  277.     currently being sorted by, and if so, resorts the `data` ModelList.

  278.     @method _afterSortRecordChange
  279.     @param {EventFacade} e The Model's `change` event
  280.     @protected
  281.     @since 3.5.0
  282.     **/
  283.     _afterSortRecordChange: function (e) {
  284.         var i, len;

  285.         for (i = 0, len = this._sortBy.length; i < len; ++i) {
  286.             if (e.changed[this._sortBy[i].key]) {
  287.                 this.data.sort();
  288.                 break;
  289.             }
  290.         }
  291.     },

  292.     /**
  293.     Subscribes to state changes that warrant updating the UI, and adds the
  294.     click handler for triggering the sort operation from the UI.

  295.     @method _bindSortUI
  296.     @protected
  297.     @since 3.5.0
  298.     **/
  299.     _bindSortUI: function () {
  300.         var handles = this._eventHandles;
  301.        
  302.         if (!handles.sortAttrs) {
  303.             handles.sortAttrs = this.after(
  304.                 ['sortableChange', 'sortByChange', 'columnsChange'],
  305.                 Y.bind('_uiSetSortable', this));
  306.         }

  307.         if (!handles.sortUITrigger && this._theadNode) {
  308.             handles.sortUITrigger = this.delegate(['click','keydown'],
  309.                 Y.rbind('_onUITriggerSort', this),
  310.                 '.' + this.getClassName('sortable', 'column'));
  311.         }
  312.     },

  313.     /**
  314.     Sets the `sortBy` attribute from the `sort` event's `e.sortBy` value.

  315.     @method _defSortFn
  316.     @param {EventFacade} e The `sort` event
  317.     @protected
  318.     @since 3.5.0
  319.     **/
  320.     _defSortFn: function (e) {
  321.         this.set.apply(this, ['sortBy', e.sortBy].concat(e.details));
  322.     },

  323.     /**
  324.     Getter for the `sortBy` attribute.
  325.    
  326.     Supports the special subattribute "sortBy.state" to get a normalized JSON
  327.     version of the current sort state.  Otherwise, returns the last assigned
  328.     value.

  329.     For example:

  330.     <pre><code>var table = new Y.DataTable({
  331.         columns: [ ... ],
  332.         data: [ ... ],
  333.         sortBy: 'username'
  334.     });

  335.     table.get('sortBy'); // 'username'
  336.     table.get('sortBy.state'); // { key: 'username', dir: 1 }

  337.     table.sort(['lastName', { firstName: "desc" }]);
  338.     table.get('sortBy'); // ['lastName', { firstName: "desc" }]
  339.     table.get('sortBy.state'); // [{ key: "lastName", dir: 1 }, { key: "firstName", dir: -1 }]
  340.     </code></pre>

  341.     @method _getSortBy
  342.     @param {String|String[]|Object|Object[]} val The current sortBy value
  343.     @param {String} detail String passed to `get(HERE)`. to parse subattributes
  344.     @protected
  345.     @since 3.5.0
  346.     **/
  347.     _getSortBy: function (val, detail) {
  348.         var state, i, len, col;

  349.         // "sortBy." is 7 characters. Used to catch
  350.         detail = detail.slice(7);

  351.         // TODO: table.get('sortBy.asObject')? table.get('sortBy.json')?
  352.         if (detail === 'state') {
  353.             state = [];

  354.             for (i = 0, len = this._sortBy.length; i < len; ++i) {
  355.                 col = this._sortBy[i];
  356.                 state.push({
  357.                     column: col._id,
  358.                     dir: col.sortDir
  359.                 });
  360.             }

  361.             // TODO: Always return an array?
  362.             return { state: (state.length === 1) ? state[0] : state };
  363.         } else {
  364.             return val;
  365.         }
  366.     },

  367.     /**
  368.     Sets up the initial sort state and instance properties.  Publishes events
  369.     and subscribes to attribute change events to maintain internal state.

  370.     @method initializer
  371.     @protected
  372.     @since 3.5.0
  373.     **/
  374.     initializer: function () {
  375.         var boundParseSortable = Y.bind('_parseSortable', this);

  376.         this._parseSortable();

  377.         this._setSortBy();

  378.         this._initSortFn();

  379.         this._initSortStrings();

  380.         this.after({
  381.             'table:renderHeader': Y.bind('_renderSortable', this),
  382.             dataChange          : Y.bind('_afterSortDataChange', this),
  383.             sortByChange        : Y.bind('_afterSortByChange', this),
  384.             sortableChange      : boundParseSortable,
  385.             columnsChange       : boundParseSortable
  386.         });
  387.         this.data.after(this.data.model.NAME + ":change",
  388.             Y.bind('_afterSortRecordChange', this));

  389.         // TODO: this event needs magic, allowing async remote sorting
  390.         this.publish('sort', {
  391.             defaultFn: Y.bind('_defSortFn', this)
  392.         });
  393.     },

  394.     /**
  395.     Creates a `_compare` function for the `data` ModelList to allow custom
  396.     sorting by multiple fields.

  397.     @method _initSortFn
  398.     @protected
  399.     @since 3.5.0
  400.     **/
  401.     _initSortFn: function () {
  402.         var self = this;

  403.         // TODO: This should be a ModelList extension.
  404.         // FIXME: Modifying a component of the host seems a little smelly
  405.         // FIXME: Declaring inline override to leverage closure vs
  406.         // compiling a new function for each column/sortable change or
  407.         // binding the _compare implementation to this, resulting in an
  408.         // extra function hop during sorting. Lesser of three evils?
  409.         this.data._compare = function (a, b) {
  410.             var cmp = 0,
  411.                 i, len, col, dir, aa, bb;

  412.             for (i = 0, len = self._sortBy.length; !cmp && i < len; ++i) {
  413.                 col = self._sortBy[i];
  414.                 dir = col.sortDir;

  415.                 if (col.sortFn) {
  416.                     cmp = col.sortFn(a, b, (dir === -1));
  417.                 } else {
  418.                     // FIXME? Requires columns without sortFns to have key
  419.                     aa = a.get(col.key) || '';
  420.                     bb = b.get(col.key) || '';

  421.                     cmp = (aa > bb) ? dir : ((aa < bb) ? -dir : 0);
  422.                 }
  423.             }

  424.             return cmp;
  425.         };

  426.         if (this._sortBy.length) {
  427.             this.data.comparator = this._sortComparator;

  428.             // TODO: is this necessary? Should it be elsewhere?
  429.             this.data.sort();
  430.         } else {
  431.             // Leave the _compare method in place to avoid having to set it
  432.             // up again.  Mistake?
  433.             delete this.data.comparator;
  434.         }
  435.     },

  436.     /**
  437.     Add the sort related strings to the `strings` map.
  438.    
  439.     @method _initSortStrings
  440.     @protected
  441.     @since 3.5.0
  442.     **/
  443.     _initSortStrings: function () {
  444.         // Not a valueFn because other class extensions will want to add to it
  445.         this.set('strings', Y.mix((this.get('strings') || {}),
  446.             Y.Intl.get('datatable-sort')));
  447.     },

  448.     /**
  449.     Fires the `sort` event in response to user clicks on sortable column
  450.     headers.

  451.     @method _onUITriggerSort
  452.     @param {DOMEventFacade} e The `click` event
  453.     @protected
  454.     @since 3.5.0
  455.     **/
  456.     _onUITriggerSort: function (e) {
  457.         var id = e.currentTarget.getAttribute('data-yui3-col-id'),
  458.             sortBy = e.shiftKey ? this.get('sortBy') : [{}],
  459.             column = id && this.getColumn(id),
  460.             i, len;

  461.         if (e.type === 'keydown' && e.keyCode !== 32) {
  462.             return;
  463.         }

  464.         // In case a headerTemplate injected a link
  465.         // TODO: Is this overreaching?
  466.         e.preventDefault();

  467.         if (column) {
  468.             if (e.shiftKey) {
  469.                 for (i = 0, len = sortBy.length; i < len; ++i) {
  470.                     if (id === sortBy[i] || Math.abs(sortBy[i][id] === 1)) {
  471.                         if (!isObject(sortBy[i])) {
  472.                             sortBy[i] = {};
  473.                         }

  474.                         sortBy[i][id] = -(column.sortDir|0) || 1;
  475.                         break;
  476.                     }
  477.                 }

  478.                 if (i >= len) {
  479.                     sortBy.push(column._id);
  480.                 }
  481.             } else {
  482.                 sortBy[0][id] = -(column.sortDir|0) || 1;
  483.             }

  484.             this.fire('sort', {
  485.                 originEvent: e,
  486.                 sortBy: sortBy
  487.             });
  488.         }
  489.     },

  490.     /**
  491.     Normalizes the possible input values for the `sortable` attribute, storing
  492.     the results in the `_sortable` property.

  493.     @method _parseSortable
  494.     @protected
  495.     @since 3.5.0
  496.     **/
  497.     _parseSortable: function () {
  498.         var sortable = this.get('sortable'),
  499.             columns  = [],
  500.             i, len, col;

  501.         if (isArray(sortable)) {
  502.             for (i = 0, len = sortable.length; i < len; ++i) {
  503.                 col = sortable[i];

  504.                 // isArray is called because arrays are objects, but will rely
  505.                 // on getColumn to nullify them for the subsequent if (col)
  506.                 if (!isObject(col, true) || isArray(col)) {
  507.                     col = this.getColumn(col);
  508.                 }

  509.                 if (col) {
  510.                     columns.push(col);
  511.                 }
  512.             }
  513.         } else if (sortable) {
  514.             columns = this._displayColumns.slice();

  515.             if (sortable === 'auto') {
  516.                 for (i = columns.length - 1; i >= 0; --i) {
  517.                     if (!columns[i].sortable) {
  518.                         columns.splice(i, 1);
  519.                     }
  520.                 }
  521.             }
  522.         }

  523.         this._sortable = columns;
  524.     },

  525.     /**
  526.     Initial application of the sortable UI.

  527.     @method _renderSortable
  528.     @protected
  529.     @since 3.5.0
  530.     **/
  531.     _renderSortable: function () {
  532.         this._uiSetSortable();

  533.         this._bindSortUI();
  534.     },

  535.     /**
  536.     Parses the current `sortBy` attribute into a normalized structure for the
  537.     `data` ModelList's `_compare` method.  Also updates the column
  538.     configurations' `sortDir` properties.

  539.     @method _setSortBy
  540.     @protected
  541.     @since 3.5.0
  542.     **/
  543.     _setSortBy: function () {
  544.         var columns     = this._displayColumns,
  545.             sortBy      = this.get('sortBy') || [],
  546.             sortedClass = ' ' + this.getClassName('sorted'),
  547.             i, len, name, dir, field, column;

  548.         this._sortBy = [];

  549.         // Purge current sort state from column configs
  550.         for (i = 0, len = columns.length; i < len; ++i) {
  551.             column = columns[i];

  552.             delete column.sortDir;

  553.             if (column.className) {
  554.                 // TODO: be more thorough
  555.                 column.className = column.className.replace(sortedClass, '');
  556.             }
  557.         }

  558.         sortBy = toArray(sortBy);

  559.         for (i = 0, len = sortBy.length; i < len; ++i) {
  560.             name = sortBy[i];
  561.             dir  = 1;

  562.             if (isObject(name)) {
  563.                 field = name;
  564.                 // Have to use a for-in loop to process sort({ foo: -1 })
  565.                 for (name in field) {
  566.                     if (field.hasOwnProperty(name)) {
  567.                         dir = dirMap[field[name]];
  568.                         break;
  569.                     }
  570.                 }
  571.             }

  572.             if (name) {
  573.                 // Allow sorting of any model field and any column
  574.                 // FIXME: this isn't limited to model attributes, but there's no
  575.                 // convenient way to get a list of the attributes for a Model
  576.                 // subclass *including* the attributes of its superclasses.
  577.                 column = this.getColumn(name) || { _id: name, key: name };

  578.                 if (column) {
  579.                     column.sortDir = dir;

  580.                     if (!column.className) {
  581.                         column.className = '';
  582.                     }

  583.                     column.className += sortedClass;

  584.                     this._sortBy.push(column);
  585.                 }
  586.             }
  587.         }
  588.     },

  589.     /**
  590.     Array of column configuration objects of those columns that need UI setup
  591.     for user interaction.

  592.     @property _sortable
  593.     @type {Object[]}
  594.     @protected
  595.     @since 3.5.0
  596.     **/
  597.     //_sortable: null,

  598.     /**
  599.     Array of column configuration objects for those columns that are currently
  600.     being used to sort the data.  Fake column objects are used for fields that
  601.     are not rendered as columns.

  602.     @property _sortBy
  603.     @type {Object[]}
  604.     @protected
  605.     @since 3.5.0
  606.     **/
  607.     //_sortBy: null,

  608.     /**
  609.     Replacement `comparator` for the `data` ModelList that defers sorting logic
  610.     to the `_compare` method.  The deferral is accomplished by returning `this`.

  611.     @method _sortComparator
  612.     @param {Model} item The record being evaluated for sort position
  613.     @return {Model} The record
  614.     @protected
  615.     @since 3.5.0
  616.     **/
  617.     _sortComparator: function (item) {
  618.         // Defer sorting to ModelList's _compare
  619.         return item;
  620.     },

  621.     /**
  622.     Applies the appropriate classes to the `boundingBox` and column headers to
  623.     indicate sort state and sortability.

  624.     Also currently wraps the header content of sortable columns in a `<div>`
  625.     liner to give a CSS anchor for sort indicators.

  626.     @method _uiSetSortable
  627.     @protected
  628.     @since 3.5.0
  629.     **/
  630.     _uiSetSortable: function () {
  631.         var columns       = this._sortable || [],
  632.             sortableClass = this.getClassName('sortable', 'column'),
  633.             ascClass      = this.getClassName('sorted'),
  634.             descClass     = this.getClassName('sorted', 'desc'),
  635.             linerClass    = this.getClassName('sort', 'liner'),
  636.             indicatorClass= this.getClassName('sort', 'indicator'),
  637.             sortableCols  = {},
  638.             i, len, col, node, liner, title, desc;

  639.         this.get('boundingBox').toggleClass(
  640.             this.getClassName('sortable'),
  641.             columns.length);

  642.         for (i = 0, len = columns.length; i < len; ++i) {
  643.             sortableCols[columns[i].id] = columns[i];
  644.         }

  645.         // TODO: this.head.render() + decorate cells?
  646.         this._theadNode.all('.' + sortableClass).each(function (node) {
  647.             var col       = sortableCols[node.get('id')],
  648.                 liner     = node.one('.' + linerClass),
  649.                 indicator;

  650.             if (col) {
  651.                 if (!col.sortDir) {
  652.                     node.removeClass(ascClass)
  653.                         .removeClass(descClass);
  654.                 }
  655.             } else {
  656.                 node.removeClass(sortableClass)
  657.                     .removeClass(ascClass)
  658.                     .removeClass(descClass);

  659.                 if (liner) {
  660.                     liner.replace(liner.get('childNodes').toFrag());
  661.                 }

  662.                 indicator = node.one('.' + indicatorClass);

  663.                 if (indicator) {
  664.                     indicator.remove().destroy(true);
  665.                 }
  666.             }
  667.         });

  668.         for (i = 0, len = columns.length; i < len; ++i) {
  669.             col  = columns[i];
  670.             node = this._theadNode.one('#' + col.id);
  671.             desc = col.sortDir === -1;

  672.             if (node) {
  673.                 liner = node.one('.' + linerClass);

  674.                 node.addClass(sortableClass);

  675.                 if (col.sortDir) {
  676.                     node.addClass(ascClass);

  677.                     node.toggleClass(descClass, desc);

  678.                     node.setAttribute('aria-sort', desc ?
  679.                         'descending' : 'ascending');
  680.                 }

  681.                 if (!liner) {
  682.                     liner = Y.Node.create(Y.Lang.sub(
  683.                         this.SORTABLE_HEADER_TEMPLATE, {
  684.                             className: linerClass,
  685.                             indicatorClass: indicatorClass
  686.                         }));

  687.                     liner.prepend(node.get('childNodes').toFrag());

  688.                     node.append(liner);
  689.                 }

  690.                 title = sub(this.getString(
  691.                     (col.sortDir === 1) ? 'reverseSortBy' : 'sortBy'), {
  692.                         column: col.abbr || col.label ||
  693.                                 col.key  || ('column ' + i)
  694.                 });

  695.                 node.setAttribute('title', title);
  696.                 // To combat VoiceOver from reading the sort title as the
  697.                 // column header
  698.                 node.setAttribute('aria-labelledby', col.id);
  699.             }
  700.         }
  701.     },

  702.     /**
  703.     Allows values `true`, `false`, "auto", or arrays of column names through.

  704.     @method _validateSortable
  705.     @param {Any} val The input value to `set("sortable", VAL)`
  706.     @return {Boolean}
  707.     @protected
  708.     @since 3.5.0
  709.     **/
  710.     _validateSortable: function (val) {
  711.         return val === 'auto' || isBoolean(val) || isArray(val);
  712.     },

  713.     /**
  714.     Allows strings, arrays of strings, objects, or arrays of objects.

  715.     @method _validateSortBy
  716.     @param {String|String[]|Object|Object[]} val The new `sortBy` value
  717.     @return {Boolean}
  718.     @protected
  719.     @since 3.5.0
  720.     **/
  721.     _validateSortBy: function (val) {
  722.         return val === null ||
  723.                isString(val) ||
  724.                isObject(val, true) ||
  725.                (isArray(val) && (isString(val[0]) || isObject(val, true)));
  726.     }

  727. }, true);

  728. Y.DataTable.Sortable = Sortable;

  729. Y.Base.mix(Y.DataTable, [Sortable]);

  730.