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