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() {
23 scope.$apply(model.assign(scope, false));
31 * <input blur-me="pleaseBlurMe"/>
32 * $scope.pleaseBlurMe = true
33 * Useful for de-focusing when no other obvious focus target exists
37 function($timeout , $parse) {
39 link: function(scope, element, attrs) {
40 var model = $parse(attrs.blurMe);
41 scope.$watch(model, function(value) {
43 $timeout(function() {element[0].blur()});
45 element.bind('focus', function() {
47 scope.$apply(model.assign(scope, false));
55 // <input select-me="iWantToBeSelected"/>
56 // $scope.iWantToBeSelected = true;
57 .directive('selectMe',
59 function($timeout , $parse) {
61 link: function(scope, element, attrs) {
62 var model = $parse(attrs.selectMe);
63 scope.$watch(model, function(value) {
65 $timeout(function() {element[0].select()});
67 element.bind('blur', function() {
69 scope.$apply(model.assign(scope, false));
78 // <div ng-repeat="item in items | reverse">{{item.name}}</div>
79 // http://stackoverflow.com/questions/15266671/angular-ng-repeat-in-reverse
80 // TODO: perhaps this should live elsewhere
81 .filter('reverse', function() {
82 return function(items) {
83 return items.slice().reverse();
90 * egProgressDialog.open();
91 * egProgressDialog.open({value : 0});
92 * egProgressDialog.open({value : 0, max : 123});
93 * egProgressDialog.increment();
94 * egProgressDialog.increment();
95 * egProgressDialog.close();
97 * Each dialog has 2 numbers, 'max' and 'value'.
98 * The content of these values determines how the dialog displays.
100 * There are 3 flavors:
102 * -- value is set, max is set
103 * determinate: shows a progression with a percent complete.
105 * -- value is set, max is unset
106 * semi-determinate, with a value report. Shows a value-less
107 * <progress/>, but shows the value as a number in the dialog.
109 * This is useful in cases where the total number of items to retrieve
110 * from the server is unknown, but we know how many items we've
111 * retrieved thus far. It helps to reinforce that something specific
112 * is happening, but we don't know when it will end.
115 * indeterminate: shows a generic value-less <progress/> with no
116 * clear indication of progress.
118 * Only 1 egProgressDialog instance will be activate at a time.
119 * Each invocation of .open() destroys any existing instance.
122 /* Simple storage class for egProgressDialog data maintenance.
123 * This data lives outside of egProgressDialog so it can be
124 * directly imported into egProgressDialog's $uibModalInstance.
126 .factory('egProgressData', [
128 var service = {}; // max/value initially unset
130 service.reset = function() {
132 delete service.value;
135 service.hasvalue = function() {
136 return Number.isInteger(service.value);
139 service.hasmax = function() {
140 return Number.isInteger(service.max);
143 service.percent = function() {
144 if (service.hasvalue() &&
147 service.value <= service.max)
148 return Math.floor((service.value / service.max) * 100);
156 .factory('egProgressDialog', [
157 'egProgressData','$uibModal',
158 function(egProgressData , $uibModal) {
161 service.open = function(args) {
162 service.close(); // force-kill existing instances.
164 // Reset to an indeterminate progress bar,
165 // overlay with caller values.
166 egProgressData.reset();
167 service.update(angular.extend({}, args));
169 return $uibModal.open({
170 templateUrl: './share/t_progress_dialog',
171 controller: ['$scope','$uibModalInstance','egProgressData',
172 function( $scope , $uibModalInstance , egProgressData) {
173 service.currentInstance = $uibModalInstance;
174 $scope.data = egProgressData; // tiny service
180 service.close = function() {
181 if (service.currentInstance) {
182 service.currentInstance.close();
183 delete service.currentInstance;
187 // Set the current state of the progress bar.
188 service.update = function(args) {
189 if (args.max != undefined)
190 egProgressData.max = args.max;
191 if (args.value != undefined)
192 egProgressData.value = args.value;
195 // Increment the current value. If no amount is specified,
196 // it increments by 1. Calling increment() on an indetermite
197 // progress bar will force it to be a (semi-)determinate bar.
198 service.increment = function(amt) {
199 if (!Number.isInteger(amt)) amt = 1;
201 if (!egProgressData.hasvalue())
202 egProgressData.value = 0;
204 egProgressData.value += amt;
211 * egAlertDialog.open({message : 'hello {{name}}'}).result.then(
212 * function() { console.log('alert closed') });
214 .factory('egAlertDialog',
216 ['$uibModal','$interpolate',
217 function($uibModal , $interpolate) {
220 service.open = function(message, msg_scope) {
221 return $uibModal.open({
222 templateUrl: './share/t_alert_dialog',
223 controller: ['$scope', '$uibModalInstance',
224 function($scope, $uibModalInstance) {
225 $scope.message = $interpolate(message)(msg_scope);
226 $scope.ok = function() {
227 if (msg_scope && msg_scope.ok) msg_scope.ok();
228 $uibModalInstance.close()
239 * egConfirmDialog.open("some message goes {{here}}", {
240 * here : 'foo', ok : function() {}, cancel : function() {}},
243 .factory('egConfirmDialog',
245 ['$uibModal','$interpolate',
246 function($uibModal, $interpolate) {
249 service.open = function(title, message, msg_scope, ok_button_label, cancel_button_label) {
250 return $uibModal.open({
251 templateUrl: './share/t_confirm_dialog',
252 controller: ['$scope', '$uibModalInstance',
253 function($scope, $uibModalInstance) {
254 $scope.title = $interpolate(title)(msg_scope);
255 $scope.message = $interpolate(message)(msg_scope);
256 $scope.ok_button_label = $interpolate(ok_button_label || '')(msg_scope);
257 $scope.cancel_button_label = $interpolate(cancel_button_label || '')(msg_scope);
258 $scope.ok = function() {
259 if (msg_scope.ok) msg_scope.ok();
260 $uibModalInstance.close()
262 $scope.cancel = function() {
263 if (msg_scope.cancel) msg_scope.cancel();
264 $uibModalInstance.dismiss();
275 * egPromptDialog.open(
276 * "prompt message goes {{here}}",
277 * promptValue, // optional
280 * ok : function(value) {console.log(value)},
281 * cancel : function() {console.log('prompt denied')}
285 .factory('egPromptDialog',
287 ['$uibModal','$interpolate',
288 function($uibModal, $interpolate) {
291 service.open = function(message, promptValue, msg_scope) {
292 return $uibModal.open({
293 templateUrl: './share/t_prompt_dialog',
294 controller: ['$scope', '$uibModalInstance',
295 function($scope, $uibModalInstance) {
296 $scope.message = $interpolate(message)(msg_scope);
297 $scope.args = {value : promptValue || ''};
299 $scope.ok = function() {
300 if (msg_scope.ok) msg_scope.ok($scope.args.value);
301 $uibModalInstance.close()
303 $scope.cancel = function() {
304 if (msg_scope.cancel) msg_scope.cancel();
305 $uibModalInstance.dismiss();
316 * egSelectDialog.open(
317 * "message goes {{here}}",
318 * list, // ['values','for','dropdown'],
319 * selectedValue, // optional
322 * ok : function(value) {console.log(value)},
323 * cancel : function() {console.log('prompt denied')}
327 .factory('egSelectDialog',
329 ['$uibModal','$interpolate',
330 function($uibModal, $interpolate) {
333 service.open = function(message, inputList, selectedValue, msg_scope) {
334 return $uibModal.open({
335 templateUrl: './share/t_select_dialog',
336 controller: ['$scope', '$uibModalInstance',
337 function($scope, $uibModalInstance) {
338 $scope.message = $interpolate(message)(msg_scope);
341 value : selectedValue
344 $scope.ok = function() {
345 if (msg_scope.ok) msg_scope.ok($scope.args.value);
346 $uibModalInstance.close()
348 $scope.cancel = function() {
349 if (msg_scope.cancel) msg_scope.cancel();
350 $uibModalInstance.dismiss();
361 * Warn on page unload and give the user a chance to avoid navigating
362 * away from the current page.
363 * Only one handler is supported per page.
364 * NOTE: we can't use an egUnloadDialog as the dialog builder, because
365 * it renders asynchronously, which allows the page to redirect before
366 * the dialog appears.
368 .factory('egUnloadPrompt', [
369 '$window','egStrings',
370 function($window , egStrings) {
371 var service = {attached : false};
373 // attach a page/scope unload prompt
374 service.attach = function($scope, msg) {
375 if (service.attached) return;
376 service.attached = true;
378 // handle page change
379 $($window).on('beforeunload', function() {
381 return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
386 // If a scope was provided, attach a scope-change handler,
387 // similar to the page-page prompt.
388 service.locChangeCancel =
389 $scope.$on('$locationChangeStart', function(evt, next, current) {
390 if (confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) {
391 // user allowed the page to change.
392 // Clear the unload handler.
395 evt.preventDefault();
400 // remove the page unload prompt
401 service.clear = function() {
402 $($window).off('beforeunload');
403 if (service.locChangeCancel)
404 service.locChangeCancel();
405 service.attached = false;
411 .directive('aDisabled', function() {
414 compile: function(tElement, tAttrs, transclude) {
416 tAttrs["ngClick"] = ("ng-click", "!("+tAttrs["aDisabled"]+") && ("+tAttrs["ngClick"]+")");
418 //Toggle "disabled" to class when aDisabled becomes true
419 return function (scope, iElement, iAttrs) {
420 scope.$watch(iAttrs["aDisabled"], function(newValue) {
421 if (newValue !== undefined) {
422 iElement.toggleClass("disabled", newValue);
426 //Disable href on click
427 iElement.on("click", function(e) {
428 if (scope.$eval(iAttrs["aDisabled"])) {
437 .directive('egBasicComboBox', function() {
442 list: "=", // list of strings
448 '<div class="input-group">'+
449 '<input type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()">'+
450 '<div class="input-group-btn" dropdown ng-class="{open:isopen}">'+
451 '<button type="button" ng-click="showAll()" class="btn btn-default dropdown-toggle"><span class="caret"></span></button>'+
452 '<ul class="dropdown-menu dropdown-menu-right">'+
453 '<li ng-repeat="item in list|filter:selected"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
454 '<li ng-if="complete_list" class="divider"><span></span></li>'+
455 '<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
459 controller: ['$scope','$filter',
460 function( $scope , $filter) {
462 $scope.complete_list = false;
463 $scope.isopen = false;
464 $scope.clickedopen = false;
465 $scope.clickedclosed = null;
467 $scope.showAll = function () {
469 $scope.clickedopen = !$scope.clickedopen;
471 if ($scope.clickedclosed === null) {
472 if (!$scope.clickedopen) {
473 $scope.clickedclosed = true;
476 $scope.clickedclosed = !$scope.clickedopen;
479 if ($scope.selected.length > 0) $scope.complete_list = true;
480 if ($scope.selected.length == 0) $scope.complete_list = false;
484 $scope.makeOpen = function () {
485 $scope.isopen = $scope.clickedopen || ($filter('filter')(
488 ).length > 0 && $scope.selected.length > 0);
489 if ($scope.clickedclosed) $scope.isopen = false;
492 $scope.changeValue = function (newVal) {
493 $scope.selected = newVal;
494 $scope.isopen = false;
495 $scope.clickedclosed = null;
496 $scope.clickedopen = false;
497 if ($scope.selected.length == 0) $scope.complete_list = false;
506 * Nested org unit selector modeled as a Bootstrap dropdown button.
508 .directive('egOrgSelector', function() {
512 replace : true, // makes styling easier
514 selected : '=', // defaults to workstation or root org,
515 // unless the nodefault attibute exists
517 // Each org unit is passed into this function and, for
518 // any org units where the response value is true, the
519 // org unit will not be added to the selector.
522 // Each org unit is passed into this function and, for
523 // any org units where the response value is true, the
524 // org unit will not be available for selection.
527 // if set to true, disable the UI element altogether
530 // Caller can either $watch(selected, ..) or register an
534 // optional primary drop-down button label
537 // optional name of settings key for persisting
538 // the last selected org unit
542 // any reason to move this into a TT2 template?
544 '<div class="btn-group eg-org-selector" uib-dropdown>'
545 + '<button type="button" class="btn btn-default" uib-dropdown-toggle ng-disabled="disable_button">'
546 + '<span style="padding-right: 5px;">{{getSelectedName()}}</span>'
547 + '<span class="caret"></span>'
549 + '<ul uib-dropdown-menu class="scrollable-menu">'
550 + '<li ng-repeat="org in orgList" ng-hide="hiddenTest(org.id)">'
551 + '<a href ng-click="orgChanged(org)" a-disabled="disableTest(org.id)" '
552 + 'style="padding-left: {{org.depth * 10 + 5}}px">'
553 + '{{org.shortname}}'
559 controller : ['$scope','$timeout','egCore','egStartup',
560 function($scope , $timeout , egCore , egStartup) {
562 if ($scope.alldisabled) {
563 $scope.disable_button = $scope.alldisabled == 'true' ? true : false;
565 $scope.disable_button = false;
568 // avoid linking the full fleshed tree to the scope by
569 // tossing in a flattened list.
571 // Run-time code referencing post-start data should be run
572 // from within a startup block, otherwise accessing this
573 // module before startup completes will lead to failure.
575 // controller() runs before link().
576 // This post-startup code runs after link().
577 egStartup.go().then(function() {
579 $scope.orgList = egCore.org.list().map(function(org) {
582 shortname : org.shortname(),
583 depth : org.ou_type().depth()
587 // Apply default values
589 if ($scope.stickySetting) {
590 var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
592 $scope.selected = egCore.org.get(orgId);
596 if (!$scope.selected && !$scope.nodefault) {
598 egCore.org.get(egCore.auth.user().ws_ou());
601 fire_orgsel_onchange(); // no-op if nothing is selected
605 * Fire onchange handler after a timeout, so the
606 * $scope.selected value has a chance to propagate to
607 * the page controllers before the onchange fires. This
608 * way, the caller does not have to manually capture the
609 * $scope.selected value during onchange.
611 function fire_orgsel_onchange() {
612 if (!$scope.selected || !$scope.onchange) return;
613 $timeout(function() {
615 'egOrgSelector onchange('+$scope.selected.id()+')');
616 $scope.onchange($scope.selected)
620 $scope.getSelectedName = function() {
621 if ($scope.selected && $scope.selected.shortname)
622 return $scope.selected.shortname();
626 $scope.orgChanged = function(org) {
627 $scope.selected = egCore.org.get(org.id);
628 if ($scope.stickySetting) {
629 egCore.hatch.setLocalItem($scope.stickySetting, org.id);
631 fire_orgsel_onchange();
635 link : function(scope, element, attrs, egGridCtrl) {
637 // boolean fields are presented as value-less attributes
641 if (angular.isDefined(attrs[field]))
644 scope[field] = false;
651 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
652 .directive('egEnter', function () {
653 return function (scope, element, attrs) {
654 element.bind("keydown keypress", function (event) {
655 if(event.which === 13) {
656 scope.$apply(function (){
657 scope.$eval(attrs.egEnter);
660 event.preventDefault();
667 * Handy wrapper directive for uib-datapicker-popup
670 'egDateInput', ['egStrings', 'egCore',
671 function(egStrings, egCore) {
680 hideDatePicker : '=',
684 templateUrl: './share/t_datetime',
686 link : function(scope, elm, attrs) {
687 if (!scope.closeText)
688 scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
690 if ('showTimePicker' in attrs)
691 scope.showTimePicker = true;
693 var default_format = 'mediumDate';
694 egCore.org.settings(['format.date']).then(function(set) {
695 default_format = set['format.date'];
696 scope.date_format = (scope.dateFormat) ?
706 * egFmValueSelector - widget for selecting a value from list specified
709 .directive('egFmValueSelector', function() {
717 // optional filter for refining the set of rows that
718 // get returned. Example:
720 // filter="{'column':{'=':null}}"
723 // optional name of settings key for persisting
724 // the last selected value
727 // optional OU setting for fetching default value;
728 // used only if sticky setting not set
732 templateUrl : './share/t_fm_value_selector',
733 controller : ['$scope','egCore', function($scope , egCore) {
735 $scope.org = egCore.org; // for use in the link function
736 $scope.auth = egCore.auth; // for use in the link function
737 $scope.hatch = egCore.hatch // for use in the link function
739 function flatten_linked_values(cls, list) {
741 var fields = egCore.idl.classes[cls].fields;
744 angular.forEach(fields, function(fld) {
745 if (fld.datatype == 'id') {
747 selector = fld.selector ? fld.selector : id_field;
751 angular.forEach(list, function(item) {
752 var rec = egCore.idl.toHash(item);
762 search[egCore.idl.classes[$scope.idlClass].pkey] = {'!=' : null};
764 angular.extend(search, $scope.filter);
767 $scope.idlClass, search, {}, {atomic : true}
768 ).then(function(list) {
769 $scope.linked_values = flatten_linked_values($scope.idlClass, list);
772 $scope.handleChange = function(value) {
773 if ($scope.stickySetting) {
774 egCore.hatch.setLocalItem($scope.stickySetting, value);
779 link : function(scope, element, attrs) {
780 if (scope.stickySetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
781 var value = scope.hatch.getLocalItem(scope.stickySetting);
782 scope.ngModel = value;
784 if (scope.ouSetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
785 scope.org.settings([scope.ouSetting], scope.auth.user().ws_ou())
786 .then(function(set) {
787 var value = parseInt(set[scope.ouSetting]);
789 scope.ngModel = value;
796 .factory('egWorkLog', ['egCore', function(egCore) {
799 service.retrieve_all = function() {
800 var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
801 var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
803 return { 'work_log' : workLog, 'patron_log' : patronLog };
806 service.record = function(message,data) {
809 if (typeof egCore != 'undefined') {
810 if (typeof egCore.env != 'undefined') {
811 if (typeof egCore.env.aous != 'undefined') {
812 max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
813 max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
815 console.log('worklog: missing egCore.env.aous');
818 console.log('worklog: missing egCore.env');
821 console.log('worklog: missing egCore');
824 if (typeof egCore.org != 'undefined') {
825 if (typeof egCore.org.cachedSettings != 'undefined') {
826 max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
828 console.log('worklog: missing egCore.org.cachedSettings');
831 console.log('worklog: missing egCore.org');
835 if (typeof egCore.org != 'undefined') {
836 if (typeof egCore.org.cachedSettings != 'undefined') {
837 max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
839 console.log('worklog: missing egCore.org.cachedSettings');
842 console.log('worklog: missing egCore.org');
847 console.log('worklog: defaulting to max_entries = ' + max_entries);
851 console.log('worklog: defaulting to max_patrons = ' + max_patrons);
854 var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
855 var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
860 'action' : data.action,
861 'actor' : egCore.auth.user().usrname()
863 if (data.action == 'checkin') {
864 entry['item'] = data.response.params.copy_barcode;
865 entry['item_id'] = data.response.data.acp.id();
866 if (data.response.data.au) {
867 entry['user'] = data.response.data.au.family_name();
868 entry['patron_id'] = data.response.data.au.id();
871 if (data.action == 'checkout') {
872 entry['item'] = data.response.params.copy_barcode;
873 entry['user'] = data.response.data.au.family_name();
874 entry['item_id'] = data.response.data.acp.id();
875 entry['patron_id'] = data.response.data.au.id();
877 if (data.action == 'renew') {
878 entry['item'] = data.response.params.copy_barcode;
879 entry['user'] = data.response.data.au.family_name();
880 entry['item_id'] = data.response.data.acp.id();
881 entry['patron_id'] = data.response.data.au.id();
883 if (data.action == 'requested_hold'
884 || data.action == 'edited_patron'
885 || data.action == 'registered_patron'
886 || data.action == 'paid_bill') {
887 entry['patron_id'] = data.patron_id;
889 if (data.action == 'paid_bill') {
890 entry['amount'] = data.total_amount;
893 workLog.push( entry );
894 if (workLog.length > max_entries) workLog.shift();
895 egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
897 if (entry['patron_id']) {
899 for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
900 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
903 if (temp.length > max_patrons) temp.shift();
905 egCore.hatch.setLocalItem('eg.patron_log',patronLog);
908 console.log('worklog',entry);