3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('datatable-sort', function (Y, NAME) {
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.
14 @submodule datatable-sort
18 isBoolean = YLang.isBoolean,
19 isString = YLang.isString,
20 isArray = YLang.isArray,
21 isObject = YLang.isObject,
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
46 var table = new Y.DataTable({
47 columns: [ 'id', 'username', 'name', 'birthdate' ],
52 table.render('#table');
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.
61 var table = new Y.DataTable({
64 { key: 'username', sortable: true },
65 { key: 'name', sortable: true },
66 { key: 'birthdate', sortable: true }
69 // sortable: 'auto' is the default
73 var table = new Y.DataTable({
74 columns: [ 'id', 'username', 'name', 'birthdate' ],
76 sortable: [ 'username', 'name', 'birthdate' ]
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 < and > 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.
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);
98 return desc ? -order : order;
101 var table = new Y.DataTable({
102 columns: [ 'id', 'username', { key: name, sortFn: nameSort }, 'birthdate' ],
104 sortable: [ 'username', 'name', 'birthdate' ]
108 See the user guide for more details.
110 @class DataTable.Sortable
114 function Sortable() {}
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
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
131 @type {String|String[]|Boolean}
137 validator: '_validateSortable'
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.
160 @type {String|String[]|Object|Object[]}
164 validator: '_validateSortBy',
169 Strings containing language for sorting tooltips.
173 @default (strings for current lang configured in the YUI instance config)
179 Y.mix(Sortable.prototype, {
182 Sort the data in the `data` ModelList and refresh the table with the new
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.
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
208 sort: function (fields, payload) {
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.
218 @param {String|String[]|Object|Object[]} sortBy The requested sort
219 @preventable _defSortFn
221 return this.fire('sort', Y.merge((payload || {}), {
222 sortBy: fields || this.get('sortBy')
227 Template for the node that will wrap the header content for sortable
230 @property SORTABLE_HEADER_TEMPLATE
232 @value '<div class="{className}" tabindex="0"><span class="{indicatorClass}"></span></div>'
235 SORTABLE_HEADER_TEMPLATE: '<div class="{className}" tabindex="0"><span class="{indicatorClass}"></span></div>',
238 Reverse the current sort direction of one or more fields currently being
241 Pass the `key` of the column or columns you want the sort order reversed
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
251 toggleSort: function (columns, payload) {
252 var current = this._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) {
259 col[current[i]._id] = current[i].sortDir;
264 columns = toArray(columns);
266 for (i = 0, len = columns.length; i < len; ++i) {
270 for (j = sortBy.length - 1; i >= 0; --i) {
271 if (sortBy[j][col]) {
272 sortBy[j][col] *= -1;
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;
288 return this.fire('sort', Y.merge((payload || {}), {
293 //--------------------------------------------------------------------------
294 // Protected properties and methods
295 //--------------------------------------------------------------------------
297 Sorts the `data` ModelList based on the new `sortBy` configuration.
299 @method _afterSortByChange
300 @param {EventFacade} e The `sortByChange` event
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
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;
322 Applies the sorting logic to the new ModelList if the `newVal` is a new
325 @method _afterSortDataChange
326 @param {EventFacade} e the `dataChange` event
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
335 if (e.prevVal !== e.newVal || e.newVal.hasOwnProperty('_compare')) {
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
349 _afterSortRecordChange: function (e) {
352 for (i = 0, len = this._sortBy.length; i < len; ++i) {
353 if (e.changed[this._sortBy[i].key]) {
361 Subscribes to state changes that warrant updating the UI, and adds the
362 click handler for triggering the sort operation from the UI.
368 _bindSortUI: function () {
369 var handles = this._eventHandles;
371 if (!handles.sortAttrs) {
372 handles.sortAttrs = this.after(
373 ['sortableChange', 'sortByChange', 'columnsChange'],
374 Y.bind('_uiSetSortable', this));
377 if (!handles.sortUITrigger && this._theadNode) {
378 handles.sortUITrigger = this.delegate(['click','keydown'],
379 Y.rbind('_onUITriggerSort', this),
380 '.' + this.getClassName('sortable', 'column'));
385 Sets the `sortBy` attribute from the `sort` event's `e.sortBy` value.
388 @param {EventFacade} e The `sort` event
392 _defSortFn: function (e) {
393 this.set.apply(this, ['sortBy', e.sortBy].concat(e.details));
397 Getter for the `sortBy` attribute.
399 Supports the special subattribute "sortBy.state" to get a normalized JSON
400 version of the current sort state. Otherwise, returns the last assigned
405 <pre><code>var table = new Y.DataTable({
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 }]
420 @param {String|String[]|Object|Object[]} val The current sortBy value
421 @param {String} detail String passed to `get(HERE)`. to parse subattributes
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') {
435 for (i = 0, len = this._sortBy.length; i < len; ++i) {
436 col = this._sortBy[i];
443 // TODO: Always return an array?
444 return { state: (state.length === 1) ? state[0] : state };
451 Sets up the initial sort state and instance properties. Publishes events
452 and subscribes to attribute change events to maintain internal state.
458 initializer: function () {
459 var boundParseSortable = Y.bind('_parseSortable', this);
461 this._parseSortable();
467 this._initSortStrings();
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
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)
486 Creates a `_compare` function for the `data` ModelList to allow custom
487 sorting by multiple fields.
493 _initSortFn: function () {
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) {
504 i, len, col, dir, aa, bb;
506 for (i = 0, len = self._sortBy.length; !cmp && i < len; ++i) {
507 col = self._sortBy[i];
511 cmp = col.sortFn(a, b, (dir === -1));
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);
524 if (this._sortBy.length) {
525 this.data.comparator = this._sortComparator;
527 // TODO: is this necessary? Should it be elsewhere?
530 // Leave the _compare method in place to avoid having to set it
531 // up again. Mistake?
532 delete this.data.comparator;
537 Add the sort related strings to the `strings` map.
539 @method _initSortStrings
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')));
550 Fires the `sort` event in response to user clicks on sortable column
553 @method _onUITriggerSort
554 @param {DOMEventFacade} e The `click` event
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),
564 if (e.type === 'keydown' && e.keyCode !== 32) {
568 // In case a headerTemplate injected a link
569 // TODO: Is this overreaching?
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])) {
580 sortBy[i][id] = -(column.sortDir|0) || 1;
586 sortBy.push(column._id);
589 sortBy[0][id] = -(column.sortDir|0) || 1;
600 Normalizes the possible input values for the `sortable` attribute, storing
601 the results in the `_sortable` property.
603 @method _parseSortable
607 _parseSortable: function () {
608 var sortable = this.get('sortable'),
612 if (isArray(sortable)) {
613 for (i = 0, len = sortable.length; i < len; ++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);
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);
638 this._sortable = columns;
642 Initial application of the sortable UI.
644 @method _renderSortable
648 _renderSortable: function () {
649 this._uiSetSortable();
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.
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;
671 // Purge current sort state from column configs
672 for (i = 0, len = columns.length; i < len; ++i) {
675 delete column.sortDir;
677 if (column.className) {
678 // TODO: be more thorough
679 column.className = column.className.replace(sortedClass, '');
683 sortBy = toArray(sortBy);
685 for (i = 0, len = sortBy.length; i < len; ++i) {
689 if (isObject(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]];
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 };
708 column.sortDir = dir;
710 if (!column.className) {
711 column.className = '';
714 column.className += sortedClass;
716 this._sortBy.push(column);
723 Array of column configuration objects of those columns that need UI setup
724 for user interaction.
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.
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
755 _sortComparator: function (item) {
756 // Defer sorting to ModelList's _compare
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
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'),
779 i, len, col, node, liner, title, desc;
781 this.get('boundingBox').toggleClass(
782 this.getClassName('sortable'),
785 for (i = 0, len = columns.length; i < len; ++i) {
786 sortableCols[columns[i].id] = columns[i];
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),
797 node.removeClass(ascClass)
798 .removeClass(descClass);
801 node.removeClass(sortableClass)
802 .removeClass(ascClass)
803 .removeClass(descClass);
806 liner.replace(liner.get('childNodes').toFrag());
809 indicator = node.one('.' + indicatorClass);
812 indicator.remove().destroy(true);
817 for (i = 0, len = columns.length; i < len; ++i) {
819 node = this._theadNode.one('#' + col.id);
820 desc = col.sortDir === -1;
823 liner = node.one('.' + linerClass);
825 node.addClass(sortableClass);
828 node.addClass(ascClass);
830 node.toggleClass(descClass, desc);
832 node.setAttribute('aria-sort', desc ?
833 'descending' : 'ascending');
837 liner = Y.Node.create(Y.Lang.sub(
838 this.SORTABLE_HEADER_TEMPLATE, {
839 className: linerClass,
840 indicatorClass: indicatorClass
843 liner.prepend(node.get('childNodes').toFrag());
848 title = sub(this.getString(
849 (col.sortDir === 1) ? 'reverseSortBy' : 'sortBy'), {
850 column: col.abbr || col.label ||
851 col.key || ('column ' + i)
854 node.setAttribute('title', title);
855 // To combat VoiceOver from reading the sort title as the
857 node.setAttribute('aria-labelledby', col.id);
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)`
871 _validateSortable: function (val) {
872 return val === 'auto' || isBoolean(val) || isArray(val);
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
884 _validateSortBy: function (val) {
885 return val === null ||
887 isObject(val, true) ||
888 (isArray(val) && (isString(val[0]) || isObject(val, true)));
893 Y.DataTable.Sortable = Sortable;
895 Y.Base.mix(Y.DataTable, [Sortable]);
898 }, '3.7.2', {"requires": ["datatable-base"], "lang": ["en"], "skinnable": true});