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) {
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 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'));
380 Sets the `sortBy` attribute from the `sort` event's `e.sortBy` value.
383 @param {EventFacade} e The `sort` event
387 _defSortFn: function (e) {
388 this.set.apply(this, ['sortBy', e.sortBy].concat(e.details));
392 Removes the click subscription from the header for sorting.
398 destructor: function () {
399 if (this._sortHandle) {
400 this._sortHandle.detach();
405 Getter for the `sortBy` attribute.
407 Supports the special subattribute "sortBy.state" to get a normalized JSON
408 version of the current sort state. Otherwise, returns the last assigned
413 <pre><code>var table = new Y.DataTable({
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 }]
428 @param {String|String[]|Object|Object[]} val The current sortBy value
429 @param {String} detail String passed to `get(HERE)`. to parse subattributes
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') {
443 for (i = 0, len = this._sortBy.length; i < len; ++i) {
444 col = this._sortBy[i];
451 // TODO: Always return an array?
452 return { state: (state.length === 1) ? state[0] : state };
459 Sets up the initial sort state and instance properties. Publishes events
460 and subscribes to attribute change events to maintain internal state.
466 initializer: function () {
467 var boundParseSortable = Y.bind('_parseSortable', this);
469 this._parseSortable();
475 this._initSortStrings();
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)
486 this.publish('sort', {
487 defaultFn: Y.bind('_defSortFn', this)
492 Creates a `_compare` function for the `data` ModelList to allow custom
493 sorting by multiple fields.
499 _initSortFn: function () {
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) {
510 i, len, col, dir, aa, bb;
512 for (i = 0, len = self._sortBy.length; !cmp && i < len; ++i) {
513 col = self._sortBy[i];
517 cmp = col.sortFn(a, b, (dir === -1));
519 // FIXME? Requires columns without sortFns to have key
523 cmp = (aa > bb) ? dir : ((aa < bb) ? -dir : 0);
530 if (this._sortBy.length) {
531 this.data.comparator = this._sortComparator;
533 // TODO: is this necessary? Should it be elsewhere?
536 // Leave the _compare method in place to avoid having to set it
537 // up again. Mistake?
538 delete this.data.comparator;
543 Add the sort related strings to the `strings` map.
545 @method _initSortStrings
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')));
556 Fires the `sort` event in response to user clicks on sortable column
559 @method _onUITriggerSort
560 @param {DOMEventFacade} e The `click` event
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),
570 if (e.type === 'keydown' && e.keyCode !== 32) {
574 // In case a headerTemplate injected a link
575 // TODO: Is this overreaching?
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])) {
586 sortBy[i][id] = -(column.sortDir|0) || 1;
592 sortBy.push(column._id);
595 sortBy[0][id] = -(column.sortDir|0) || 1;
606 Normalizes the possible input values for the `sortable` attribute, storing
607 the results in the `_sortable` property.
609 @method _parseSortable
613 _parseSortable: function () {
614 var sortable = this.get('sortable'),
618 if (isArray(sortable)) {
619 for (i = 0, len = sortable.length; i < len; ++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);
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);
644 this._sortable = columns;
648 Initial application of the sortable UI.
650 @method _renderSortable
654 _renderSortable: function () {
655 this._uiSetSortable();
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.
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;
677 // Purge current sort state from column configs
678 for (i = 0, len = columns.length; i < len; ++i) {
681 delete column.sortDir;
683 if (column.className) {
684 // TODO: be more thorough
685 column.className = column.className.replace(sortedClass, '');
689 sortBy = toArray(sortBy);
691 for (i = 0, len = sortBy.length; i < len; ++i) {
695 if (isObject(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]];
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 };
714 column.sortDir = dir;
716 if (!column.className) {
717 column.className = '';
720 column.className += sortedClass;
722 this._sortBy.push(column);
729 Array of column configuration objects of those columns that need UI setup
730 for user interaction.
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.
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
761 _sortComparator: function (item) {
762 // Defer sorting to ModelList's _compare
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
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'),
785 i, len, col, node, liner, title, desc;
787 this.get('boundingBox').toggleClass(
788 this.getClassName('sortable'),
791 for (i = 0, len = columns.length; i < len; ++i) {
792 sortableCols[columns[i].id] = columns[i];
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),
803 node.removeClass(ascClass)
804 .removeClass(descClass);
807 node.removeClass(sortableClass)
808 .removeClass(ascClass)
809 .removeClass(descClass);
812 liner.replace(liner.get('childNodes').toFrag());
815 indicator = node.one('.' + indicatorClass);
818 indicator.remove().destroy(true);
823 for (i = 0, len = columns.length; i < len; ++i) {
825 node = this._theadNode.one('#' + col.id);
826 desc = col.sortDir === -1;
829 liner = node.one('.' + linerClass);
831 node.addClass(sortableClass);
834 node.addClass(ascClass);
836 node.toggleClass(descClass, desc);
838 node.setAttribute('aria-sort', desc ?
839 'descending' : 'ascending');
843 liner = Y.Node.create(Y.Lang.sub(
844 this.SORTABLE_HEADER_TEMPLATE, {
845 className: linerClass,
846 indicatorClass: indicatorClass
849 liner.prepend(node.get('childNodes').toFrag());
854 title = sub(this.getString(
855 (col.sortDir === 1) ? 'reverseSortBy' : 'sortBy'), {
856 column: col.abbr || col.label ||
857 col.key || ('column ' + i)
860 node.setAttribute('title', title);
861 // To combat VoiceOver from reading the sort title as the
863 node.setAttribute('aria-labelledby', col.id);
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)`
877 _validateSortable: function (val) {
878 return val === 'auto' || isBoolean(val) || isArray(val);
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
890 _validateSortBy: function (val) {
891 return val === null ||
893 isObject(val, true) ||
894 (isArray(val) && (isString(val[0]) || isObject(val, true)));
899 Y.DataTable.Sortable = Sortable;
901 Y.Base.mix(Y.DataTable, [Sortable]);
904 }, '3.5.0' ,{lang:['en'], requires:['datatable-base']});