ec3ee99ac9ffebc28a78b063afad26df28d38986b2228043c91a69b3c7c3835f81f57a2657f5179f94f0e41f5af0c4fb8b9001ed0633253e7bb23ca9421315 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. import moment from 'moment';
  2. import {
  3. addClass,
  4. hasClass,
  5. removeClass,
  6. } from './../../helpers/dom/element';
  7. import {arrayMap, arrayReduce} from './../../helpers/array';
  8. import {isEmpty} from './../../helpers/mixed';
  9. import {hasOwnProperty} from './../../helpers/object';
  10. import BasePlugin from './../_base';
  11. import {registerPlugin} from './../../plugins';
  12. import mergeSort from './../../utils/sortingAlgorithms/mergeSort';
  13. import Hooks from './../../pluginHooks';
  14. Hooks.getSingleton().register('beforeColumnSort');
  15. Hooks.getSingleton().register('afterColumnSort');
  16. // TODO: Implement mixin arrayMapper to ColumnSorting plugin.
  17. /**
  18. * @plugin ColumnSorting
  19. *
  20. * @description
  21. * This plugin sorts the view by a column (but does not sort the data source!).
  22. * To enable the plugin, set the `columnSorting` property to either:
  23. * * a boolean value (`true`/`false`),
  24. * * an object defining the initial sorting order (see the example below).
  25. *
  26. * @example
  27. * ```js
  28. * ...
  29. * // as boolean
  30. * columnSorting: true
  31. * ...
  32. * // as a object with initial order (sort ascending column at index 2)
  33. * columnSorting: {
  34. * column: 2,
  35. * sortOrder: true, // true = ascending, false = descending, undefined = original order
  36. * sortEmptyCells: true // true = the table sorts empty cells, false = the table moves all empty cells to the end of the table
  37. * }
  38. * ...
  39. * ```
  40. * @dependencies ObserveChanges
  41. */
  42. class ColumnSorting extends BasePlugin {
  43. constructor(hotInstance) {
  44. super(hotInstance);
  45. this.sortIndicators = [];
  46. this.lastSortedColumn = null;
  47. this.sortEmptyCells = false;
  48. }
  49. /**
  50. * Check if the plugin is enabled in the handsontable settings.
  51. *
  52. * @returns {Boolean}
  53. */
  54. isEnabled() {
  55. return !!(this.hot.getSettings().columnSorting);
  56. }
  57. /**
  58. * Enable plugin for this Handsontable instance.
  59. */
  60. enablePlugin() {
  61. if (this.enabled) {
  62. return;
  63. }
  64. this.setPluginOptions();
  65. const _this = this;
  66. this.hot.sortIndex = [];
  67. this.hot.sort = function() {
  68. let args = Array.prototype.slice.call(arguments);
  69. return _this.sortByColumn(...args);
  70. };
  71. if (typeof this.hot.getSettings().observeChanges === 'undefined') {
  72. this.enableObserveChangesPlugin();
  73. }
  74. this.addHook('afterTrimRow', (row) => this.sort());
  75. this.addHook('afterUntrimRow', (row) => this.sort());
  76. this.addHook('modifyRow', (row) => this.translateRow(row));
  77. this.addHook('unmodifyRow', (row) => this.untranslateRow(row));
  78. this.addHook('afterUpdateSettings', () => this.onAfterUpdateSettings());
  79. this.addHook('afterGetColHeader', (col, TH) => this.getColHeader(col, TH));
  80. this.addHook('afterOnCellMouseDown', (event, target) => this.onAfterOnCellMouseDown(event, target));
  81. this.addHook('afterCreateRow', function() {
  82. _this.afterCreateRow(...arguments);
  83. });
  84. this.addHook('afterRemoveRow', function() {
  85. _this.afterRemoveRow(...arguments);
  86. });
  87. this.addHook('afterInit', () => this.sortBySettings());
  88. this.addHook('afterLoadData', () => {
  89. this.hot.sortIndex = [];
  90. if (this.hot.view) {
  91. this.sortBySettings();
  92. }
  93. });
  94. if (this.hot.view) {
  95. this.sortBySettings();
  96. }
  97. super.enablePlugin();
  98. }
  99. /**
  100. * Disable plugin for this Handsontable instance.
  101. */
  102. disablePlugin() {
  103. this.hot.sort = void 0;
  104. super.disablePlugin();
  105. }
  106. /**
  107. * afterUpdateSettings callback.
  108. *
  109. * @private
  110. */
  111. onAfterUpdateSettings() {
  112. this.sortBySettings();
  113. }
  114. sortBySettings() {
  115. let sortingSettings = this.hot.getSettings().columnSorting;
  116. let loadedSortingState = this.loadSortingState();
  117. let sortingColumn;
  118. let sortingOrder;
  119. if (typeof loadedSortingState === 'undefined') {
  120. sortingColumn = sortingSettings.column;
  121. sortingOrder = sortingSettings.sortOrder;
  122. } else {
  123. sortingColumn = loadedSortingState.sortColumn;
  124. sortingOrder = loadedSortingState.sortOrder;
  125. }
  126. if (typeof sortingColumn === 'number') {
  127. this.lastSortedColumn = sortingColumn;
  128. this.sortByColumn(sortingColumn, sortingOrder);
  129. }
  130. }
  131. /**
  132. * Set sorted column and order info
  133. *
  134. * @param {number} col Sorted column index.
  135. * @param {boolean|undefined} order Sorting order (`true` for ascending, `false` for descending).
  136. */
  137. setSortingColumn(col, order) {
  138. if (typeof col == 'undefined') {
  139. this.hot.sortColumn = void 0;
  140. this.hot.sortOrder = void 0;
  141. return;
  142. } else if (this.hot.sortColumn === col && typeof order == 'undefined') {
  143. if (this.hot.sortOrder === false) {
  144. this.hot.sortOrder = void 0;
  145. } else {
  146. this.hot.sortOrder = !this.hot.sortOrder;
  147. }
  148. } else {
  149. this.hot.sortOrder = typeof order === 'undefined' ? true : order;
  150. }
  151. this.hot.sortColumn = col;
  152. }
  153. sortByColumn(col, order) {
  154. this.setSortingColumn(col, order);
  155. if (typeof this.hot.sortColumn == 'undefined') {
  156. return;
  157. }
  158. let allowSorting = this.hot.runHooks('beforeColumnSort', this.hot.sortColumn, this.hot.sortOrder);
  159. if (allowSorting !== false) {
  160. this.sort();
  161. }
  162. this.updateOrderClass();
  163. this.updateSortIndicator();
  164. this.hot.runHooks('afterColumnSort', this.hot.sortColumn, this.hot.sortOrder);
  165. this.hot.render();
  166. this.saveSortingState();
  167. }
  168. /**
  169. * Save the sorting state
  170. */
  171. saveSortingState() {
  172. let sortingState = {};
  173. if (typeof this.hot.sortColumn != 'undefined') {
  174. sortingState.sortColumn = this.hot.sortColumn;
  175. }
  176. if (typeof this.hot.sortOrder != 'undefined') {
  177. sortingState.sortOrder = this.hot.sortOrder;
  178. }
  179. if (hasOwnProperty(sortingState, 'sortColumn') || hasOwnProperty(sortingState, 'sortOrder')) {
  180. this.hot.runHooks('persistentStateSave', 'columnSorting', sortingState);
  181. }
  182. }
  183. /**
  184. * Load the sorting state.
  185. *
  186. * @returns {*} Previously saved sorting state.
  187. */
  188. loadSortingState() {
  189. let storedState = {};
  190. this.hot.runHooks('persistentStateLoad', 'columnSorting', storedState);
  191. return storedState.value;
  192. }
  193. /**
  194. * Update sorting class name state.
  195. */
  196. updateOrderClass() {
  197. let orderClass;
  198. if (this.hot.sortOrder === true) {
  199. orderClass = 'ascending';
  200. } else if (this.hot.sortOrder === false) {
  201. orderClass = 'descending';
  202. }
  203. this.sortOrderClass = orderClass;
  204. }
  205. enableObserveChangesPlugin() {
  206. let _this = this;
  207. this.hot._registerTimeout(
  208. setTimeout(() => {
  209. _this.hot.updateSettings({
  210. observeChanges: true
  211. });
  212. }, 0));
  213. }
  214. /**
  215. * Default sorting algorithm.
  216. *
  217. * @param {Boolean} sortOrder Sorting order - `true` for ascending, `false` for descending.
  218. * @param {Object} columnMeta Column meta object.
  219. * @returns {Function} The comparing function.
  220. */
  221. defaultSort(sortOrder, columnMeta) {
  222. return function(a, b) {
  223. if (typeof a[1] == 'string') {
  224. a[1] = a[1].toLowerCase();
  225. }
  226. if (typeof b[1] == 'string') {
  227. b[1] = b[1].toLowerCase();
  228. }
  229. if (a[1] === b[1]) {
  230. return 0;
  231. }
  232. if (isEmpty(a[1])) {
  233. if (isEmpty(b[1])) {
  234. return 0;
  235. }
  236. if (columnMeta.columnSorting.sortEmptyCells) {
  237. return sortOrder ? -1 : 1;
  238. }
  239. return 1;
  240. }
  241. if (isEmpty(b[1])) {
  242. if (isEmpty(a[1])) {
  243. return 0;
  244. }
  245. if (columnMeta.columnSorting.sortEmptyCells) {
  246. return sortOrder ? 1 : -1;
  247. }
  248. return -1;
  249. }
  250. if (isNaN(a[1]) && !isNaN(b[1])) {
  251. return sortOrder ? 1 : -1;
  252. } else if (!isNaN(a[1]) && isNaN(b[1])) {
  253. return sortOrder ? -1 : 1;
  254. } else if (!(isNaN(a[1]) || isNaN(b[1]))) {
  255. a[1] = parseFloat(a[1]);
  256. b[1] = parseFloat(b[1]);
  257. }
  258. if (a[1] < b[1]) {
  259. return sortOrder ? -1 : 1;
  260. }
  261. if (a[1] > b[1]) {
  262. return sortOrder ? 1 : -1;
  263. }
  264. return 0;
  265. };
  266. }
  267. /**
  268. * Date sorting algorithm
  269. * @param {Boolean} sortOrder Sorting order (`true` for ascending, `false` for descending).
  270. * @param {Object} columnMeta Column meta object.
  271. * @returns {Function} The compare function.
  272. */
  273. dateSort(sortOrder, columnMeta) {
  274. return function(a, b) {
  275. if (a[1] === b[1]) {
  276. return 0;
  277. }
  278. if (isEmpty(a[1])) {
  279. if (isEmpty(b[1])) {
  280. return 0;
  281. }
  282. if (columnMeta.columnSorting.sortEmptyCells) {
  283. return sortOrder ? -1 : 1;
  284. }
  285. return 1;
  286. }
  287. if (isEmpty(b[1])) {
  288. if (isEmpty(a[1])) {
  289. return 0;
  290. }
  291. if (columnMeta.columnSorting.sortEmptyCells) {
  292. return sortOrder ? 1 : -1;
  293. }
  294. return -1;
  295. }
  296. var aDate = moment(a[1], columnMeta.dateFormat);
  297. var bDate = moment(b[1], columnMeta.dateFormat);
  298. if (!aDate.isValid()) {
  299. return 1;
  300. }
  301. if (!bDate.isValid()) {
  302. return -1;
  303. }
  304. if (bDate.isAfter(aDate)) {
  305. return sortOrder ? -1 : 1;
  306. }
  307. if (bDate.isBefore(aDate)) {
  308. return sortOrder ? 1 : -1;
  309. }
  310. return 0;
  311. };
  312. }
  313. /**
  314. * Numeric sorting algorithm.
  315. *
  316. * @param {Boolean} sortOrder Sorting order (`true` for ascending, `false` for descending).
  317. * @param {Object} columnMeta Column meta object.
  318. * @returns {Function} The compare function.
  319. */
  320. numericSort(sortOrder, columnMeta) {
  321. return function(a, b) {
  322. const parsedA = parseFloat(a[1]);
  323. const parsedB = parseFloat(b[1]);
  324. // Watch out when changing this part of code!
  325. // Check below returns 0 (as expected) when comparing empty string, null, undefined
  326. if (parsedA === parsedB || (isNaN(parsedA) && isNaN(parsedB))) {
  327. return 0;
  328. }
  329. if (columnMeta.columnSorting.sortEmptyCells) {
  330. if (isEmpty(a[1])) {
  331. return sortOrder ? -1 : 1;
  332. }
  333. if (isEmpty(b[1])) {
  334. return sortOrder ? 1 : -1;
  335. }
  336. }
  337. if (isNaN(parsedA)) {
  338. return 1;
  339. }
  340. if (isNaN(parsedB)) {
  341. return -1;
  342. }
  343. if (parsedA < parsedB) {
  344. return sortOrder ? -1 : 1;
  345. } else if (parsedA > parsedB) {
  346. return sortOrder ? 1 : -1;
  347. }
  348. return 0;
  349. };
  350. }
  351. /**
  352. * Perform the sorting.
  353. */
  354. sort() {
  355. if (typeof this.hot.sortOrder == 'undefined') {
  356. this.hot.sortIndex.length = 0;
  357. return;
  358. }
  359. const colMeta = this.hot.getCellMeta(0, this.hot.sortColumn);
  360. const emptyRows = this.hot.countEmptyRows();
  361. let sortFunction;
  362. let nrOfRows;
  363. this.hot.sortingEnabled = false; // this is required by translateRow plugin hook
  364. this.hot.sortIndex.length = 0;
  365. if (typeof colMeta.columnSorting.sortEmptyCells === 'undefined') {
  366. colMeta.columnSorting = {sortEmptyCells: this.sortEmptyCells};
  367. }
  368. if (this.hot.getSettings().maxRows === Number.POSITIVE_INFINITY) {
  369. nrOfRows = this.hot.countRows() - this.hot.getSettings().minSpareRows;
  370. } else {
  371. nrOfRows = this.hot.countRows() - emptyRows;
  372. }
  373. for (let i = 0, ilen = nrOfRows; i < ilen; i++) {
  374. this.hot.sortIndex.push([i, this.hot.getDataAtCell(i, this.hot.sortColumn)]);
  375. }
  376. if (colMeta.sortFunction) {
  377. sortFunction = colMeta.sortFunction;
  378. } else {
  379. switch (colMeta.type) {
  380. case 'date':
  381. sortFunction = this.dateSort;
  382. break;
  383. case 'numeric':
  384. sortFunction = this.numericSort;
  385. break;
  386. default:
  387. sortFunction = this.defaultSort;
  388. }
  389. }
  390. mergeSort(this.hot.sortIndex, sortFunction(this.hot.sortOrder, colMeta));
  391. // Append spareRows
  392. for (let i = this.hot.sortIndex.length; i < this.hot.countRows(); i++) {
  393. this.hot.sortIndex.push([i, this.hot.getDataAtCell(i, this.hot.sortColumn)]);
  394. }
  395. this.hot.sortingEnabled = true; // this is required by translateRow plugin hook
  396. }
  397. /**
  398. * Update indicator states.
  399. */
  400. updateSortIndicator() {
  401. if (typeof this.hot.sortOrder == 'undefined') {
  402. return;
  403. }
  404. const colMeta = this.hot.getCellMeta(0, this.hot.sortColumn);
  405. this.sortIndicators[this.hot.sortColumn] = colMeta.sortIndicator;
  406. }
  407. /**
  408. * `modifyRow` hook callback. Translates physical row index to the sorted row index.
  409. *
  410. * @param {Number} row Row index.
  411. * @returns {Number} Sorted row index.
  412. */
  413. translateRow(row) {
  414. if (this.hot.sortingEnabled && (typeof this.hot.sortOrder !== 'undefined') && this.hot.sortIndex && this.hot.sortIndex.length && this.hot.sortIndex[row]) {
  415. return this.hot.sortIndex[row][0];
  416. }
  417. return row;
  418. }
  419. /**
  420. * Translates sorted row index to physical row index.
  421. *
  422. * @param {Number} row Sorted row index.
  423. * @returns {number} Physical row index.
  424. */
  425. untranslateRow(row) {
  426. if (this.hot.sortingEnabled && this.hot.sortIndex && this.hot.sortIndex.length) {
  427. for (var i = 0; i < this.hot.sortIndex.length; i++) {
  428. if (this.hot.sortIndex[i][0] == row) {
  429. return i;
  430. }
  431. }
  432. }
  433. }
  434. /**
  435. * `afterGetColHeader` callback. Adds column sorting css classes to clickable headers.
  436. *
  437. * @private
  438. * @param {Number} col Column index.
  439. * @param {Element} TH TH HTML element.
  440. */
  441. getColHeader(col, TH) {
  442. if (col < 0 || !TH.parentNode) {
  443. return false;
  444. }
  445. let headerLink = TH.querySelector('.colHeader');
  446. let colspan = TH.getAttribute('colspan');
  447. let TRs = TH.parentNode.parentNode.childNodes;
  448. let headerLevel = Array.prototype.indexOf.call(TRs, TH.parentNode);
  449. headerLevel -= TRs.length;
  450. if (!headerLink) {
  451. return;
  452. }
  453. if (this.hot.getSettings().columnSorting && col >= 0 && headerLevel === -1) {
  454. addClass(headerLink, 'columnSorting');
  455. }
  456. removeClass(headerLink, 'descending');
  457. removeClass(headerLink, 'ascending');
  458. if (this.sortIndicators[col]) {
  459. if (col === this.hot.sortColumn) {
  460. if (this.sortOrderClass === 'ascending') {
  461. addClass(headerLink, 'ascending');
  462. } else if (this.sortOrderClass === 'descending') {
  463. addClass(headerLink, 'descending');
  464. }
  465. }
  466. }
  467. }
  468. /**
  469. * Check if any column is in a sorted state.
  470. *
  471. * @returns {Boolean}
  472. */
  473. isSorted() {
  474. return typeof this.hot.sortColumn != 'undefined';
  475. }
  476. /**
  477. * `afterCreateRow` callback. Updates the sorting state after a row have been created.
  478. *
  479. * @private
  480. * @param {Number} index
  481. * @param {Number} amount
  482. */
  483. afterCreateRow(index, amount) {
  484. if (!this.isSorted()) {
  485. return;
  486. }
  487. for (let i = 0; i < this.hot.sortIndex.length; i++) {
  488. if (this.hot.sortIndex[i][0] >= index) {
  489. this.hot.sortIndex[i][0] += amount;
  490. }
  491. }
  492. for (let i = 0; i < amount; i++) {
  493. this.hot.sortIndex.splice(index + i, 0, [index + i, this.hot.getSourceData()[index + i][this.hot.sortColumn + this.hot.colOffset()]]);
  494. }
  495. this.saveSortingState();
  496. }
  497. /**
  498. * `afterRemoveRow` hook callback.
  499. *
  500. * @private
  501. * @param {Number} index
  502. * @param {Number} amount
  503. */
  504. afterRemoveRow(index, amount) {
  505. if (!this.isSorted()) {
  506. return;
  507. }
  508. let removedRows = this.hot.sortIndex.splice(index, amount);
  509. removedRows = arrayMap(removedRows, (row) => row[0]);
  510. function countRowShift(logicalRow) {
  511. // Todo: compare perf between reduce vs sort->each->brake
  512. return arrayReduce(removedRows, (count, removedLogicalRow) => {
  513. if (logicalRow > removedLogicalRow) {
  514. count++;
  515. }
  516. return count;
  517. }, 0);
  518. }
  519. this.hot.sortIndex = arrayMap(this.hot.sortIndex, (logicalRow, physicalRow) => {
  520. let rowShift = countRowShift(logicalRow[0]);
  521. if (rowShift) {
  522. logicalRow[0] -= rowShift;
  523. }
  524. return logicalRow;
  525. });
  526. this.saveSortingState();
  527. }
  528. /**
  529. * Set options by passed settings
  530. *
  531. * @private
  532. */
  533. setPluginOptions() {
  534. const columnSorting = this.hot.getSettings().columnSorting;
  535. if (typeof columnSorting === 'object') {
  536. this.sortEmptyCells = columnSorting.sortEmptyCells || false;
  537. } else {
  538. this.sortEmptyCells = false;
  539. }
  540. }
  541. /**
  542. * `onAfterOnCellMouseDown` hook callback.
  543. *
  544. * @private
  545. * @param {Event} event Event which are provided by hook.
  546. * @param {CellCoords} coords Coords of the selected cell.
  547. */
  548. onAfterOnCellMouseDown(event, coords) {
  549. if (coords.row > -1) {
  550. return;
  551. }
  552. if (hasClass(event.realTarget, 'columnSorting')) {
  553. // reset order state on every new column header click
  554. if (coords.col !== this.lastSortedColumn) {
  555. this.hot.sortOrder = true;
  556. }
  557. this.lastSortedColumn = coords.col;
  558. this.sortByColumn(coords.col);
  559. }
  560. }
  561. }
  562. registerPlugin('columnSorting', ColumnSorting);
  563. export default ColumnSorting;