- /**
- 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`.
- <pre><code>
- var table = new Y.DataTable({
- columns: [ 'id', 'username', 'name', 'birthdate' ],
- data: [ ... ],
- sortable: true
- });
- table.render('#table');
- </code></pre>
- 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.
- <pre><code>
- 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' ]
- });
- </code></pre>
- 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.
- <pre><code>
- 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' ]
- });
- </code></pre>
- 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 {HTML}
- @value '<div class="{className}" tabindex="0"><span class="{indicatorClass}"></span></div>'
- @since 3.5.0
- **/
- SORTABLE_HEADER_TEMPLATE: '<div class="{className}" tabindex="0"><span class="{indicatorClass}"></span></div>',
- /**
- Reverse the current sort direction of one or more fields currently being
- sorted by.
- Pass the `key` of the column or columns you want the sort order reversed
- for.
- @method toggleSort
- @param {String|String[]} fields The field(s) to reverse sort order for
- @param {Object} [payload] Extra `sort` event payload you want to send along
- @return {DataTable}
- @chainable
- @since 3.5.0
- **/
- toggleSort: function (columns, payload) {
- var current = this._sortBy,
- sortBy = [],
- i, len, j, col, index;
- // To avoid updating column configs or sortBy directly
- for (i = 0, len = current.length; i < len; ++i) {
- col = {};
- col[current[i]._id] = current[i].sortDir;
- sortBy.push(col);
- }
- if (columns) {
- columns = toArray(columns);
- for (i = 0, len = columns.length; i < len; ++i) {
- col = columns[i];
- index = -1;
- for (j = sortBy.length - 1; i >= 0; --i) {
- if (sortBy[j][col]) {
- sortBy[j][col] *= -1;
- break;
- }
- }
- }
- } else {
- for (i = 0, len = sortBy.length; i < len; ++i) {
- for (col in sortBy[i]) {
- if (sortBy[i].hasOwnProperty(col)) {
- sortBy[i][col] *= -1;
- break;
- }
- }
- }
- }
- return this.fire('sort', Y.merge((payload || {}), {
- sortBy: sortBy
- }));
- },
- //--------------------------------------------------------------------------
- // Protected properties and methods
- //--------------------------------------------------------------------------
- /**
- Sorts the `data` ModelList based on the new `sortBy` configuration.
- @method _afterSortByChange
- @param {EventFacade} e The `sortByChange` event
- @protected
- @since 3.5.0
- **/
- _afterSortByChange: function (e) {
- // Can't use a setter because it's a chicken and egg problem. The
- // columns need to be set up to translate, but columns are initialized
- // from Core's initializer. So construction-time assignment would
- // fail.
- this._setSortBy();
- // Don't sort unless sortBy has been set
- if (this._sortBy.length) {
- if (!this.data.comparator) {
- this.data.comparator = this._sortComparator;
- }
- this.data.sort();
- }
- },
- /**
- Applies the sorting logic to the new ModelList if the `newVal` is a new
- ModelList.
- @method _afterSortDataChange
- @param {EventFacade} e the `dataChange` event
- @protected
- @since 3.5.0
- **/
- _afterSortDataChange: function (e) {
- // object values always trigger a change event, but we only want to
- // call _initSortFn if the value passed to the `data` attribute was a
- // new ModelList, not a set of new data as an array, or even the same
- // ModelList.
- if (e.prevVal !== e.newVal || e.newVal.hasOwnProperty('_compare')) {
- this._initSortFn();
- }
- },
- /**
- Checks if any of the fields in the modified record are fields that are
- currently being sorted by, and if so, resorts the `data` ModelList.
- @method _afterSortRecordChange
- @param {EventFacade} e The Model's `change` event
- @protected
- @since 3.5.0
- **/
- _afterSortRecordChange: function (e) {
- var i, len;
- for (i = 0, len = this._sortBy.length; i < len; ++i) {
- if (e.changed[this._sortBy[i].key]) {
- this.data.sort();
- break;
- }
- }
- },
- /**
- Subscribes to state changes that warrant updating the UI, and adds the
- click handler for triggering the sort operation from the UI.
- @method _bindSortUI
- @protected
- @since 3.5.0
- **/
- _bindSortUI: function () {
- var handles = this._eventHandles;
-
- if (!handles.sortAttrs) {
- handles.sortAttrs = this.after(
- ['sortableChange', 'sortByChange', 'columnsChange'],
- Y.bind('_uiSetSortable', this));
- }
- if (!handles.sortUITrigger && this._theadNode) {
- handles.sortUITrigger = this.delegate(['click','keydown'],
- Y.rbind('_onUITriggerSort', this),
- '.' + this.getClassName('sortable', 'column'));
- }
- },
- /**
- Sets the `sortBy` attribute from the `sort` event's `e.sortBy` value.
- @method _defSortFn
- @param {EventFacade} e The `sort` event
- @protected
- @since 3.5.0
- **/
- _defSortFn: function (e) {
- this.set.apply(this, ['sortBy', e.sortBy].concat(e.details));
- },
- /**
- Getter for the `sortBy` attribute.
-
- Supports the special subattribute "sortBy.state" to get a normalized JSON
- version of the current sort state. Otherwise, returns the last assigned
- value.
- For example:
- <pre><code>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 }]
- </code></pre>
- @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, aa, bb;
- for (i = 0, len = self._sortBy.length; !cmp && i < len; ++i) {
- col = self._sortBy[i];
- dir = col.sortDir;
- 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) || '';
- 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'),
- sortBy = e.shiftKey ? this.get('sortBy') : [{}],
- column = id && this.getColumn(id),
- 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) {
- 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[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 `<div>`
- liner to give a CSS anchor for sort indicators.
- @method _uiSetSortable
- @protected
- @since 3.5.0
- **/
- _uiSetSortable: function () {
- var columns = this._sortable || [],
- sortableClass = this.getClassName('sortable', 'column'),
- ascClass = this.getClassName('sorted'),
- descClass = this.getClassName('sorted', 'desc'),
- linerClass = this.getClassName('sort', 'liner'),
- indicatorClass= this.getClassName('sort', 'indicator'),
- sortableCols = {},
- i, len, col, node, liner, title, desc;
- this.get('boundingBox').toggleClass(
- this.getClassName('sortable'),
- columns.length);
- for (i = 0, len = columns.length; i < len; ++i) {
- sortableCols[columns[i].id] = columns[i];
- }
- // TODO: this.head.render() + decorate cells?
- this._theadNode.all('.' + sortableClass).each(function (node) {
- var col = sortableCols[node.get('id')],
- liner = node.one('.' + linerClass),
- indicator;
- if (col) {
- if (!col.sortDir) {
- node.removeClass(ascClass)
- .removeClass(descClass);
- }
- } else {
- node.removeClass(sortableClass)
- .removeClass(ascClass)
- .removeClass(descClass);
- if (liner) {
- liner.replace(liner.get('childNodes').toFrag());
- }
- indicator = node.one('.' + indicatorClass);
- if (indicator) {
- indicator.remove().destroy(true);
- }
- }
- });
- for (i = 0, len = columns.length; i < len; ++i) {
- col = columns[i];
- node = this._theadNode.one('#' + col.id);
- desc = col.sortDir === -1;
- if (node) {
- liner = node.one('.' + linerClass);
- node.addClass(sortableClass);
- if (col.sortDir) {
- node.addClass(ascClass);
- node.toggleClass(descClass, desc);
- node.setAttribute('aria-sort', desc ?
- 'descending' : 'ascending');
- }
- if (!liner) {
- liner = Y.Node.create(Y.Lang.sub(
- this.SORTABLE_HEADER_TEMPLATE, {
- className: linerClass,
- indicatorClass: indicatorClass
- }));
- liner.prepend(node.get('childNodes').toFrag());
- node.append(liner);
- }
- title = sub(this.getString(
- (col.sortDir === 1) ? 'reverseSortBy' : 'sortBy'), {
- column: col.abbr || col.label ||
- col.key || ('column ' + i)
- });
- node.setAttribute('title', title);
- // To combat VoiceOver from reading the sort title as the
- // column header
- node.setAttribute('aria-labelledby', col.id);
- }
- }
- },
- /**
- Allows values `true`, `false`, "auto", or arrays of column names through.
- @method _validateSortable
- @param {Any} val The input value to `set("sortable", VAL)`
- @return {Boolean}
- @protected
- @since 3.5.0
- **/
- _validateSortable: function (val) {
- return val === 'auto' || isBoolean(val) || isArray(val);
- },
- /**
- Allows strings, arrays of strings, objects, or arrays of objects.
- @method _validateSortBy
- @param {String|String[]|Object|Object[]} val The new `sortBy` value
- @return {Boolean}
- @protected
- @since 3.5.0
- **/
- _validateSortBy: function (val) {
- return val === null ||
- isString(val) ||
- isObject(val, true) ||
- (isArray(val) && (isString(val[0]) || isObject(val, true)));
- }
- }, true);
- Y.DataTable.Sortable = Sortable;
- Y.Base.mix(Y.DataTable, [Sortable]);
-