| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678 |
- import moment from 'moment';
- import {
- addClass,
- hasClass,
- removeClass,
- } from './../../helpers/dom/element';
- import {arrayMap, arrayReduce} from './../../helpers/array';
- import {isEmpty} from './../../helpers/mixed';
- import {hasOwnProperty} from './../../helpers/object';
- import BasePlugin from './../_base';
- import {registerPlugin} from './../../plugins';
- import mergeSort from './../../utils/sortingAlgorithms/mergeSort';
- import Hooks from './../../pluginHooks';
- Hooks.getSingleton().register('beforeColumnSort');
- Hooks.getSingleton().register('afterColumnSort');
- // TODO: Implement mixin arrayMapper to ColumnSorting plugin.
- /**
- * @plugin ColumnSorting
- *
- * @description
- * This plugin sorts the view by a column (but does not sort the data source!).
- * To enable the plugin, set the `columnSorting` property to either:
- * * a boolean value (`true`/`false`),
- * * an object defining the initial sorting order (see the example below).
- *
- * @example
- * ```js
- * ...
- * // as boolean
- * columnSorting: true
- * ...
- * // as a object with initial order (sort ascending column at index 2)
- * columnSorting: {
- * column: 2,
- * sortOrder: true, // true = ascending, false = descending, undefined = original order
- * sortEmptyCells: true // true = the table sorts empty cells, false = the table moves all empty cells to the end of the table
- * }
- * ...
- * ```
- * @dependencies ObserveChanges
- */
- class ColumnSorting extends BasePlugin {
- constructor(hotInstance) {
- super(hotInstance);
- this.sortIndicators = [];
- this.lastSortedColumn = null;
- this.sortEmptyCells = false;
- }
- /**
- * Check if the plugin is enabled in the handsontable settings.
- *
- * @returns {Boolean}
- */
- isEnabled() {
- return !!(this.hot.getSettings().columnSorting);
- }
- /**
- * Enable plugin for this Handsontable instance.
- */
- enablePlugin() {
- if (this.enabled) {
- return;
- }
- this.setPluginOptions();
- const _this = this;
- this.hot.sortIndex = [];
- this.hot.sort = function() {
- let args = Array.prototype.slice.call(arguments);
- return _this.sortByColumn(...args);
- };
- if (typeof this.hot.getSettings().observeChanges === 'undefined') {
- this.enableObserveChangesPlugin();
- }
- this.addHook('afterTrimRow', (row) => this.sort());
- this.addHook('afterUntrimRow', (row) => this.sort());
- this.addHook('modifyRow', (row) => this.translateRow(row));
- this.addHook('unmodifyRow', (row) => this.untranslateRow(row));
- this.addHook('afterUpdateSettings', () => this.onAfterUpdateSettings());
- this.addHook('afterGetColHeader', (col, TH) => this.getColHeader(col, TH));
- this.addHook('afterOnCellMouseDown', (event, target) => this.onAfterOnCellMouseDown(event, target));
- this.addHook('afterCreateRow', function() {
- _this.afterCreateRow(...arguments);
- });
- this.addHook('afterRemoveRow', function() {
- _this.afterRemoveRow(...arguments);
- });
- this.addHook('afterInit', () => this.sortBySettings());
- this.addHook('afterLoadData', () => {
- this.hot.sortIndex = [];
- if (this.hot.view) {
- this.sortBySettings();
- }
- });
- if (this.hot.view) {
- this.sortBySettings();
- }
- super.enablePlugin();
- }
- /**
- * Disable plugin for this Handsontable instance.
- */
- disablePlugin() {
- this.hot.sort = void 0;
- super.disablePlugin();
- }
- /**
- * afterUpdateSettings callback.
- *
- * @private
- */
- onAfterUpdateSettings() {
- this.sortBySettings();
- }
- sortBySettings() {
- let sortingSettings = this.hot.getSettings().columnSorting;
- let loadedSortingState = this.loadSortingState();
- let sortingColumn;
- let sortingOrder;
- if (typeof loadedSortingState === 'undefined') {
- sortingColumn = sortingSettings.column;
- sortingOrder = sortingSettings.sortOrder;
- } else {
- sortingColumn = loadedSortingState.sortColumn;
- sortingOrder = loadedSortingState.sortOrder;
- }
- if (typeof sortingColumn === 'number') {
- this.lastSortedColumn = sortingColumn;
- this.sortByColumn(sortingColumn, sortingOrder);
- }
- }
- /**
- * Set sorted column and order info
- *
- * @param {number} col Sorted column index.
- * @param {boolean|undefined} order Sorting order (`true` for ascending, `false` for descending).
- */
- setSortingColumn(col, order) {
- if (typeof col == 'undefined') {
- this.hot.sortColumn = void 0;
- this.hot.sortOrder = void 0;
- return;
- } else if (this.hot.sortColumn === col && typeof order == 'undefined') {
- if (this.hot.sortOrder === false) {
- this.hot.sortOrder = void 0;
- } else {
- this.hot.sortOrder = !this.hot.sortOrder;
- }
- } else {
- this.hot.sortOrder = typeof order === 'undefined' ? true : order;
- }
- this.hot.sortColumn = col;
- }
- sortByColumn(col, order) {
- this.setSortingColumn(col, order);
- if (typeof this.hot.sortColumn == 'undefined') {
- return;
- }
- let allowSorting = this.hot.runHooks('beforeColumnSort', this.hot.sortColumn, this.hot.sortOrder);
- if (allowSorting !== false) {
- this.sort();
- }
- this.updateOrderClass();
- this.updateSortIndicator();
- this.hot.runHooks('afterColumnSort', this.hot.sortColumn, this.hot.sortOrder);
- this.hot.render();
- this.saveSortingState();
- }
- /**
- * Save the sorting state
- */
- saveSortingState() {
- let sortingState = {};
- if (typeof this.hot.sortColumn != 'undefined') {
- sortingState.sortColumn = this.hot.sortColumn;
- }
- if (typeof this.hot.sortOrder != 'undefined') {
- sortingState.sortOrder = this.hot.sortOrder;
- }
- if (hasOwnProperty(sortingState, 'sortColumn') || hasOwnProperty(sortingState, 'sortOrder')) {
- this.hot.runHooks('persistentStateSave', 'columnSorting', sortingState);
- }
- }
- /**
- * Load the sorting state.
- *
- * @returns {*} Previously saved sorting state.
- */
- loadSortingState() {
- let storedState = {};
- this.hot.runHooks('persistentStateLoad', 'columnSorting', storedState);
- return storedState.value;
- }
- /**
- * Update sorting class name state.
- */
- updateOrderClass() {
- let orderClass;
- if (this.hot.sortOrder === true) {
- orderClass = 'ascending';
- } else if (this.hot.sortOrder === false) {
- orderClass = 'descending';
- }
- this.sortOrderClass = orderClass;
- }
- enableObserveChangesPlugin() {
- let _this = this;
- this.hot._registerTimeout(
- setTimeout(() => {
- _this.hot.updateSettings({
- observeChanges: true
- });
- }, 0));
- }
- /**
- * Default sorting algorithm.
- *
- * @param {Boolean} sortOrder Sorting order - `true` for ascending, `false` for descending.
- * @param {Object} columnMeta Column meta object.
- * @returns {Function} The comparing function.
- */
- defaultSort(sortOrder, columnMeta) {
- return function(a, b) {
- if (typeof a[1] == 'string') {
- a[1] = a[1].toLowerCase();
- }
- if (typeof b[1] == 'string') {
- b[1] = b[1].toLowerCase();
- }
- if (a[1] === b[1]) {
- return 0;
- }
- if (isEmpty(a[1])) {
- if (isEmpty(b[1])) {
- return 0;
- }
- if (columnMeta.columnSorting.sortEmptyCells) {
- return sortOrder ? -1 : 1;
- }
- return 1;
- }
- if (isEmpty(b[1])) {
- if (isEmpty(a[1])) {
- return 0;
- }
- if (columnMeta.columnSorting.sortEmptyCells) {
- return sortOrder ? 1 : -1;
- }
- return -1;
- }
- if (isNaN(a[1]) && !isNaN(b[1])) {
- return sortOrder ? 1 : -1;
- } else if (!isNaN(a[1]) && isNaN(b[1])) {
- return sortOrder ? -1 : 1;
- } else if (!(isNaN(a[1]) || isNaN(b[1]))) {
- a[1] = parseFloat(a[1]);
- b[1] = parseFloat(b[1]);
- }
- if (a[1] < b[1]) {
- return sortOrder ? -1 : 1;
- }
- if (a[1] > b[1]) {
- return sortOrder ? 1 : -1;
- }
- return 0;
- };
- }
- /**
- * Date sorting algorithm
- * @param {Boolean} sortOrder Sorting order (`true` for ascending, `false` for descending).
- * @param {Object} columnMeta Column meta object.
- * @returns {Function} The compare function.
- */
- dateSort(sortOrder, columnMeta) {
- return function(a, b) {
- if (a[1] === b[1]) {
- return 0;
- }
- if (isEmpty(a[1])) {
- if (isEmpty(b[1])) {
- return 0;
- }
- if (columnMeta.columnSorting.sortEmptyCells) {
- return sortOrder ? -1 : 1;
- }
- return 1;
- }
- if (isEmpty(b[1])) {
- if (isEmpty(a[1])) {
- return 0;
- }
- if (columnMeta.columnSorting.sortEmptyCells) {
- return sortOrder ? 1 : -1;
- }
- return -1;
- }
- var aDate = moment(a[1], columnMeta.dateFormat);
- var bDate = moment(b[1], columnMeta.dateFormat);
- if (!aDate.isValid()) {
- return 1;
- }
- if (!bDate.isValid()) {
- return -1;
- }
- if (bDate.isAfter(aDate)) {
- return sortOrder ? -1 : 1;
- }
- if (bDate.isBefore(aDate)) {
- return sortOrder ? 1 : -1;
- }
- return 0;
- };
- }
- /**
- * Numeric sorting algorithm.
- *
- * @param {Boolean} sortOrder Sorting order (`true` for ascending, `false` for descending).
- * @param {Object} columnMeta Column meta object.
- * @returns {Function} The compare function.
- */
- numericSort(sortOrder, columnMeta) {
- return function(a, b) {
- const parsedA = parseFloat(a[1]);
- const parsedB = parseFloat(b[1]);
- // Watch out when changing this part of code!
- // Check below returns 0 (as expected) when comparing empty string, null, undefined
- if (parsedA === parsedB || (isNaN(parsedA) && isNaN(parsedB))) {
- return 0;
- }
- if (columnMeta.columnSorting.sortEmptyCells) {
- if (isEmpty(a[1])) {
- return sortOrder ? -1 : 1;
- }
- if (isEmpty(b[1])) {
- return sortOrder ? 1 : -1;
- }
- }
- if (isNaN(parsedA)) {
- return 1;
- }
- if (isNaN(parsedB)) {
- return -1;
- }
- if (parsedA < parsedB) {
- return sortOrder ? -1 : 1;
- } else if (parsedA > parsedB) {
- return sortOrder ? 1 : -1;
- }
- return 0;
- };
- }
- /**
- * Perform the sorting.
- */
- sort() {
- if (typeof this.hot.sortOrder == 'undefined') {
- this.hot.sortIndex.length = 0;
- return;
- }
- const colMeta = this.hot.getCellMeta(0, this.hot.sortColumn);
- const emptyRows = this.hot.countEmptyRows();
- let sortFunction;
- let nrOfRows;
- this.hot.sortingEnabled = false; // this is required by translateRow plugin hook
- this.hot.sortIndex.length = 0;
- if (typeof colMeta.columnSorting.sortEmptyCells === 'undefined') {
- colMeta.columnSorting = {sortEmptyCells: this.sortEmptyCells};
- }
- if (this.hot.getSettings().maxRows === Number.POSITIVE_INFINITY) {
- nrOfRows = this.hot.countRows() - this.hot.getSettings().minSpareRows;
- } else {
- nrOfRows = this.hot.countRows() - emptyRows;
- }
- for (let i = 0, ilen = nrOfRows; i < ilen; i++) {
- this.hot.sortIndex.push([i, this.hot.getDataAtCell(i, this.hot.sortColumn)]);
- }
- if (colMeta.sortFunction) {
- sortFunction = colMeta.sortFunction;
- } else {
- switch (colMeta.type) {
- case 'date':
- sortFunction = this.dateSort;
- break;
- case 'numeric':
- sortFunction = this.numericSort;
- break;
- default:
- sortFunction = this.defaultSort;
- }
- }
- mergeSort(this.hot.sortIndex, sortFunction(this.hot.sortOrder, colMeta));
- // Append spareRows
- for (let i = this.hot.sortIndex.length; i < this.hot.countRows(); i++) {
- this.hot.sortIndex.push([i, this.hot.getDataAtCell(i, this.hot.sortColumn)]);
- }
- this.hot.sortingEnabled = true; // this is required by translateRow plugin hook
- }
- /**
- * Update indicator states.
- */
- updateSortIndicator() {
- if (typeof this.hot.sortOrder == 'undefined') {
- return;
- }
- const colMeta = this.hot.getCellMeta(0, this.hot.sortColumn);
- this.sortIndicators[this.hot.sortColumn] = colMeta.sortIndicator;
- }
- /**
- * `modifyRow` hook callback. Translates physical row index to the sorted row index.
- *
- * @param {Number} row Row index.
- * @returns {Number} Sorted row index.
- */
- translateRow(row) {
- if (this.hot.sortingEnabled && (typeof this.hot.sortOrder !== 'undefined') && this.hot.sortIndex && this.hot.sortIndex.length && this.hot.sortIndex[row]) {
- return this.hot.sortIndex[row][0];
- }
- return row;
- }
- /**
- * Translates sorted row index to physical row index.
- *
- * @param {Number} row Sorted row index.
- * @returns {number} Physical row index.
- */
- untranslateRow(row) {
- if (this.hot.sortingEnabled && this.hot.sortIndex && this.hot.sortIndex.length) {
- for (var i = 0; i < this.hot.sortIndex.length; i++) {
- if (this.hot.sortIndex[i][0] == row) {
- return i;
- }
- }
- }
- }
- /**
- * `afterGetColHeader` callback. Adds column sorting css classes to clickable headers.
- *
- * @private
- * @param {Number} col Column index.
- * @param {Element} TH TH HTML element.
- */
- getColHeader(col, TH) {
- if (col < 0 || !TH.parentNode) {
- return false;
- }
- let headerLink = TH.querySelector('.colHeader');
- let colspan = TH.getAttribute('colspan');
- let TRs = TH.parentNode.parentNode.childNodes;
- let headerLevel = Array.prototype.indexOf.call(TRs, TH.parentNode);
- headerLevel -= TRs.length;
- if (!headerLink) {
- return;
- }
- if (this.hot.getSettings().columnSorting && col >= 0 && headerLevel === -1) {
- addClass(headerLink, 'columnSorting');
- }
- removeClass(headerLink, 'descending');
- removeClass(headerLink, 'ascending');
- if (this.sortIndicators[col]) {
- if (col === this.hot.sortColumn) {
- if (this.sortOrderClass === 'ascending') {
- addClass(headerLink, 'ascending');
- } else if (this.sortOrderClass === 'descending') {
- addClass(headerLink, 'descending');
- }
- }
- }
- }
- /**
- * Check if any column is in a sorted state.
- *
- * @returns {Boolean}
- */
- isSorted() {
- return typeof this.hot.sortColumn != 'undefined';
- }
- /**
- * `afterCreateRow` callback. Updates the sorting state after a row have been created.
- *
- * @private
- * @param {Number} index
- * @param {Number} amount
- */
- afterCreateRow(index, amount) {
- if (!this.isSorted()) {
- return;
- }
- for (let i = 0; i < this.hot.sortIndex.length; i++) {
- if (this.hot.sortIndex[i][0] >= index) {
- this.hot.sortIndex[i][0] += amount;
- }
- }
- for (let i = 0; i < amount; i++) {
- this.hot.sortIndex.splice(index + i, 0, [index + i, this.hot.getSourceData()[index + i][this.hot.sortColumn + this.hot.colOffset()]]);
- }
- this.saveSortingState();
- }
- /**
- * `afterRemoveRow` hook callback.
- *
- * @private
- * @param {Number} index
- * @param {Number} amount
- */
- afterRemoveRow(index, amount) {
- if (!this.isSorted()) {
- return;
- }
- let removedRows = this.hot.sortIndex.splice(index, amount);
- removedRows = arrayMap(removedRows, (row) => row[0]);
- function countRowShift(logicalRow) {
- // Todo: compare perf between reduce vs sort->each->brake
- return arrayReduce(removedRows, (count, removedLogicalRow) => {
- if (logicalRow > removedLogicalRow) {
- count++;
- }
- return count;
- }, 0);
- }
- this.hot.sortIndex = arrayMap(this.hot.sortIndex, (logicalRow, physicalRow) => {
- let rowShift = countRowShift(logicalRow[0]);
- if (rowShift) {
- logicalRow[0] -= rowShift;
- }
- return logicalRow;
- });
- this.saveSortingState();
- }
- /**
- * Set options by passed settings
- *
- * @private
- */
- setPluginOptions() {
- const columnSorting = this.hot.getSettings().columnSorting;
- if (typeof columnSorting === 'object') {
- this.sortEmptyCells = columnSorting.sortEmptyCells || false;
- } else {
- this.sortEmptyCells = false;
- }
- }
- /**
- * `onAfterOnCellMouseDown` hook callback.
- *
- * @private
- * @param {Event} event Event which are provided by hook.
- * @param {CellCoords} coords Coords of the selected cell.
- */
- onAfterOnCellMouseDown(event, coords) {
- if (coords.row > -1) {
- return;
- }
- if (hasClass(event.realTarget, 'columnSorting')) {
- // reset order state on every new column header click
- if (coords.col !== this.lastSortedColumn) {
- this.hot.sortOrder = true;
- }
- this.lastSortedColumn = coords.col;
- this.sortByColumn(coords.col);
- }
- }
- }
- registerPlugin('columnSorting', ColumnSorting);
- export default ColumnSorting;
|