/* YUI 3.17.2 (build 9c3c78e) Copyright 2014 Yahoo! Inc. All rights reserved. Licensed under the BSD License. http://yuilibrary.com/license/ */ YUI.add('datatable-sort', function (Y, NAME) { /** Adds support for sorting the table data by API methods `table.sort(...)` or `table.toggleSort(...)` or by clicking on column headers in the rendered UI. @module datatable @submodule datatable-sort @since 3.5.0 **/ var YLang = Y.Lang, isBoolean = YLang.isBoolean, isString = YLang.isString, isArray = YLang.isArray, isObject = YLang.isObject, toArray = Y.Array, sub = YLang.sub, dirMap = { asc : 1, desc: -1, "1" : 1, "-1": -1 }; /** _API docs for this extension are included in the DataTable class._ This DataTable class extension adds support for sorting the table data by API methods `table.sort(...)` or `table.toggleSort(...)` or by clicking on column headers in the rendered UI. Sorting by the API is enabled automatically when this module is `use()`d. To enable UI triggered sorting, set the DataTable's `sortable` attribute to `true`.
var table = new Y.DataTable({
columns: [ 'id', 'username', 'name', 'birthdate' ],
data: [ ... ],
sortable: true
});
table.render('#table');
Setting `sortable` to `true` will enable UI sorting for all columns. To enable
UI sorting for certain columns only, set `sortable` to an array of column keys,
or just add `sortable: true` to the respective column configuration objects.
This uses the default setting of `sortable: auto` for the DataTable instance.
var table = new Y.DataTable({
columns: [
'id',
{ key: 'username', sortable: true },
{ key: 'name', sortable: true },
{ key: 'birthdate', sortable: true }
],
data: [ ... ]
// sortable: 'auto' is the default
});
// OR
var table = new Y.DataTable({
columns: [ 'id', 'username', 'name', 'birthdate' ],
data: [ ... ],
sortable: [ 'username', 'name', 'birthdate' ]
});
To disable UI sorting for all columns, set `sortable` to `false`. This still
permits sorting via the API methods.
As new records are inserted into the table's `data` ModelList, they will be inserted at the correct index to preserve the sort order.
The current sort order is stored in the `sortBy` attribute. Assigning this value at instantiation will automatically sort your data.
Sorting is done by a simple value comparison using < and > on the field
value. If you need custom sorting, add a sort function in the column's
`sortFn` property. Columns whose content is generated by formatters, but don't
relate to a single `key`, require a `sortFn` to be sortable.
function nameSort(a, b, desc) {
var aa = a.get('lastName') + a.get('firstName'),
bb = a.get('lastName') + b.get('firstName'),
order = (aa > bb) ? 1 : -(aa < bb);
return desc ? -order : order;
}
var table = new Y.DataTable({
columns: [ 'id', 'username', { key: name, sortFn: nameSort }, 'birthdate' ],
data: [ ... ],
sortable: [ 'username', 'name', 'birthdate' ]
});
See the user guide for more details.
@class DataTable.Sortable
@for DataTable
@since 3.5.0
**/
function Sortable() {}
Sortable.ATTRS = {
// Which columns in the UI should suggest and respond to sorting interaction
// pass an empty array if no UI columns should show sortable, but you want the
// table.sort(...) API
/**
Controls which column headers can trigger sorting by user clicks.
Acceptable values are:
* "auto" - (default) looks for `sortable: true` in the column configurations
* `true` - all columns are enabled
* `false - no UI sortable is enabled
* {String[]} - array of key names to give sortable headers
@attribute sortable
@type {String|String[]|Boolean}
@default "auto"
@since 3.5.0
**/
sortable: {
value: 'auto',
validator: '_validateSortable'
},
/**
The current sort configuration to maintain in the data.
Accepts column `key` strings or objects with a single property, the column
`key`, with a value of 1, -1, "asc", or "desc". E.g. `{ username: 'asc'
}`. String values are assumed to be ascending.
Example values would be:
* `"username"` - sort by the data's `username` field or the `key`
associated to a column with that `name`.
* `{ username: "desc" }` - sort by `username` in descending order.
Alternately, use values "asc", 1 (same as "asc"), or -1 (same as "desc").
* `["lastName", "firstName"]` - ascending sort by `lastName`, but for
records with the same `lastName`, ascending subsort by `firstName`.
Array can have as many items as you want.
* `[{ lastName: -1 }, "firstName"]` - descending sort by `lastName`,
ascending subsort by `firstName`. Mixed types are ok.
@attribute sortBy
@type {String|String[]|Object|Object[]}
@since 3.5.0
**/
sortBy: {
validator: '_validateSortBy',
getter: '_getSortBy'
},
/**
Strings containing language for sorting tooltips.
@attribute strings
@type {Object}
@default (strings for current lang configured in the YUI instance config)
@since 3.5.0
**/
strings: {}
};
Y.mix(Sortable.prototype, {
/**
Sort the data in the `data` ModelList and refresh the table with the new
order.
Acceptable values for `fields` are `key` strings or objects with a single
property, the column `key`, with a value of 1, -1, "asc", or "desc". E.g.
`{ username: 'asc' }`. String values are assumed to be ascending.
Example values would be:
* `"username"` - sort by the data's `username` field or the `key`
associated to a column with that `name`.
* `{ username: "desc" }` - sort by `username` in descending order.
Alternately, use values "asc", 1 (same as "asc"), or -1 (same as "desc").
* `["lastName", "firstName"]` - ascending sort by `lastName`, but for
records with the same `lastName`, ascending subsort by `firstName`.
Array can have as many items as you want.
* `[{ lastName: -1 }, "firstName"]` - descending sort by `lastName`,
ascending subsort by `firstName`. Mixed types are ok.
@method sort
@param {String|String[]|Object|Object[]} fields The field(s) to sort by
@param {Object} [payload] Extra `sort` event payload you want to send along
@return {DataTable}
@chainable
@since 3.5.0
**/
sort: function (fields, payload) {
/**
Notifies of an impending sort, either from clicking on a column
header, or from a call to the `sort` or `toggleSort` method.
The requested sort is available in the `sortBy` property of the event.
The default behavior of this event sets the table's `sortBy` attribute.
@event sort
@param {String|String[]|Object|Object[]} sortBy The requested sort
@preventable _defSortFn
**/
return this.fire('sort', Y.merge((payload || {}), {
sortBy: fields || this.get('sortBy')
}));
},
/**
Template for the node that will wrap the header content for sortable
columns.
@property SORTABLE_HEADER_TEMPLATE
@type {String}
@value 'var table = new Y.DataTable({
columns: [ ... ],
data: [ ... ],
sortBy: 'username'
});
table.get('sortBy'); // 'username'
table.get('sortBy.state'); // { key: 'username', dir: 1 }
table.sort(['lastName', { firstName: "desc" }]);
table.get('sortBy'); // ['lastName', { firstName: "desc" }]
table.get('sortBy.state'); // [{ key: "lastName", dir: 1 }, { key: "firstName", dir: -1 }]
@method _getSortBy
@param {String|String[]|Object|Object[]} val The current sortBy value
@param {String} detail String passed to `get(HERE)`. to parse subattributes
@protected
@since 3.5.0
**/
_getSortBy: function (val, detail) {
var state, i, len, col;
// "sortBy." is 7 characters. Used to catch
detail = detail.slice(7);
// TODO: table.get('sortBy.asObject')? table.get('sortBy.json')?
if (detail === 'state') {
state = [];
for (i = 0, len = this._sortBy.length; i < len; ++i) {
col = this._sortBy[i];
state.push({
column: col._id,
dir: col.sortDir
});
}
// TODO: Always return an array?
return { state: (state.length === 1) ? state[0] : state };
} else {
return val;
}
},
/**
Sets up the initial sort state and instance properties. Publishes events
and subscribes to attribute change events to maintain internal state.
@method initializer
@protected
@since 3.5.0
**/
initializer: function () {
var boundParseSortable = Y.bind('_parseSortable', this);
this._parseSortable();
this._setSortBy();
this._initSortFn();
this._initSortStrings();
this.after({
'table:renderHeader': Y.bind('_renderSortable', this),
dataChange : Y.bind('_afterSortDataChange', this),
sortByChange : Y.bind('_afterSortByChange', this),
sortableChange : boundParseSortable,
columnsChange : boundParseSortable
});
this.data.after(this.data.model.NAME + ":change",
Y.bind('_afterSortRecordChange', this));
// TODO: this event needs magic, allowing async remote sorting
this.publish('sort', {
defaultFn: Y.bind('_defSortFn', this)
});
},
/**
Creates a `_compare` function for the `data` ModelList to allow custom
sorting by multiple fields.
@method _initSortFn
@protected
@since 3.5.0
**/
_initSortFn: function () {
var self = this;
// TODO: This should be a ModelList extension.
// FIXME: Modifying a component of the host seems a little smelly
// FIXME: Declaring inline override to leverage closure vs
// compiling a new function for each column/sortable change or
// binding the _compare implementation to this, resulting in an
// extra function hop during sorting. Lesser of three evils?
this.data._compare = function (a, b) {
var cmp = 0,
i, len, col, dir, cs, aa, bb;
for (i = 0, len = self._sortBy.length; !cmp && i < len; ++i) {
col = self._sortBy[i];
dir = col.sortDir,
cs = col.caseSensitive;
if (col.sortFn) {
cmp = col.sortFn(a, b, (dir === -1));
} else {
// FIXME? Requires columns without sortFns to have key
aa = a.get(col.key) || '';
bb = b.get(col.key) || '';
if (!cs && typeof(aa) === "string" && typeof(bb) === "string"){// Not case sensitive
aa = aa.toLowerCase();
bb = bb.toLowerCase();
}
cmp = (aa > bb) ? dir : ((aa < bb) ? -dir : 0);
}
}
return cmp;
};
if (this._sortBy.length) {
this.data.comparator = this._sortComparator;
// TODO: is this necessary? Should it be elsewhere?
this.data.sort();
} else {
// Leave the _compare method in place to avoid having to set it
// up again. Mistake?
delete this.data.comparator;
}
},
/**
Add the sort related strings to the `strings` map.
@method _initSortStrings
@protected
@since 3.5.0
**/
_initSortStrings: function () {
// Not a valueFn because other class extensions will want to add to it
this.set('strings', Y.mix((this.get('strings') || {}),
Y.Intl.get('datatable-sort')));
},
/**
Fires the `sort` event in response to user clicks on sortable column
headers.
@method _onUITriggerSort
@param {DOMEventFacade} e The `click` event
@protected
@since 3.5.0
**/
_onUITriggerSort: function (e) {
var id = e.currentTarget.getAttribute('data-yui3-col-id'),
column = id && this.getColumn(id),
sortBy, i, len;
if (e.type === 'keydown' && e.keyCode !== 32) {
return;
}
// In case a headerTemplate injected a link
// TODO: Is this overreaching?
e.preventDefault();
if (column) {
if (e.shiftKey) {
sortBy = this.get('sortBy') || [];
for (i = 0, len = sortBy.length; i < len; ++i) {
if (id === sortBy[i] || Math.abs(sortBy[i][id]) === 1) {
if (!isObject(sortBy[i])) {
sortBy[i] = {};
}
sortBy[i][id] = -(column.sortDir||0) || 1;
break;
}
}
if (i >= len) {
sortBy.push(column._id);
}
} else {
sortBy = [{}];
sortBy[0][id] = -(column.sortDir||0) || 1;
}
this.fire('sort', {
originEvent: e,
sortBy: sortBy
});
}
},
/**
Normalizes the possible input values for the `sortable` attribute, storing
the results in the `_sortable` property.
@method _parseSortable
@protected
@since 3.5.0
**/
_parseSortable: function () {
var sortable = this.get('sortable'),
columns = [],
i, len, col;
if (isArray(sortable)) {
for (i = 0, len = sortable.length; i < len; ++i) {
col = sortable[i];
// isArray is called because arrays are objects, but will rely
// on getColumn to nullify them for the subsequent if (col)
if (!isObject(col, true) || isArray(col)) {
col = this.getColumn(col);
}
if (col) {
columns.push(col);
}
}
} else if (sortable) {
columns = this._displayColumns.slice();
if (sortable === 'auto') {
for (i = columns.length - 1; i >= 0; --i) {
if (!columns[i].sortable) {
columns.splice(i, 1);
}
}
}
}
this._sortable = columns;
},
/**
Initial application of the sortable UI.
@method _renderSortable
@protected
@since 3.5.0
**/
_renderSortable: function () {
this._uiSetSortable();
this._bindSortUI();
},
/**
Parses the current `sortBy` attribute into a normalized structure for the
`data` ModelList's `_compare` method. Also updates the column
configurations' `sortDir` properties.
@method _setSortBy
@protected
@since 3.5.0
**/
_setSortBy: function () {
var columns = this._displayColumns,
sortBy = this.get('sortBy') || [],
sortedClass = ' ' + this.getClassName('sorted'),
i, len, name, dir, field, column;
this._sortBy = [];
// Purge current sort state from column configs
for (i = 0, len = columns.length; i < len; ++i) {
column = columns[i];
delete column.sortDir;
if (column.className) {
// TODO: be more thorough
column.className = column.className.replace(sortedClass, '');
}
}
sortBy = toArray(sortBy);
for (i = 0, len = sortBy.length; i < len; ++i) {
name = sortBy[i];
dir = 1;
if (isObject(name)) {
field = name;
// Have to use a for-in loop to process sort({ foo: -1 })
for (name in field) {
if (field.hasOwnProperty(name)) {
dir = dirMap[field[name]];
break;
}
}
}
if (name) {
// Allow sorting of any model field and any column
// FIXME: this isn't limited to model attributes, but there's no
// convenient way to get a list of the attributes for a Model
// subclass *including* the attributes of its superclasses.
column = this.getColumn(name) || { _id: name, key: name };
if (column) {
column.sortDir = dir;
if (!column.className) {
column.className = '';
}
column.className += sortedClass;
this._sortBy.push(column);
}
}
}
},
/**
Array of column configuration objects of those columns that need UI setup
for user interaction.
@property _sortable
@type {Object[]}
@protected
@since 3.5.0
**/
//_sortable: null,
/**
Array of column configuration objects for those columns that are currently
being used to sort the data. Fake column objects are used for fields that
are not rendered as columns.
@property _sortBy
@type {Object[]}
@protected
@since 3.5.0
**/
//_sortBy: null,
/**
Replacement `comparator` for the `data` ModelList that defers sorting logic
to the `_compare` method. The deferral is accomplished by returning `this`.
@method _sortComparator
@param {Model} item The record being evaluated for sort position
@return {Model} The record
@protected
@since 3.5.0
**/
_sortComparator: function (item) {
// Defer sorting to ModelList's _compare
return item;
},
/**
Applies the appropriate classes to the `boundingBox` and column headers to
indicate sort state and sortability.
Also currently wraps the header content of sortable columns in a `