]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/ui.js
LP#1452950 Remove unsaved data warning after click-thru
[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         ['$modal','$interpolate',
89 function($modal , $interpolate) {
90     var service = {};
91
92     service.open = function(message, msg_scope) {
93         return $modal.open({
94             templateUrl: './share/t_alert_dialog',
95             controller: ['$scope', '$modalInstance',
96                 function($scope, $modalInstance) {
97                     $scope.message = $interpolate(message)(msg_scope);
98                     $scope.ok = function() {
99                         if (msg_scope && msg_scope.ok) msg_scope.ok();
100                         $modalInstance.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        ['$modal','$interpolate',
118 function($modal, $interpolate) {
119     var service = {};
120
121     service.open = function(title, message, msg_scope, ok_button_label, cancel_button_label) {
122         return $modal.open({
123             templateUrl: './share/t_confirm_dialog',
124             controller: ['$scope', '$modalInstance',
125                 function($scope, $modalInstance) {
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                         $modalInstance.close()
133                     }
134                     $scope.cancel = function() {
135                         if (msg_scope.cancel) msg_scope.cancel();
136                         $modalInstance.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        ['$modal','$interpolate',
160 function($modal, $interpolate) {
161     var service = {};
162
163     service.open = function(message, promptValue, msg_scope) {
164         return $modal.open({
165             templateUrl: './share/t_prompt_dialog',
166             controller: ['$scope', '$modalInstance',
167                 function($scope, $modalInstance) {
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                         $modalInstance.close()
174                     }
175                     $scope.cancel = function() {
176                         if (msg_scope.cancel) msg_scope.cancel();
177                         $modalInstance.dismiss();
178                     }
179                 }
180             ]
181         })
182     }
183
184     return service;
185 }])
186
187 /**
188  * Warn on page unload and give the user a chance to avoid navigating
189  * away from the current page.  
190  * Only one handler is supported per page.
191  * NOTE: we can't use an egUnloadDialog as the dialog builder, because
192  * it renders asynchronously, which allows the page to redirect before
193  * the dialog appears.
194  */
195 .factory('egUnloadPrompt', [
196         '$window','egStrings', 
197 function($window , egStrings) {
198     var service = {};
199
200     // attach a page/scope unload prompt
201     service.attach = function($scope, msg) {
202
203         // handle page change
204         $($window).on('beforeunload', function() { 
205             service.clear();
206             return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
207         });
208
209         if (!$scope) return;
210
211         // If a scope was provided, attach a scope-change handler,
212         // similar to the page-page prompt.
213         service.locChangeCancel = 
214             $scope.$on('$locationChangeStart', function(evt, next, current) {
215             if (confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) {
216                 // user allowed the page to change.  
217                 // Clear the unload handler.
218                 service.clear();
219             } else {
220                 evt.preventDefault();
221             }
222         });
223     };
224
225     // remove the page unload prompt
226     service.clear = function() {
227         $($window).off('beforeunload');
228         if (service.locChangeCancel)
229             service.locChangeCancel();
230     }
231
232     return service;
233 }])
234
235 .directive('aDisabled', function() {
236     return {
237         restrict : 'A',
238         compile: function(tElement, tAttrs, transclude) {
239             //Disable ngClick
240             tAttrs["ngClick"] = ("ng-click", "!("+tAttrs["aDisabled"]+") && ("+tAttrs["ngClick"]+")");
241
242             //Toggle "disabled" to class when aDisabled becomes true
243             return function (scope, iElement, iAttrs) {
244                 scope.$watch(iAttrs["aDisabled"], function(newValue) {
245                     if (newValue !== undefined) {
246                         iElement.toggleClass("disabled", newValue);
247                     }
248                 });
249
250                 //Disable href on click
251                 iElement.on("click", function(e) {
252                     if (scope.$eval(iAttrs["aDisabled"])) {
253                         e.preventDefault();
254                     }
255                 });
256             };
257         }
258     };
259 })
260
261 .directive('egBasicComboBox', function() {
262     return {
263         restrict: 'E',
264         replace: true,
265         scope: {
266             list: "=", // list of strings
267             selected: "=",
268             egDisabled: "="
269         },
270         template:
271             '<div class="input-group">'+
272                 '<input type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()">'+
273                 '<div class="input-group-btn" dropdown ng-class="{open:isopen}">'+
274                     '<button type="button" ng-click="showAll()" class="btn btn-default dropdown-toggle"><span class="caret"></span></button>'+
275                     '<ul class="dropdown-menu dropdown-menu-right">'+
276                         '<li ng-repeat="item in list|filter:selected"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
277                         '<li ng-if="all" class="divider"><span></span></li>'+
278                         '<li ng-if="all" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
279                     '</ul>'+
280                 '</div>'+
281             '</div>',
282         controller: ['$scope','$filter',
283             function( $scope , $filter) {
284
285                 $scope.all = false;
286                 $scope.isopen = false;
287
288                 $scope.showAll = function () {
289                     if ($scope.selected.length > 0)
290                         $scope.all = true;
291                 }
292
293                 $scope.makeOpen = function () {
294                     $scope.isopen = $filter('filter')(
295                         $scope.list,
296                         $scope.selected
297                     ).length > 0 && $scope.selected.length > 0;
298                     $scope.all = false;
299                 }
300
301                 $scope.changeValue = function (newVal) {
302                     $scope.selected = newVal;
303                     $scope.isopen = false;
304                 }
305
306             }
307         ]
308     };
309 })
310
311 /**
312  * Nested org unit selector modeled as a Bootstrap dropdown button.
313  */
314 .directive('egOrgSelector', function() {
315     return {
316         restrict : 'AE',
317         transclude : true,
318         replace : true, // makes styling easier
319         scope : {
320             selected : '=', // defaults to workstation or root org,
321                             // unless the nodefault attibute exists
322
323             // Each org unit is passed into this function and, for
324             // any org units where the response value is true, the
325             // org unit will not be added to the selector.
326             hiddenTest : '=',
327
328             // Each org unit is passed into this function and, for
329             // any org units where the response value is true, the
330             // org unit will not be available for selection.
331             disableTest : '=',
332
333             // if set to true, disable the UI element altogether
334             alldisabled : '@',
335
336             // Caller can either $watch(selected, ..) or register an
337             // onchange handler.
338             onchange : '=',
339
340             // optional primary drop-down button label
341             label : '@',
342
343             // optional name of settings key for persisting
344             // the last selected org unit
345             stickySetting : '@'
346         },
347
348         // any reason to move this into a TT2 template?
349         template : 
350             '<div class="btn-group eg-org-selector" dropdown>'
351             + '<button type="button" class="btn btn-default dropdown-toggle" ng-disabled="disable_button">'
352              + '<span style="padding-right: 5px;">{{getSelectedName()}}</span>'
353              + '<span class="caret"></span>'
354            + '</button>'
355            + '<ul class="dropdown-menu scrollable-menu">'
356              + '<li ng-repeat="org in orgList" ng-hide="hiddenTest(org.id)">'
357                + '<a href ng-click="orgChanged(org)" a-disabled="disableTest(org.id)" '
358                  + 'style="padding-left: {{org.depth * 10 + 5}}px">'
359                  + '{{org.shortname}}'
360                + '</a>'
361              + '</li>'
362            + '</ul>'
363           + '</div>',
364
365         controller : ['$scope','$timeout','egOrg','egAuth','egCore','egStartup',
366               function($scope , $timeout , egOrg , egAuth , egCore , egStartup) {
367
368             if ($scope.alldisabled) {
369                 $scope.disable_button = $scope.alldisabled == 'true' ? true : false;
370             } else {
371                 $scope.disable_button = false;
372             }
373
374             $scope.egOrg = egOrg; // for use in the link function
375             $scope.egAuth = egAuth; // for use in the link function
376             $scope.hatch = egCore.hatch // for use in the link function
377
378             // avoid linking the full fleshed tree to the scope by 
379             // tossing in a flattened list.
380             // --
381             // Run-time code referencing post-start data should be run
382             // from within a startup block, otherwise accessing this
383             // module before startup completes will lead to failure.
384             egStartup.go().then(function() {
385
386                 $scope.orgList = egOrg.list().map(function(org) {
387                     return {
388                         id : org.id(),
389                         shortname : org.shortname(), 
390                         depth : org.ou_type().depth()
391                     }
392                 });
393
394                 if (!$scope.selected)
395                     $scope.selected = egOrg.get(egAuth.user().ws_ou());
396             });
397
398             $scope.getSelectedName = function() {
399                 if ($scope.selected && $scope.selected.shortname)
400                     return $scope.selected.shortname();
401                 return $scope.label;
402             }
403
404             $scope.orgChanged = function(org) {
405                 $scope.selected = egOrg.get(org.id);
406                 if ($scope.stickySetting) {
407                     egCore.hatch.setLocalItem($scope.stickySetting, org.id);
408                 }
409                 if ($scope.onchange) $scope.onchange($scope.selected);
410             }
411
412         }],
413         link : function(scope, element, attrs, egGridCtrl) {
414
415             // boolean fields are presented as value-less attributes
416             angular.forEach(
417                 ['nodefault'],
418                 function(field) {
419                     if (angular.isDefined(attrs[field]))
420                         scope[field] = true;
421                     else
422                         scope[field] = false;
423                 }
424             );
425
426             if (scope.stickySetting) {
427                 var orgId = scope.hatch.getLocalItem(scope.stickySetting);
428                 if (orgId) {
429                     scope.selected = scope.egOrg.get(orgId);
430                 }
431             }
432
433             if (!scope.selected && !scope.nodefault)
434                 scope.selected = scope.egOrg.get(scope.egAuth.user().ws_ou());
435         }
436
437     }
438 })
439
440 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
441 .directive('egEnter', function () {
442     return function (scope, element, attrs) {
443         element.bind("keydown keypress", function (event) {
444             if(event.which === 13) {
445                 scope.$apply(function (){
446                     scope.$eval(attrs.egEnter);
447                 });
448  
449                 event.preventDefault();
450             }
451         });
452     };
453 })
454
455 /*
456 http://stackoverflow.com/questions/18061757/angular-js-and-html5-date-input-value-how-to-get-firefox-to-show-a-readable-d
457
458 This directive allows us to use html5 input type="date" (for Chrome) and 
459 gracefully fall back to a regular ISO text input for Firefox.
460 It also allows us to abstract away some browser finickiness.
461 */
462 .directive(
463     'egDateInput',
464     function(dateFilter) {
465         return {
466             require: 'ngModel',
467             template: '<input type="date"></input>',
468             replace: true,
469             link: function(scope, elm, attrs, ngModelCtrl) {
470
471                 // since this is a date-only selector, set the time
472                 // portion to 00:00:00, which should better match the
473                 // user's expectations.  Note this allows us to retain
474                 // the timezone.
475                 function strip_time(date) {
476                     if (!date) date = new Date();
477                     date.setHours(0);
478                     date.setMinutes(0);
479                     date.setSeconds(0);
480                     date.setMilliseconds(0);
481                     return date;
482                 }
483
484                 ngModelCtrl.$formatters.unshift(function (modelValue) {
485                     // apply strip_time here in case the user never 
486                     // modifies the date value.
487                     return dateFilter(strip_time(modelValue), 'yyyy-MM-dd');
488                 });
489                 
490                 ngModelCtrl.$parsers.unshift(function(viewValue) {
491                     return strip_time(new Date(viewValue));
492                 });
493             },
494         };
495 })