smart-table.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. /**
  2. * @version 2.1.4
  3. * @license MIT
  4. */
  5. (function (ng, undefined){
  6. 'use strict';
  7. ng.module('smart-table', []).run(['$templateCache', function ($templateCache) {
  8. $templateCache.put('template/smart-table/pagination.html',
  9. '<nav ng-if="numPages && pages.length >= 2"><ul class="pagination">' +
  10. '<li ng-repeat="page in pages" ng-class="{active: page==currentPage}"><a ng-click="selectPage(page)">{{page}}</a></li>' +
  11. '</ul></nav>');
  12. }]);
  13. ng.module('smart-table')
  14. .constant('stConfig', {
  15. pagination: {
  16. template: 'template/smart-table/pagination.html',
  17. itemsByPage: 10,
  18. displayedPages: 5
  19. },
  20. search: {
  21. delay: 400, // ms
  22. inputEvent: 'input'
  23. },
  24. select: {
  25. mode: 'single',
  26. selectedClass: 'st-selected'
  27. },
  28. sort: {
  29. ascentClass: 'st-sort-ascent',
  30. descentClass: 'st-sort-descent',
  31. skipNatural: false,
  32. delay:300
  33. },
  34. pipe: {
  35. delay: 100 //ms
  36. }
  37. });
  38. ng.module('smart-table')
  39. .controller('stTableController', ['$scope', '$parse', '$filter', '$attrs', function StTableController ($scope, $parse, $filter, $attrs) {
  40. var propertyName = $attrs.stTable;
  41. var displayGetter = $parse(propertyName);
  42. var displaySetter = displayGetter.assign;
  43. var safeGetter;
  44. var orderBy = $filter('orderBy');
  45. var filter = $filter('filter');
  46. var safeCopy = copyRefs(displayGetter($scope));
  47. var tableState = {
  48. sort: {},
  49. search: {},
  50. pagination: {
  51. start: 0,
  52. totalItemCount: 0
  53. }
  54. };
  55. var filtered;
  56. var pipeAfterSafeCopy = true;
  57. var ctrl = this;
  58. var lastSelected;
  59. function copyRefs (src) {
  60. return src ? [].concat(src) : [];
  61. }
  62. function updateSafeCopy () {
  63. safeCopy = copyRefs(safeGetter($scope));
  64. if (pipeAfterSafeCopy === true) {
  65. ctrl.pipe();
  66. }
  67. }
  68. function deepDelete (object, path) {
  69. if (path.indexOf('.') != -1) {
  70. var partials = path.split('.');
  71. var key = partials.pop();
  72. var parentPath = partials.join('.');
  73. var parentObject = $parse(parentPath)(object)
  74. delete parentObject[key];
  75. if (Object.keys(parentObject).length == 0) {
  76. deepDelete(object, parentPath);
  77. }
  78. } else {
  79. delete object[path];
  80. }
  81. }
  82. if ($attrs.stSafeSrc) {
  83. safeGetter = $parse($attrs.stSafeSrc);
  84. $scope.$watch(function () {
  85. var safeSrc = safeGetter($scope);
  86. return safeSrc ? safeSrc.length : 0;
  87. }, function (newValue, oldValue) {
  88. if (newValue !== safeCopy.length) {
  89. updateSafeCopy();
  90. }
  91. });
  92. $scope.$watch(function () {
  93. return safeGetter($scope);
  94. }, function (newValue, oldValue) {
  95. if (newValue !== oldValue) {
  96. tableState.pagination.start = 0;
  97. updateSafeCopy();
  98. }
  99. });
  100. }
  101. /**
  102. * sort the rows
  103. * @param {Function | String} predicate - function or string which will be used as predicate for the sorting
  104. * @param [reverse] - if you want to reverse the order
  105. */
  106. this.sortBy = function sortBy (predicate, reverse) {
  107. tableState.sort.predicate = predicate;
  108. tableState.sort.reverse = reverse === true;
  109. if (ng.isFunction(predicate)) {
  110. tableState.sort.functionName = predicate.name;
  111. } else {
  112. delete tableState.sort.functionName;
  113. }
  114. tableState.pagination.start = 0;
  115. return this.pipe();
  116. };
  117. /**
  118. * search matching rows
  119. * @param {String} input - the input string
  120. * @param {String} [predicate] - the property name against you want to check the match, otherwise it will search on all properties
  121. */
  122. this.search = function search (input, predicate) {
  123. var predicateObject = tableState.search.predicateObject || {};
  124. var prop = predicate ? predicate : '$';
  125. input = ng.isString(input) ? input.trim() : input;
  126. $parse(prop).assign(predicateObject, input);
  127. // to avoid to filter out null value
  128. if (!input) {
  129. deepDelete(predicateObject, prop);
  130. }
  131. tableState.search.predicateObject = predicateObject;
  132. tableState.pagination.start = 0;
  133. return this.pipe();
  134. };
  135. /**
  136. * this will chain the operations of sorting and filtering based on the current table state (sort options, filtering, ect)
  137. */
  138. this.pipe = function pipe () {
  139. var pagination = tableState.pagination;
  140. var output;
  141. filtered = tableState.search.predicateObject ? filter(safeCopy, tableState.search.predicateObject) : safeCopy;
  142. if (tableState.sort.predicate) {
  143. filtered = orderBy(filtered, tableState.sort.predicate, tableState.sort.reverse);
  144. }
  145. pagination.totalItemCount = filtered.length;
  146. if (pagination.number !== undefined) {
  147. pagination.numberOfPages = filtered.length > 0 ? Math.ceil(filtered.length / pagination.number) : 1;
  148. pagination.start = pagination.start >= filtered.length ? (pagination.numberOfPages - 1) * pagination.number : pagination.start;
  149. output = filtered.slice(pagination.start, pagination.start + parseInt(pagination.number));
  150. }
  151. displaySetter($scope, output || filtered);
  152. };
  153. /**
  154. * select a dataRow (it will add the attribute isSelected to the row object)
  155. * @param {Object} row - the row to select
  156. * @param {String} [mode] - "single" or "multiple" (multiple by default)
  157. */
  158. this.select = function select (row, mode) {
  159. var rows = copyRefs(displayGetter($scope));
  160. var index = rows.indexOf(row);
  161. if (index !== -1) {
  162. if (mode === 'single') {
  163. row.isSelected = row.isSelected !== true;
  164. if (lastSelected) {
  165. lastSelected.isSelected = false;
  166. }
  167. lastSelected = row.isSelected === true ? row : undefined;
  168. } else {
  169. rows[index].isSelected = !rows[index].isSelected;
  170. }
  171. }
  172. };
  173. /**
  174. * take a slice of the current sorted/filtered collection (pagination)
  175. *
  176. * @param {Number} start - start index of the slice
  177. * @param {Number} number - the number of item in the slice
  178. */
  179. this.slice = function splice (start, number) {
  180. tableState.pagination.start = start;
  181. tableState.pagination.number = number;
  182. return this.pipe();
  183. };
  184. /**
  185. * return the current state of the table
  186. * @returns {{sort: {}, search: {}, pagination: {start: number}}}
  187. */
  188. this.tableState = function getTableState () {
  189. return tableState;
  190. };
  191. this.getFilteredCollection = function getFilteredCollection () {
  192. return filtered || safeCopy;
  193. };
  194. /**
  195. * Use a different filter function than the angular FilterFilter
  196. * @param filterName the name under which the custom filter is registered
  197. */
  198. this.setFilterFunction = function setFilterFunction (filterName) {
  199. filter = $filter(filterName);
  200. };
  201. /**
  202. * Use a different function than the angular orderBy
  203. * @param sortFunctionName the name under which the custom order function is registered
  204. */
  205. this.setSortFunction = function setSortFunction (sortFunctionName) {
  206. orderBy = $filter(sortFunctionName);
  207. };
  208. /**
  209. * Usually when the safe copy is updated the pipe function is called.
  210. * Calling this method will prevent it, which is something required when using a custom pipe function
  211. */
  212. this.preventPipeOnWatch = function preventPipe () {
  213. pipeAfterSafeCopy = false;
  214. };
  215. }])
  216. .directive('stTable', function () {
  217. return {
  218. restrict: 'A',
  219. controller: 'stTableController',
  220. link: function (scope, element, attr, ctrl) {
  221. if (attr.stSetFilter) {
  222. ctrl.setFilterFunction(attr.stSetFilter);
  223. }
  224. if (attr.stSetSort) {
  225. ctrl.setSortFunction(attr.stSetSort);
  226. }
  227. }
  228. };
  229. });
  230. ng.module('smart-table')
  231. .directive('stSearch', ['stConfig', '$timeout','$parse', function (stConfig, $timeout, $parse) {
  232. return {
  233. require: '^stTable',
  234. link: function (scope, element, attr, ctrl) {
  235. var tableCtrl = ctrl;
  236. var promise = null;
  237. var throttle = attr.stDelay || stConfig.search.delay;
  238. var event = attr.stInputEvent || stConfig.search.inputEvent;
  239. attr.$observe('stSearch', function (newValue, oldValue) {
  240. var input = element[0].value;
  241. if (newValue !== oldValue && input) {
  242. ctrl.tableState().search = {};
  243. tableCtrl.search(input, newValue);
  244. }
  245. });
  246. //table state -> view
  247. scope.$watch(function () {
  248. return ctrl.tableState().search;
  249. }, function (newValue, oldValue) {
  250. var predicateExpression = attr.stSearch || '$';
  251. if (newValue.predicateObject && $parse(predicateExpression)(newValue.predicateObject) !== element[0].value) {
  252. element[0].value = $parse(predicateExpression)(newValue.predicateObject) || '';
  253. }
  254. }, true);
  255. // view -> table state
  256. element.bind(event, function (evt) {
  257. evt = evt.originalEvent || evt;
  258. if (promise !== null) {
  259. $timeout.cancel(promise);
  260. }
  261. promise = $timeout(function () {
  262. tableCtrl.search(evt.target.value, attr.stSearch || '');
  263. promise = null;
  264. }, throttle);
  265. });
  266. }
  267. };
  268. }]);
  269. ng.module('smart-table')
  270. .directive('stSelectRow', ['stConfig', function (stConfig) {
  271. return {
  272. restrict: 'A',
  273. require: '^stTable',
  274. scope: {
  275. row: '=stSelectRow'
  276. },
  277. link: function (scope, element, attr, ctrl) {
  278. var mode = attr.stSelectMode || stConfig.select.mode;
  279. element.bind('click', function () {
  280. scope.$apply(function () {
  281. ctrl.select(scope.row, mode);
  282. });
  283. });
  284. scope.$watch('row.isSelected', function (newValue) {
  285. if (newValue === true) {
  286. element.addClass(stConfig.select.selectedClass);
  287. } else {
  288. element.removeClass(stConfig.select.selectedClass);
  289. }
  290. });
  291. }
  292. };
  293. }]);
  294. ng.module('smart-table')
  295. .directive('stSort', ['stConfig', '$parse', '$timeout', function (stConfig, $parse, $timeout) {
  296. return {
  297. restrict: 'A',
  298. require: '^stTable',
  299. link: function (scope, element, attr, ctrl) {
  300. var predicate = attr.stSort;
  301. var getter = $parse(predicate);
  302. var index = 0;
  303. var classAscent = attr.stClassAscent || stConfig.sort.ascentClass;
  304. var classDescent = attr.stClassDescent || stConfig.sort.descentClass;
  305. var stateClasses = [classAscent, classDescent];
  306. var sortDefault;
  307. var skipNatural = attr.stSkipNatural !== undefined ? attr.stSkipNatural : stConfig.sort.skipNatural;
  308. var promise = null;
  309. var throttle = attr.stDelay || stConfig.sort.delay;
  310. if (attr.stSortDefault) {
  311. sortDefault = scope.$eval(attr.stSortDefault) !== undefined ? scope.$eval(attr.stSortDefault) : attr.stSortDefault;
  312. }
  313. //view --> table state
  314. function sort () {
  315. index++;
  316. var func;
  317. predicate = ng.isFunction(getter(scope)) ? getter(scope) : attr.stSort;
  318. if (index % 3 === 0 && !!skipNatural !== true) {
  319. //manual reset
  320. index = 0;
  321. ctrl.tableState().sort = {};
  322. ctrl.tableState().pagination.start = 0;
  323. func = ctrl.pipe.bind(ctrl);
  324. } else {
  325. func = ctrl.sortBy.bind(ctrl, predicate, index % 2 === 0);
  326. }
  327. if (promise !== null) {
  328. $timeout.cancel(promise);
  329. }
  330. promise = $timeout(func, throttle);
  331. }
  332. element.bind('click', function sortClick () {
  333. if (predicate) {
  334. sort();
  335. }
  336. });
  337. if (sortDefault) {
  338. index = sortDefault === 'reverse' ? 1 : 0;
  339. sort();
  340. }
  341. //table state --> view
  342. scope.$watch(function () {
  343. return ctrl.tableState().sort;
  344. }, function (newValue) {
  345. if (newValue.predicate !== predicate) {
  346. index = 0;
  347. element
  348. .removeClass(classAscent)
  349. .removeClass(classDescent);
  350. } else {
  351. index = newValue.reverse === true ? 2 : 1;
  352. element
  353. .removeClass(stateClasses[index % 2])
  354. .addClass(stateClasses[index - 1]);
  355. }
  356. }, true);
  357. }
  358. };
  359. }]);
  360. ng.module('smart-table')
  361. .directive('stPagination', ['stConfig', function (stConfig) {
  362. return {
  363. restrict: 'EA',
  364. require: '^stTable',
  365. scope: {
  366. stItemsByPage: '=?',
  367. stDisplayedPages: '=?',
  368. stPageChange: '&'
  369. },
  370. templateUrl: function (element, attrs) {
  371. if (attrs.stTemplate) {
  372. return attrs.stTemplate;
  373. }
  374. return stConfig.pagination.template;
  375. },
  376. link: function (scope, element, attrs, ctrl) {
  377. scope.stItemsByPage = scope.stItemsByPage ? +(scope.stItemsByPage) : stConfig.pagination.itemsByPage;
  378. scope.stDisplayedPages = scope.stDisplayedPages ? +(scope.stDisplayedPages) : stConfig.pagination.displayedPages;
  379. scope.currentPage = 1;
  380. scope.pages = [];
  381. function redraw () {
  382. var paginationState = ctrl.tableState().pagination;
  383. var start = 1;
  384. var end;
  385. var i;
  386. var prevPage = scope.currentPage;
  387. scope.totalItemCount = paginationState.totalItemCount;
  388. scope.currentPage = Math.floor(paginationState.start / paginationState.number) + 1;
  389. start = Math.max(start, scope.currentPage - Math.abs(Math.floor(scope.stDisplayedPages / 2)));
  390. end = start + scope.stDisplayedPages;
  391. if (end > paginationState.numberOfPages) {
  392. end = paginationState.numberOfPages + 1;
  393. start = Math.max(1, end - scope.stDisplayedPages);
  394. }
  395. scope.pages = [];
  396. scope.numPages = paginationState.numberOfPages;
  397. for (i = start; i < end; i++) {
  398. scope.pages.push(i);
  399. }
  400. if (prevPage !== scope.currentPage) {
  401. scope.stPageChange({newPage: scope.currentPage});
  402. }
  403. }
  404. //table state --> view
  405. scope.$watch(function () {
  406. return ctrl.tableState().pagination;
  407. }, redraw, true);
  408. //scope --> table state (--> view)
  409. scope.$watch('stItemsByPage', function (newValue, oldValue) {
  410. if (newValue !== oldValue) {
  411. scope.selectPage(1);
  412. }
  413. });
  414. scope.$watch('stDisplayedPages', redraw);
  415. //view -> table state
  416. scope.selectPage = function (page) {
  417. if (page > 0 && page <= scope.numPages) {
  418. ctrl.slice((page - 1) * scope.stItemsByPage, scope.stItemsByPage);
  419. }
  420. };
  421. if (!ctrl.tableState().pagination.number) {
  422. ctrl.slice(0, scope.stItemsByPage);
  423. }
  424. }
  425. };
  426. }]);
  427. ng.module('smart-table')
  428. .directive('stPipe', ['stConfig', '$timeout', function (config, $timeout) {
  429. return {
  430. require: 'stTable',
  431. scope: {
  432. stPipe: '='
  433. },
  434. link: {
  435. pre: function (scope, element, attrs, ctrl) {
  436. var pipePromise = null;
  437. if (ng.isFunction(scope.stPipe)) {
  438. ctrl.preventPipeOnWatch();
  439. ctrl.pipe = function () {
  440. if (pipePromise !== null) {
  441. $timeout.cancel(pipePromise)
  442. }
  443. pipePromise = $timeout(function () {
  444. scope.stPipe(ctrl.tableState(), ctrl);
  445. }, config.pipe.delay);
  446. return pipePromise;
  447. }
  448. }
  449. },
  450. post: function (scope, element, attrs, ctrl) {
  451. ctrl.pipe();
  452. }
  453. }
  454. };
  455. }]);
  456. })(angular);