]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/ui.js
webstaff: fix "nodefault" attribute for egOrgSelector
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / services / ui.js
1 /**
2   * UI tools and directives.
3   */
4 angular.module('egUiMod', ['egCoreMod', 'ui.bootstrap'])
5
6
7 /**
8  * <input focus-me="iAmOpen"/>
9  * $scope.iAmOpen = true;
10  */
11 .directive('focusMe', 
12        ['$timeout','$parse', 
13 function($timeout , $parse) {
14     return {
15         link: function(scope, element, attrs) {
16             var model = $parse(attrs.focusMe);
17             scope.$watch(model, function(value) {
18                 if(value === true) 
19                     $timeout(function() {element[0].focus()});
20             });
21             element.bind('blur', function() {
22                 scope.$apply(model.assign(scope, false));
23             })
24         }
25     };
26 }])
27
28 /**
29  * <input blur-me="pleaseBlurMe"/>
30  * $scope.pleaseBlurMe = true
31  * Useful for de-focusing when no other obvious focus target exists
32  */
33 .directive('blurMe', 
34        ['$timeout','$parse', 
35 function($timeout , $parse) {
36     return {
37         link: function(scope, element, attrs) {
38             var model = $parse(attrs.blurMe);
39             scope.$watch(model, function(value) {
40                 if(value === true) 
41                     $timeout(function() {element[0].blur()});
42             });
43             element.bind('focus', function() {
44                 scope.$apply(model.assign(scope, false));
45             })
46         }
47     };
48 }])
49
50
51 // <input select-me="iWantToBeSelected"/>
52 // $scope.iWantToBeSelected = true;
53 .directive('selectMe', 
54        ['$timeout','$parse', 
55 function($timeout , $parse) {
56     return {
57         link: function(scope, element, attrs) {
58             var model = $parse(attrs.selectMe);
59             scope.$watch(model, function(value) {
60                 if(value === true) 
61                     $timeout(function() {element[0].select()});
62             });
63             element.bind('blur', function() {
64                 scope.$apply(model.assign(scope, false));
65             })
66         }
67     };
68 }])
69
70
71 // 'reverse' filter 
72 // <div ng-repeat="item in items | reverse">{{item.name}}</div>
73 // http://stackoverflow.com/questions/15266671/angular-ng-repeat-in-reverse
74 // TODO: perhaps this should live elsewhere
75 .filter('reverse', function() {
76     return function(items) {
77         return items.slice().reverse();
78     };
79 })
80
81
82 /**
83  * egAlertDialog.open({message : 'hello {{name}}'}).result.then(
84  *     function() { console.log('alert closed') });
85  */
86 .factory('egAlertDialog', 
87
88         ['$uibModal','$interpolate',
89 function($uibModal , $interpolate) {
90     var service = {};
91
92     service.open = function(message, msg_scope) {
93         return $uibModal.open({
94             templateUrl: './share/t_alert_dialog',
95             controller: ['$scope', '$uibModalInstance',
96                 function($scope, $uibModalInstance) {
97                     $scope.message = $interpolate(message)(msg_scope);
98                     $scope.ok = function() {
99                         if (msg_scope && msg_scope.ok) msg_scope.ok();
100                         $uibModalInstance.close()
101                     }
102                 }
103             ]
104         });
105     }
106
107     return service;
108 }])
109
110 /**
111  * egConfirmDialog.open("some message goes {{here}}", {
112  *  here : 'foo', ok : function() {}, cancel : function() {}},
113  *  'OK', 'Cancel');
114  */
115 .factory('egConfirmDialog', 
116     
117        ['$uibModal','$interpolate',
118 function($uibModal, $interpolate) {
119     var service = {};
120
121     service.open = function(title, message, msg_scope, ok_button_label, cancel_button_label) {
122         return $uibModal.open({
123             templateUrl: './share/t_confirm_dialog',
124             controller: ['$scope', '$uibModalInstance',
125                 function($scope, $uibModalInstance) {
126                     $scope.title = $interpolate(title)(msg_scope);
127                     $scope.message = $interpolate(message)(msg_scope);
128                     $scope.ok_button_label = $interpolate(ok_button_label || '')(msg_scope);
129                     $scope.cancel_button_label = $interpolate(cancel_button_label || '')(msg_scope);
130                     $scope.ok = function() {
131                         if (msg_scope.ok) msg_scope.ok();
132                         $uibModalInstance.close()
133                     }
134                     $scope.cancel = function() {
135                         if (msg_scope.cancel) msg_scope.cancel();
136                         $uibModalInstance.dismiss();
137                     }
138                 }
139             ]
140         })
141     }
142
143     return service;
144 }])
145
146 /**
147  * egPromptDialog.open(
148  *    "prompt message goes {{here}}", 
149  *    promptValue,  // optional
150  *    {
151  *      here : 'foo',  
152  *      ok : function(value) {console.log(value)}, 
153  *      cancel : function() {console.log('prompt denied')}
154  *    }
155  *  );
156  */
157 .factory('egPromptDialog', 
158     
159        ['$uibModal','$interpolate',
160 function($uibModal, $interpolate) {
161     var service = {};
162
163     service.open = function(message, promptValue, msg_scope) {
164         return $uibModal.open({
165             templateUrl: './share/t_prompt_dialog',
166             controller: ['$scope', '$uibModalInstance',
167                 function($scope, $uibModalInstance) {
168                     $scope.message = $interpolate(message)(msg_scope);
169                     $scope.args = {value : promptValue || ''};
170                     $scope.focus = true;
171                     $scope.ok = function() {
172                         if (msg_scope.ok) msg_scope.ok($scope.args.value);
173                         $uibModalInstance.close()
174                     }
175                     $scope.cancel = function() {
176                         if (msg_scope.cancel) msg_scope.cancel();
177                         $uibModalInstance.dismiss();
178                     }
179                 }
180             ]
181         })
182     }
183
184     return service;
185 }])
186
187 /**
188  * egSelectDialog.open(
189  *    "message goes {{here}}", 
190  *    list,           // ['values','for','dropdown'],
191  *    selectedValue,  // optional
192  *    {
193  *      here : 'foo',
194  *      ok : function(value) {console.log(value)}, 
195  *      cancel : function() {console.log('prompt denied')}
196  *    }
197  *  );
198  */
199 .factory('egSelectDialog', 
200     
201        ['$uibModal','$interpolate',
202 function($uibModal, $interpolate) {
203     var service = {};
204
205     service.open = function(message, inputList, selectedValue, msg_scope) {
206         return $uibModal.open({
207             templateUrl: './share/t_select_dialog',
208             controller: ['$scope', '$uibModalInstance',
209                 function($scope, $uibModalInstance) {
210                     $scope.message = $interpolate(message)(msg_scope);
211                     $scope.args = {
212                         list  : inputList,
213                         value : selectedValue
214                     };
215                     $scope.focus = true;
216                     $scope.ok = function() {
217                         if (msg_scope.ok) msg_scope.ok($scope.args.value);
218                         $uibModalInstance.close()
219                     }
220                     $scope.cancel = function() {
221                         if (msg_scope.cancel) msg_scope.cancel();
222                         $uibModalInstance.dismiss();
223                     }
224                 }
225             ]
226         })
227     }
228
229     return service;
230 }])
231
232 /**
233  * Warn on page unload and give the user a chance to avoid navigating
234  * away from the current page.  
235  * Only one handler is supported per page.
236  * NOTE: we can't use an egUnloadDialog as the dialog builder, because
237  * it renders asynchronously, which allows the page to redirect before
238  * the dialog appears.
239  */
240 .factory('egUnloadPrompt', [
241         '$window','egStrings', 
242 function($window , egStrings) {
243     var service = {attached : false};
244
245     // attach a page/scope unload prompt
246     service.attach = function($scope, msg) {
247         if (service.attached) return;
248         service.attached = true;
249
250         // handle page change
251         $($window).on('beforeunload', function() { 
252             service.clear();
253             return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
254         });
255
256         if (!$scope) return;
257
258         // If a scope was provided, attach a scope-change handler,
259         // similar to the page-page prompt.
260         service.locChangeCancel = 
261             $scope.$on('$locationChangeStart', function(evt, next, current) {
262             if (confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) {
263                 // user allowed the page to change.  
264                 // Clear the unload handler.
265                 service.clear();
266             } else {
267                 evt.preventDefault();
268             }
269         });
270     };
271
272     // remove the page unload prompt
273     service.clear = function() {
274         $($window).off('beforeunload');
275         if (service.locChangeCancel)
276             service.locChangeCancel();
277         service.attached = false;
278     }
279
280     return service;
281 }])
282
283 .directive('aDisabled', function() {
284     return {
285         restrict : 'A',
286         compile: function(tElement, tAttrs, transclude) {
287             //Disable ngClick
288             tAttrs["ngClick"] = ("ng-click", "!("+tAttrs["aDisabled"]+") && ("+tAttrs["ngClick"]+")");
289
290             //Toggle "disabled" to class when aDisabled becomes true
291             return function (scope, iElement, iAttrs) {
292                 scope.$watch(iAttrs["aDisabled"], function(newValue) {
293                     if (newValue !== undefined) {
294                         iElement.toggleClass("disabled", newValue);
295                     }
296                 });
297
298                 //Disable href on click
299                 iElement.on("click", function(e) {
300                     if (scope.$eval(iAttrs["aDisabled"])) {
301                         e.preventDefault();
302                     }
303                 });
304             };
305         }
306     };
307 })
308
309 .directive('egBasicComboBox', function() {
310     return {
311         restrict: 'E',
312         replace: true,
313         scope: {
314             list: "=", // list of strings
315             selected: "=",
316             egDisabled: "=",
317             allowAll: "@",
318         },
319         template:
320             '<div class="input-group">'+
321                 '<input type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()">'+
322                 '<div class="input-group-btn" dropdown ng-class="{open:isopen}">'+
323                     '<button type="button" ng-click="showAll()" class="btn btn-default dropdown-toggle"><span class="caret"></span></button>'+
324                     '<ul class="dropdown-menu dropdown-menu-right">'+
325                         '<li ng-repeat="item in list|filter:selected"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
326                         '<li ng-if="complete_list" class="divider"><span></span></li>'+
327                         '<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
328                     '</ul>'+
329                 '</div>'+
330             '</div>',
331         controller: ['$scope','$filter',
332             function( $scope , $filter) {
333
334                 $scope.complete_list = false;
335                 $scope.isopen = false;
336                 $scope.clickedopen = false;
337                 $scope.clickedclosed = null;
338
339                 $scope.showAll = function () {
340
341                     $scope.clickedopen = !$scope.clickedopen;
342
343                     if ($scope.clickedclosed === null) {
344                         if (!$scope.clickedopen) {
345                             $scope.clickedclosed = true;
346                         }
347                     } else {
348                         $scope.clickedclosed = !$scope.clickedopen;
349                     }
350
351                     if ($scope.selected.length > 0) $scope.complete_list = true;
352                     if ($scope.selected.length == 0) $scope.complete_list = false;
353                     $scope.makeOpen();
354                 }
355
356                 $scope.makeOpen = function () {
357                     $scope.isopen = $scope.clickedopen || ($filter('filter')(
358                         $scope.list,
359                         $scope.selected
360                     ).length > 0 && $scope.selected.length > 0);
361                     if ($scope.clickedclosed) $scope.isopen = false;
362                 }
363
364                 $scope.changeValue = function (newVal) {
365                     $scope.selected = newVal;
366                     $scope.isopen = false;
367                     $scope.clickedclosed = null;
368                     $scope.clickedopen = false;
369                     if ($scope.selected.length == 0) $scope.complete_list = false;
370                 }
371
372             }
373         ]
374     };
375 })
376
377 /**
378  * Nested org unit selector modeled as a Bootstrap dropdown button.
379  */
380 .directive('egOrgSelector', function() {
381     return {
382         restrict : 'AE',
383         transclude : true,
384         replace : true, // makes styling easier
385         scope : {
386             selected : '=', // defaults to workstation or root org,
387                             // unless the nodefault attibute exists
388
389             // Each org unit is passed into this function and, for
390             // any org units where the response value is true, the
391             // org unit will not be added to the selector.
392             hiddenTest : '=',
393
394             // Each org unit is passed into this function and, for
395             // any org units where the response value is true, the
396             // org unit will not be available for selection.
397             disableTest : '=',
398
399             // if set to true, disable the UI element altogether
400             alldisabled : '@',
401
402             // Caller can either $watch(selected, ..) or register an
403             // onchange handler.
404             onchange : '=',
405
406             // optional primary drop-down button label
407             label : '@',
408
409             // optional name of settings key for persisting
410             // the last selected org unit
411             stickySetting : '@'
412         },
413
414         // any reason to move this into a TT2 template?
415         template : 
416             '<div class="btn-group eg-org-selector" uib-dropdown>'
417             + '<button type="button" class="btn btn-default" uib-dropdown-toggle ng-disabled="disable_button">'
418              + '<span style="padding-right: 5px;">{{getSelectedName()}}</span>'
419              + '<span class="caret"></span>'
420            + '</button>'
421            + '<ul uib-dropdown-menu class="scrollable-menu">'
422              + '<li ng-repeat="org in orgList" ng-hide="hiddenTest(org.id)">'
423                + '<a href ng-click="orgChanged(org)" a-disabled="disableTest(org.id)" '
424                  + 'style="padding-left: {{org.depth * 10 + 5}}px">'
425                  + '{{org.shortname}}'
426                + '</a>'
427              + '</li>'
428            + '</ul>'
429           + '</div>',
430
431         controller : ['$scope','$timeout','egOrg','egAuth','egCore','egStartup',
432               function($scope , $timeout , egOrg , egAuth , egCore , egStartup) {
433
434             if ($scope.alldisabled) {
435                 $scope.disable_button = $scope.alldisabled == 'true' ? true : false;
436             } else {
437                 $scope.disable_button = false;
438             }
439
440             $scope.egOrg = egOrg; // for use in the link function
441             $scope.egAuth = egAuth; // for use in the link function
442             $scope.hatch = egCore.hatch // for use in the link function
443
444             // avoid linking the full fleshed tree to the scope by 
445             // tossing in a flattened list.
446             // --
447             // Run-time code referencing post-start data should be run
448             // from within a startup block, otherwise accessing this
449             // module before startup completes will lead to failure.
450             egStartup.go().then(function() {
451
452                 $scope.orgList = egOrg.list().map(function(org) {
453                     return {
454                         id : org.id(),
455                         shortname : org.shortname(), 
456                         depth : org.ou_type().depth()
457                     }
458                 });
459
460                 if (!$scope.selected && !$scope.nodefault)
461                     $scope.selected = egOrg.get(egAuth.user().ws_ou());
462             });
463
464             $scope.getSelectedName = function() {
465                 if ($scope.selected && $scope.selected.shortname)
466                     return $scope.selected.shortname();
467                 return $scope.label;
468             }
469
470             $scope.orgChanged = function(org) {
471                 $scope.selected = egOrg.get(org.id);
472                 if ($scope.stickySetting) {
473                     egCore.hatch.setLocalItem($scope.stickySetting, org.id);
474                 }
475                 if ($scope.onchange) $scope.onchange($scope.selected);
476             }
477
478         }],
479         link : function(scope, element, attrs, egGridCtrl) {
480
481             // boolean fields are presented as value-less attributes
482             angular.forEach(
483                 ['nodefault'],
484                 function(field) {
485                     if (angular.isDefined(attrs[field]))
486                         scope[field] = true;
487                     else
488                         scope[field] = false;
489                 }
490             );
491
492             if (scope.stickySetting) {
493                 var orgId = scope.hatch.getLocalItem(scope.stickySetting);
494                 if (orgId) {
495                     scope.selected = scope.egOrg.get(orgId);
496                     if (scope.onchange)
497                         scope.onchange(scope.selected);
498                 }
499             }
500
501             if (!scope.selected && !scope.nodefault)
502                 scope.selected = scope.egOrg.get(scope.egAuth.user().ws_ou());
503         }
504
505     }
506 })
507
508 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
509 .directive('egEnter', function () {
510     return function (scope, element, attrs) {
511         element.bind("keydown keypress", function (event) {
512             if(event.which === 13) {
513                 scope.$apply(function (){
514                     scope.$eval(attrs.egEnter);
515                 });
516  
517                 event.preventDefault();
518             }
519         });
520     };
521 })
522
523 /*
524 * Handy wrapper directive for uib-datapicker-popup
525 */
526 .directive(
527     'egDateInput', ['egStrings', 'egCore',
528     function(egStrings, egCore) {
529         return {
530             scope : {
531                 closeText : '@',
532                 ngModel : '=',
533                 ngChange : '=',
534                 ngBlur : '=',
535                 ngDisabled : '=',
536                 ngRequired : '=',
537                 hideDatePicker : '=',
538                 dateFormat : '=?'
539             },
540             require: 'ngModel',
541             templateUrl: './share/t_datetime',
542             replace: true,
543             link : function(scope, elm, attrs) {
544                 if (!scope.closeText)
545                     scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
546
547                 if ('showTimePicker' in attrs)
548                     scope.showTimePicker = true;
549
550                 var default_format = 'mediumDate';
551                 egCore.org.settings(['format.date']).then(function(set) {
552                     default_format = set['format.date'];
553                     scope.date_format = (scope.dateFormat) ?
554                         scope.dateFormat :
555                         default_format;
556                 });
557             }
558         };
559     }
560 ])
561
562 .factory('egWorkLog', ['egCore', function(egCore) {
563     var service = {};
564
565     service.retrieve_all = function() {
566         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
567         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
568
569         return { 'work_log' : workLog, 'patron_log' : patronLog };
570     }
571
572     service.record = function(message,data) {
573         var max_entries;
574         var max_patrons;
575         if (typeof egCore != 'undefined') {
576             if (typeof egCore.env != 'undefined') {
577                 if (typeof egCore.env.aous != 'undefined') {
578                     max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
579                     max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
580                 } else {
581                     console.log('worklog: missing egCore.env.aous');
582                 }
583             } else {
584                 console.log('worklog: missing egCore.env');
585             }
586         } else {
587             console.log('worklog: missing egCore');
588         }
589         if (!max_entries) {
590             if (typeof egCore.org != 'undefined') {
591                 if (typeof egCore.org.cachedSettings != 'undefined') {
592                     max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
593                 } else {
594                     console.log('worklog: missing egCore.org.cachedSettings');
595                 }
596             } else {
597                 console.log('worklog: missing egCore.org');
598             }
599         }
600         if (!max_patrons) {
601             if (typeof egCore.org != 'undefined') {
602                 if (typeof egCore.org.cachedSettings != 'undefined') {
603                     max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
604                 } else {
605                     console.log('worklog: missing egCore.org.cachedSettings');
606                 }
607             } else {
608                 console.log('worklog: missing egCore.org');
609             }
610         }
611         if (!max_entries) {
612             max_entries = 20;
613             console.log('worklog: defaulting to max_entries = ' + max_entries);
614         }
615         if (!max_patrons) {
616             max_patrons = 10;
617             console.log('worklog: defaulting to max_patrons = ' + max_patrons);
618         }
619
620         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
621         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
622         var entry = {
623             'when' : new Date(),
624             'msg' : message,
625             'data' : data,
626             'action' : data.action,
627             'actor' : egCore.auth.user().usrname()
628         };
629         if (data.action == 'checkin') {
630             entry['item'] = data.response.params.copy_barcode;
631             entry['item_id'] = data.response.data.acp.id();
632             if (data.response.data.au) {
633                 entry['user'] = data.response.data.au.family_name();
634                 entry['patron_id'] = data.response.data.au.id();
635             }
636         }
637         if (data.action == 'checkout') {
638             entry['item'] = data.response.params.copy_barcode;
639             entry['user'] = data.response.data.au.family_name();
640             entry['item_id'] = data.response.data.acp.id();
641             entry['patron_id'] = data.response.data.au.id();
642         }
643         if (data.action == 'renew') {
644             entry['item'] = data.response.params.copy_barcode;
645             entry['user'] = data.response.data.au.family_name();
646             entry['item_id'] = data.response.data.acp.id();
647             entry['patron_id'] = data.response.data.au.id();
648         }
649         if (data.action == 'requested_hold'
650             || data.action == 'edited_patron'
651             || data.action == 'registered_patron'
652             || data.action == 'paid_bill') {
653             entry['patron_id'] = data.patron_id;
654         }
655         if (data.action == 'paid_bill') {
656             entry['amount'] = data.total_amount;
657         }
658
659         workLog.push( entry );
660         if (workLog.length > max_entries) workLog.shift();
661         egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
662
663         if (entry['patron_id']) {
664             var temp = [];
665             for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
666                 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
667             }
668             temp.push( entry );
669             if (temp.length > max_patrons) temp.shift();
670             patronLog = temp;
671             egCore.hatch.setLocalItem('eg.patron_log',patronLog);
672         }
673
674         console.log('worklog',entry);
675     }
676
677     return service;
678 }]);