2 * UI tools and directives.
4 angular.module('egUiMod', ['egCoreMod', 'ui.bootstrap'])
8 * <input focus-me="iAmOpen"/>
9 * $scope.iAmOpen = true;
13 function($timeout , $parse) {
15 link: function(scope, element, attrs) {
16 var model = $parse(attrs.focusMe);
17 scope.$watch(model, function(value) {
19 $timeout(function() {element[0].focus()});
21 element.bind('blur', function() {
22 scope.$apply(model.assign(scope, false));
29 * <input blur-me="pleaseBlurMe"/>
30 * $scope.pleaseBlurMe = true
31 * Useful for de-focusing when no other obvious focus target exists
35 function($timeout , $parse) {
37 link: function(scope, element, attrs) {
38 var model = $parse(attrs.blurMe);
39 scope.$watch(model, function(value) {
41 $timeout(function() {element[0].blur()});
43 element.bind('focus', function() {
44 scope.$apply(model.assign(scope, false));
51 // <input select-me="iWantToBeSelected"/>
52 // $scope.iWantToBeSelected = true;
53 .directive('selectMe',
55 function($timeout , $parse) {
57 link: function(scope, element, attrs) {
58 var model = $parse(attrs.selectMe);
59 scope.$watch(model, function(value) {
61 $timeout(function() {element[0].select()});
63 element.bind('blur', function() {
64 scope.$apply(model.assign(scope, false));
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();
83 * egProgressModal.open();
85 .factory('egProgressModal',
87 ['$uibModal','$interpolate',
88 function($uibModal, $interpolate){
91 service.open = function() {
92 return $uibModal.open({
93 templateUrl: './share/t_progress_bar',
94 controller: ['$scope', '$uibModalInstance',
95 function($scope, $uibModalInstance) {
96 service.currentInstance = $uibModalInstance;
101 service.close = function() {
102 if (service.currentInstance) {
103 service.currentInstance.close();
104 delete service.currentInstance;
111 * egAlertDialog.open({message : 'hello {{name}}'}).result.then(
112 * function() { console.log('alert closed') });
114 .factory('egAlertDialog',
116 ['$uibModal','$interpolate',
117 function($uibModal , $interpolate) {
120 service.open = function(message, msg_scope) {
121 return $uibModal.open({
122 templateUrl: './share/t_alert_dialog',
123 controller: ['$scope', '$uibModalInstance',
124 function($scope, $uibModalInstance) {
125 $scope.message = $interpolate(message)(msg_scope);
126 $scope.ok = function() {
127 if (msg_scope && msg_scope.ok) msg_scope.ok();
128 $uibModalInstance.close()
139 * egConfirmDialog.open("some message goes {{here}}", {
140 * here : 'foo', ok : function() {}, cancel : function() {}},
143 .factory('egConfirmDialog',
145 ['$uibModal','$interpolate',
146 function($uibModal, $interpolate) {
149 service.open = function(title, message, msg_scope, ok_button_label, cancel_button_label) {
150 return $uibModal.open({
151 templateUrl: './share/t_confirm_dialog',
152 controller: ['$scope', '$uibModalInstance',
153 function($scope, $uibModalInstance) {
154 $scope.title = $interpolate(title)(msg_scope);
155 $scope.message = $interpolate(message)(msg_scope);
156 $scope.ok_button_label = $interpolate(ok_button_label || '')(msg_scope);
157 $scope.cancel_button_label = $interpolate(cancel_button_label || '')(msg_scope);
158 $scope.ok = function() {
159 if (msg_scope.ok) msg_scope.ok();
160 $uibModalInstance.close()
162 $scope.cancel = function() {
163 if (msg_scope.cancel) msg_scope.cancel();
164 $uibModalInstance.dismiss();
175 * egPromptDialog.open(
176 * "prompt message goes {{here}}",
177 * promptValue, // optional
180 * ok : function(value) {console.log(value)},
181 * cancel : function() {console.log('prompt denied')}
185 .factory('egPromptDialog',
187 ['$uibModal','$interpolate',
188 function($uibModal, $interpolate) {
191 service.open = function(message, promptValue, msg_scope) {
192 return $uibModal.open({
193 templateUrl: './share/t_prompt_dialog',
194 controller: ['$scope', '$uibModalInstance',
195 function($scope, $uibModalInstance) {
196 $scope.message = $interpolate(message)(msg_scope);
197 $scope.args = {value : promptValue || ''};
199 $scope.ok = function() {
200 if (msg_scope.ok) msg_scope.ok($scope.args.value);
201 $uibModalInstance.close()
203 $scope.cancel = function() {
204 if (msg_scope.cancel) msg_scope.cancel();
205 $uibModalInstance.dismiss();
216 * egSelectDialog.open(
217 * "message goes {{here}}",
218 * list, // ['values','for','dropdown'],
219 * selectedValue, // optional
222 * ok : function(value) {console.log(value)},
223 * cancel : function() {console.log('prompt denied')}
227 .factory('egSelectDialog',
229 ['$uibModal','$interpolate',
230 function($uibModal, $interpolate) {
233 service.open = function(message, inputList, selectedValue, msg_scope) {
234 return $uibModal.open({
235 templateUrl: './share/t_select_dialog',
236 controller: ['$scope', '$uibModalInstance',
237 function($scope, $uibModalInstance) {
238 $scope.message = $interpolate(message)(msg_scope);
241 value : selectedValue
244 $scope.ok = function() {
245 if (msg_scope.ok) msg_scope.ok($scope.args.value);
246 $uibModalInstance.close()
248 $scope.cancel = function() {
249 if (msg_scope.cancel) msg_scope.cancel();
250 $uibModalInstance.dismiss();
261 * Warn on page unload and give the user a chance to avoid navigating
262 * away from the current page.
263 * Only one handler is supported per page.
264 * NOTE: we can't use an egUnloadDialog as the dialog builder, because
265 * it renders asynchronously, which allows the page to redirect before
266 * the dialog appears.
268 .factory('egUnloadPrompt', [
269 '$window','egStrings',
270 function($window , egStrings) {
271 var service = {attached : false};
273 // attach a page/scope unload prompt
274 service.attach = function($scope, msg) {
275 if (service.attached) return;
276 service.attached = true;
278 // handle page change
279 $($window).on('beforeunload', function() {
281 return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
286 // If a scope was provided, attach a scope-change handler,
287 // similar to the page-page prompt.
288 service.locChangeCancel =
289 $scope.$on('$locationChangeStart', function(evt, next, current) {
290 if (confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) {
291 // user allowed the page to change.
292 // Clear the unload handler.
295 evt.preventDefault();
300 // remove the page unload prompt
301 service.clear = function() {
302 $($window).off('beforeunload');
303 if (service.locChangeCancel)
304 service.locChangeCancel();
305 service.attached = false;
311 .directive('aDisabled', function() {
314 compile: function(tElement, tAttrs, transclude) {
316 tAttrs["ngClick"] = ("ng-click", "!("+tAttrs["aDisabled"]+") && ("+tAttrs["ngClick"]+")");
318 //Toggle "disabled" to class when aDisabled becomes true
319 return function (scope, iElement, iAttrs) {
320 scope.$watch(iAttrs["aDisabled"], function(newValue) {
321 if (newValue !== undefined) {
322 iElement.toggleClass("disabled", newValue);
326 //Disable href on click
327 iElement.on("click", function(e) {
328 if (scope.$eval(iAttrs["aDisabled"])) {
337 .directive('egBasicComboBox', function() {
342 list: "=", // list of strings
348 '<div class="input-group">'+
349 '<input type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()">'+
350 '<div class="input-group-btn" dropdown ng-class="{open:isopen}">'+
351 '<button type="button" ng-click="showAll()" class="btn btn-default dropdown-toggle"><span class="caret"></span></button>'+
352 '<ul class="dropdown-menu dropdown-menu-right">'+
353 '<li ng-repeat="item in list|filter:selected"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
354 '<li ng-if="complete_list" class="divider"><span></span></li>'+
355 '<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
359 controller: ['$scope','$filter',
360 function( $scope , $filter) {
362 $scope.complete_list = false;
363 $scope.isopen = false;
364 $scope.clickedopen = false;
365 $scope.clickedclosed = null;
367 $scope.showAll = function () {
369 $scope.clickedopen = !$scope.clickedopen;
371 if ($scope.clickedclosed === null) {
372 if (!$scope.clickedopen) {
373 $scope.clickedclosed = true;
376 $scope.clickedclosed = !$scope.clickedopen;
379 if ($scope.selected.length > 0) $scope.complete_list = true;
380 if ($scope.selected.length == 0) $scope.complete_list = false;
384 $scope.makeOpen = function () {
385 $scope.isopen = $scope.clickedopen || ($filter('filter')(
388 ).length > 0 && $scope.selected.length > 0);
389 if ($scope.clickedclosed) $scope.isopen = false;
392 $scope.changeValue = function (newVal) {
393 $scope.selected = newVal;
394 $scope.isopen = false;
395 $scope.clickedclosed = null;
396 $scope.clickedopen = false;
397 if ($scope.selected.length == 0) $scope.complete_list = false;
406 * Nested org unit selector modeled as a Bootstrap dropdown button.
408 .directive('egOrgSelector', function() {
412 replace : true, // makes styling easier
414 selected : '=', // defaults to workstation or root org,
415 // unless the nodefault attibute exists
417 // Each org unit is passed into this function and, for
418 // any org units where the response value is true, the
419 // org unit will not be added to the selector.
422 // Each org unit is passed into this function and, for
423 // any org units where the response value is true, the
424 // org unit will not be available for selection.
427 // if set to true, disable the UI element altogether
430 // Caller can either $watch(selected, ..) or register an
434 // optional primary drop-down button label
437 // optional name of settings key for persisting
438 // the last selected org unit
442 // any reason to move this into a TT2 template?
444 '<div class="btn-group eg-org-selector" uib-dropdown>'
445 + '<button type="button" class="btn btn-default" uib-dropdown-toggle ng-disabled="disable_button">'
446 + '<span style="padding-right: 5px;">{{getSelectedName()}}</span>'
447 + '<span class="caret"></span>'
449 + '<ul uib-dropdown-menu class="scrollable-menu">'
450 + '<li ng-repeat="org in orgList" ng-hide="hiddenTest(org.id)">'
451 + '<a href ng-click="orgChanged(org)" a-disabled="disableTest(org.id)" '
452 + 'style="padding-left: {{org.depth * 10 + 5}}px">'
453 + '{{org.shortname}}'
459 controller : ['$scope','$timeout','egOrg','egAuth','egCore','egStartup',
460 function($scope , $timeout , egOrg , egAuth , egCore , egStartup) {
462 if ($scope.alldisabled) {
463 $scope.disable_button = $scope.alldisabled == 'true' ? true : false;
465 $scope.disable_button = false;
468 $scope.egOrg = egOrg; // for use in the link function
469 $scope.egAuth = egAuth; // for use in the link function
470 $scope.hatch = egCore.hatch // for use in the link function
472 // avoid linking the full fleshed tree to the scope by
473 // tossing in a flattened list.
475 // Run-time code referencing post-start data should be run
476 // from within a startup block, otherwise accessing this
477 // module before startup completes will lead to failure.
478 egStartup.go().then(function() {
480 $scope.orgList = egOrg.list().map(function(org) {
483 shortname : org.shortname(),
484 depth : org.ou_type().depth()
488 if (!$scope.selected && !$scope.nodefault)
489 $scope.selected = egOrg.get(egAuth.user().ws_ou());
492 $scope.getSelectedName = function() {
493 if ($scope.selected && $scope.selected.shortname)
494 return $scope.selected.shortname();
498 $scope.orgChanged = function(org) {
499 $scope.selected = egOrg.get(org.id);
500 if ($scope.stickySetting) {
501 egCore.hatch.setLocalItem($scope.stickySetting, org.id);
503 if ($scope.onchange) $scope.onchange($scope.selected);
507 link : function(scope, element, attrs, egGridCtrl) {
509 // boolean fields are presented as value-less attributes
513 if (angular.isDefined(attrs[field]))
516 scope[field] = false;
520 if (scope.stickySetting) {
521 var orgId = scope.hatch.getLocalItem(scope.stickySetting);
523 scope.selected = scope.egOrg.get(orgId);
525 scope.onchange(scope.selected);
529 if (!scope.selected && !scope.nodefault)
530 scope.selected = scope.egOrg.get(scope.egAuth.user().ws_ou());
536 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
537 .directive('egEnter', function () {
538 return function (scope, element, attrs) {
539 element.bind("keydown keypress", function (event) {
540 if(event.which === 13) {
541 scope.$apply(function (){
542 scope.$eval(attrs.egEnter);
545 event.preventDefault();
552 * Handy wrapper directive for uib-datapicker-popup
555 'egDateInput', ['egStrings', 'egCore',
556 function(egStrings, egCore) {
565 hideDatePicker : '=',
569 templateUrl: './share/t_datetime',
571 link : function(scope, elm, attrs) {
572 if (!scope.closeText)
573 scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
575 if ('showTimePicker' in attrs)
576 scope.showTimePicker = true;
578 var default_format = 'mediumDate';
579 egCore.org.settings(['format.date']).then(function(set) {
580 default_format = set['format.date'];
581 scope.date_format = (scope.dateFormat) ?
591 * egFmValueSelector - widget for selecting a value from list specified
594 .directive('egFmValueSelector', function() {
602 // optional filter for refining the set of rows that
603 // get returned. Example:
605 // filter="{'column':{'=':null}}"
608 // optional name of settings key for persisting
609 // the last selected value
612 // optional OU setting for fetching default value;
613 // used only if sticky setting not set
617 templateUrl : './share/t_fm_value_selector',
618 controller : ['$scope','egCore', function($scope , egCore) {
620 $scope.org = egCore.org; // for use in the link function
621 $scope.auth = egCore.auth; // for use in the link function
622 $scope.hatch = egCore.hatch // for use in the link function
624 function flatten_linked_values(cls, list) {
626 var fields = egCore.idl.classes[cls].fields;
629 angular.forEach(fields, function(fld) {
630 if (fld.datatype == 'id') {
632 selector = fld.selector ? fld.selector : id_field;
636 angular.forEach(list, function(item) {
637 var rec = egCore.idl.toHash(item);
647 search[egCore.idl.classes[$scope.idlClass].pkey] = {'!=' : null};
649 angular.extend(search, $scope.filter);
652 $scope.idlClass, search, {}, {atomic : true}
653 ).then(function(list) {
654 $scope.linked_values = flatten_linked_values($scope.idlClass, list);
657 $scope.handleChange = function(value) {
658 if ($scope.stickySetting) {
659 egCore.hatch.setLocalItem($scope.stickySetting, value);
664 link : function(scope, element, attrs) {
665 if (scope.stickySetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
666 var value = scope.hatch.getLocalItem(scope.stickySetting);
667 scope.ngModel = value;
669 if (scope.ouSetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
670 scope.org.settings([scope.ouSetting], scope.auth.user().ws_ou())
671 .then(function(set) {
672 var value = parseInt(set[scope.ouSetting]);
674 scope.ngModel = value;
681 .factory('egWorkLog', ['egCore', function(egCore) {
684 service.retrieve_all = function() {
685 var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
686 var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
688 return { 'work_log' : workLog, 'patron_log' : patronLog };
691 service.record = function(message,data) {
694 if (typeof egCore != 'undefined') {
695 if (typeof egCore.env != 'undefined') {
696 if (typeof egCore.env.aous != 'undefined') {
697 max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
698 max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
700 console.log('worklog: missing egCore.env.aous');
703 console.log('worklog: missing egCore.env');
706 console.log('worklog: missing egCore');
709 if (typeof egCore.org != 'undefined') {
710 if (typeof egCore.org.cachedSettings != 'undefined') {
711 max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
713 console.log('worklog: missing egCore.org.cachedSettings');
716 console.log('worklog: missing egCore.org');
720 if (typeof egCore.org != 'undefined') {
721 if (typeof egCore.org.cachedSettings != 'undefined') {
722 max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
724 console.log('worklog: missing egCore.org.cachedSettings');
727 console.log('worklog: missing egCore.org');
732 console.log('worklog: defaulting to max_entries = ' + max_entries);
736 console.log('worklog: defaulting to max_patrons = ' + max_patrons);
739 var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
740 var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
745 'action' : data.action,
746 'actor' : egCore.auth.user().usrname()
748 if (data.action == 'checkin') {
749 entry['item'] = data.response.params.copy_barcode;
750 entry['item_id'] = data.response.data.acp.id();
751 if (data.response.data.au) {
752 entry['user'] = data.response.data.au.family_name();
753 entry['patron_id'] = data.response.data.au.id();
756 if (data.action == 'checkout') {
757 entry['item'] = data.response.params.copy_barcode;
758 entry['user'] = data.response.data.au.family_name();
759 entry['item_id'] = data.response.data.acp.id();
760 entry['patron_id'] = data.response.data.au.id();
762 if (data.action == 'renew') {
763 entry['item'] = data.response.params.copy_barcode;
764 entry['user'] = data.response.data.au.family_name();
765 entry['item_id'] = data.response.data.acp.id();
766 entry['patron_id'] = data.response.data.au.id();
768 if (data.action == 'requested_hold'
769 || data.action == 'edited_patron'
770 || data.action == 'registered_patron'
771 || data.action == 'paid_bill') {
772 entry['patron_id'] = data.patron_id;
774 if (data.action == 'paid_bill') {
775 entry['amount'] = data.total_amount;
778 workLog.push( entry );
779 if (workLog.length > max_entries) workLog.shift();
780 egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
782 if (entry['patron_id']) {
784 for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
785 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
788 if (temp.length > max_patrons) temp.shift();
790 egCore.hatch.setLocalItem('eg.patron_log',patronLog);
793 console.log('worklog',entry);