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));
76 // <select int-to-str ><option value="1">Value</option></select>
77 // use integer models for string values
78 .directive('intToStr', function() {
82 link: function(scope, element, attrs, ngModel) {
83 ngModel.$parsers.push(function(value) {
84 return parseInt(value);
86 ngModel.$formatters.push(function(value) {
93 // <input str-to-int value="10"/>
94 .directive('strToInt', function() {
98 link: function(scope, element, attrs, ngModel) {
99 ngModel.$parsers.push(function(value) {
102 ngModel.$formatters.push(function(value) {
103 return parseInt(value);
109 // <input float-to-str
110 .directive('floatToStr', function() {
114 link: function(scope, element, attrs, ngModel) {
115 ngModel.$parsers.push(function(value) {
116 return parseFloat(value);
118 ngModel.$formatters.push(function(value) {
125 .directive('strToFloat', function() {
129 link: function(scope, element, attrs, ngModel) {
130 ngModel.$parsers.push(function(value) {
133 ngModel.$formatters.push(function(value) {
134 return parseFloat(value);
141 // <div ng-repeat="item in items | reverse">{{item.name}}</div>
142 // http://stackoverflow.com/questions/15266671/angular-ng-repeat-in-reverse
143 // TODO: perhaps this should live elsewhere
144 .filter('reverse', function() {
145 return function(items) {
146 return items.slice().reverse();
151 // Overriding the core angular date filter with a moment-js based one for
152 // better timezone and formatting support.
153 .filter('date',function() {
170 var formatReplace = [
184 return function (date, format, tz) {
185 if (!date) return '';
188 date = new Date().toISOString();
191 var fmt = formatMap[format] || format;
192 angular.forEach(formatReplace, function (r) {
193 fmt = fmt.replace(r[0],r[1]);
197 var d = moment(date);
198 if (tz && tz !== '-') d.tz(tz);
200 return d.isValid() ? d.format(fmt) : '';
205 // 'egOrgDate' filter
206 // Uses moment.js and moment-timezone.js to put dates into the most appropriate
207 // timezone for a given (optional) org unit based on its lib.timezone setting
208 .filter('egOrgDate',['$filter','egCore',
209 function($filter , egCore) {
213 function eg_date_filter (date, fmt, ouID) {
215 if (angular.isObject(ouID)) {
216 if (angular.isFunction(ouID.id)) {
223 if (!tzcache[ouID]) {
225 egCore.org.settings('lib.timezone', ouID)
227 tzcache[ouID] = s['lib.timezone'] || OpenSRF.tz;
232 return $filter('date')(date, fmt, tzcache[ouID]);
235 eg_date_filter.$stateful = true;
237 return eg_date_filter;
240 // 'egOrgDateInContext' filter
241 // Uses the egOrgDate filter to make time and date location aware, and further
242 // modifies the format if one of [short, medium, long, full] to show only the
243 // date if the optional interval parameter is day-granular. This is
244 // particularly useful for due dates on circulations.
245 .filter('egOrgDateInContext',['$filter','egCore',
246 function($filter , egCore) {
248 function eg_context_date_filter (date, format, orgID, interval) {
250 if (!fmt) fmt = 'shortDate';
252 // if this is a simple, one-word format, and it doesn't say "Date" in it...
253 if (['short','medium','long','full'].filter(function(x){return fmt == x}).length > 0 && interval) {
254 var secs = egCore.date.intervalToSeconds(interval);
255 if (secs !== null && secs % 86400 == 0) fmt += 'Date';
258 return $filter('egOrgDate')(date, fmt, orgID);
261 eg_context_date_filter.$stateful = true;
263 return eg_context_date_filter;
266 // 'egDueDate' filter
267 // Uses the egOrgDateInContext filter to make time and date location aware, but
268 // only if the supplied interval is day-granular. This is as wrapper for
269 // egOrgDateInContext to be used for circulation due date /only/.
270 .filter('egDueDate',['$filter','egCore',
271 function($filter , egCore) {
273 function eg_context_due_date_filter (date, format, orgID, interval) {
275 var secs = egCore.date.intervalToSeconds(interval);
276 if (secs === null || secs % 86400 != 0) {
281 return $filter('egOrgDateInContext')(date, format, orgID, interval);
284 eg_context_due_date_filter.$stateful = true;
286 return eg_context_due_date_filter;
290 // TODO: perhaps this should live elsewhere
291 .filter('join', function() {
292 return function(arr,sep) {
293 if (typeof arr == 'object' && arr.constructor == Array) {
294 return arr.join(sep || ',');
304 * egProgressDialog.open();
305 * egProgressDialog.open({value : 0});
306 * egProgressDialog.open({value : 0, max : 123});
307 * egProgressDialog.increment();
308 * egProgressDialog.increment();
309 * egProgressDialog.close();
311 * Each dialog has 2 numbers, 'max' and 'value'.
312 * The content of these values determines how the dialog displays.
314 * There are 3 flavors:
316 * -- value is set, max is set
317 * determinate: shows a progression with a percent complete.
319 * -- value is set, max is unset
320 * semi-determinate, with a value report. Shows a value-less
321 * <progress/>, but shows the value as a number in the dialog.
323 * This is useful in cases where the total number of items to retrieve
324 * from the server is unknown, but we know how many items we've
325 * retrieved thus far. It helps to reinforce that something specific
326 * is happening, but we don't know when it will end.
329 * indeterminate: shows a generic value-less <progress/> with no
330 * clear indication of progress.
332 * Only 1 egProgressDialog instance will be activate at a time.
333 * Each invocation of .open() destroys any existing instance.
336 /* Simple storage class for egProgressDialog data maintenance.
337 * This data lives outside of egProgressDialog so it can be
338 * directly imported into egProgressDialog's $uibModalInstance.
340 .factory('egProgressData', [
342 var service = {}; // max/value initially unset
344 service.reset = function() {
346 delete service.value;
349 service.hasvalue = function() {
350 return Number.isInteger(service.value);
353 service.hasmax = function() {
354 return Number.isInteger(service.max);
357 service.percent = function() {
358 if (service.hasvalue() &&
361 service.value <= service.max)
362 return Math.floor((service.value / service.max) * 100);
370 .factory('egProgressDialog', [
371 'egProgressData','$uibModal',
372 function(egProgressData , $uibModal) {
375 service.open = function(args) {
376 return $uibModal.open({
377 templateUrl: './share/t_progress_dialog',
378 /* backdrop: 'static', */ /* allow 'cancelling' of progress dialog */
379 controller: ['$scope','$uibModalInstance','egProgressData',
380 function( $scope , $uibModalInstance , egProgressData) {
381 // Once the new modal instance is available, force-
382 // kill any other instances
385 // Reset to an indeterminate progress bar,
386 // overlay with caller values.
387 egProgressData.reset();
388 service.update(angular.extend({}, args));
390 service.currentInstance = $uibModalInstance;
391 $scope.data = egProgressData; // tiny service
397 service.close = function(warn) {
398 if (service.currentInstance) {
400 console.warn("egProgressDialog replacing existing instance. "
401 + "Only one may be open at a time.");
403 service.currentInstance.close();
404 delete service.currentInstance;
408 // Set the current state of the progress bar.
409 service.update = function(args) {
410 if (args.max != undefined)
411 egProgressData.max = args.max;
412 if (args.value != undefined)
413 egProgressData.value = args.value;
416 // Increment the current value. If no amount is specified,
417 // it increments by 1. Calling increment() on an indetermite
418 // progress bar will force it to be a (semi-)determinate bar.
419 service.increment = function(amt) {
420 if (!Number.isInteger(amt)) amt = 1;
422 if (!egProgressData.hasvalue())
423 egProgressData.value = 0;
425 egProgressData.value += amt;
432 * egAlertDialog.open({message : 'hello {{name}}'}).result.then(
433 * function() { console.log('alert closed') });
435 .factory('egAlertDialog',
437 ['$uibModal','$interpolate',
438 function($uibModal , $interpolate) {
441 service.open = function(message, msg_scope) {
442 return $uibModal.open({
443 templateUrl: './share/t_alert_dialog',
445 controller: ['$scope', '$uibModalInstance',
446 function($scope, $uibModalInstance) {
447 $scope.message = $interpolate(message)(msg_scope);
448 $scope.ok = function() {
449 if (msg_scope && msg_scope.ok) msg_scope.ok();
450 $uibModalInstance.close()
461 * egConfirmDialog.open("some message goes {{here}}", {
462 * here : 'foo', ok : function() {}, cancel : function() {}},
465 .factory('egConfirmDialog',
467 ['$uibModal','$interpolate',
468 function($uibModal, $interpolate) {
471 service.open = function(title, message, msg_scope, ok_button_label, cancel_button_label) {
472 msg_scope = msg_scope || {};
473 return $uibModal.open({
474 templateUrl: './share/t_confirm_dialog',
476 controller: ['$scope', '$uibModalInstance',
477 function($scope, $uibModalInstance) {
478 $scope.title = $interpolate(title)(msg_scope);
479 $scope.message = $interpolate(message)(msg_scope);
480 $scope.ok_button_label = $interpolate(ok_button_label || '')(msg_scope);
481 $scope.cancel_button_label = $interpolate(cancel_button_label || '')(msg_scope);
482 $scope.ok = function() {
483 if (msg_scope.ok) msg_scope.ok();
484 $uibModalInstance.close()
486 $scope.cancel = function() {
487 if (msg_scope.cancel) msg_scope.cancel();
488 $uibModalInstance.dismiss();
499 * egPromptDialog.open(
500 * "prompt message goes {{here}}",
501 * promptValue, // optional
504 * ok : function(value) {console.log(value)},
505 * cancel : function() {console.log('prompt denied')}
509 .factory('egPromptDialog',
511 ['$uibModal','$interpolate',
512 function($uibModal, $interpolate) {
515 service.open = function(message, promptValue, msg_scope) {
516 return $uibModal.open({
517 templateUrl: './share/t_prompt_dialog',
519 controller: ['$scope', '$uibModalInstance',
520 function($scope, $uibModalInstance) {
521 $scope.message = $interpolate(message)(msg_scope);
522 $scope.args = {value : promptValue || ''};
524 $scope.ok = function() {
525 if (msg_scope && msg_scope.ok) msg_scope.ok($scope.args.value);
526 $uibModalInstance.close($scope.args);
528 $scope.cancel = function() {
529 if (msg_scope && msg_scope.cancel) msg_scope.cancel();
530 $uibModalInstance.dismiss();
541 * egSelectDialog.open(
542 * "message goes {{here}}",
543 * list, // ['values','for','dropdown'],
544 * selectedValue, // optional
547 * ok : function(value) {console.log(value)},
548 * cancel : function() {console.log('prompt denied')}
552 .factory('egSelectDialog',
554 ['$uibModal','$interpolate',
555 function($uibModal, $interpolate) {
558 service.open = function(message, inputList, selectedValue, msg_scope) {
559 return $uibModal.open({
560 templateUrl: './share/t_select_dialog',
562 controller: ['$scope', '$uibModalInstance',
563 function($scope, $uibModalInstance) {
564 $scope.message = $interpolate(message)(msg_scope);
567 value : selectedValue
570 $scope.ok = function() {
571 if (msg_scope.ok) msg_scope.ok($scope.args.value);
572 $uibModalInstance.close()
574 $scope.cancel = function() {
575 if (msg_scope.cancel) msg_scope.cancel();
576 $uibModalInstance.dismiss();
587 * Warn on page unload and give the user a chance to avoid navigating
588 * away from the current page.
589 * Only one handler is supported per page.
590 * NOTE: we can't use an egUnloadDialog as the dialog builder, because
591 * it renders asynchronously, which allows the page to redirect before
592 * the dialog appears.
594 .factory('egUnloadPrompt', [
595 '$window','egStrings',
596 function($window , egStrings) {
597 var service = {attached : false};
599 // attach a page/scope unload prompt
600 service.attach = function($scope, msg) {
601 if (service.attached) return;
602 service.attached = true;
604 // handle page change
605 $($window).on('beforeunload', function() {
607 return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
612 // If a scope was provided, attach a scope-change handler,
613 // similar to the page-page prompt.
614 service.locChangeCancel =
615 $scope.$on('$locationChangeStart', function(evt, next, current) {
616 if (confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) {
617 // user allowed the page to change.
618 // Clear the unload handler.
621 evt.preventDefault();
626 // remove the page unload prompt
627 service.clear = function() {
628 $($window).off('beforeunload');
629 if (service.locChangeCancel)
630 service.locChangeCancel();
631 service.attached = false;
638 * egAddCopyAlertDialog - manage copy alerts
640 .factory('egAddCopyAlertDialog',
641 ['$uibModal','$interpolate','egCore',
642 function($uibModal , $interpolate , egCore) {
645 service.open = function(args) {
646 return $uibModal.open({
647 templateUrl: './share/t_add_copy_alert_dialog',
648 controller: ['$scope','$q','$uibModalInstance',
649 function( $scope , $q , $uibModalInstance) {
651 $scope.copy_ids = args.copy_ids;
652 egCore.pcrud.search('ccat',
656 ).then(function (ccat) {
657 $scope.alert_types = ccat;
660 $scope.copy_alert = {
661 create_staff : egCore.auth.user().id(),
666 $scope.ok = function(copy_alert) {
667 if (typeof(copy_alert.note) != 'undefined' &&
668 copy_alert.note != '') {
670 angular.forEach($scope.copy_ids, function (cp_id) {
671 var a = new egCore.idl.aca();
673 a.create_staff(copy_alert.create_staff);
674 a.note(copy_alert.note);
675 a.temp(copy_alert.temp ? 't' : 'f');
679 $scope.alert_types.filter(function(at) {
680 return at.id() == copy_alert.alert_type;
683 copy_alerts.push( a );
685 if (copy_alerts.length > 0) {
686 egCore.pcrud.apply(copy_alerts).finally(function() {
687 if (args.ok) args.ok();
688 $uibModalInstance.close()
692 if (args.ok) args.ok();
693 $uibModalInstance.close()
696 $scope.cancel = function() {
697 if (args.cancel) args.cancel();
698 $uibModalInstance.dismiss();
709 * egCopyAlertManagerDialog - manage copy alerts
711 .factory('egCopyAlertManagerDialog',
712 ['$uibModal','$interpolate','egCore',
713 function($uibModal , $interpolate , egCore) {
716 service.get_user_copy_alerts = function(copy_id) {
717 return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
718 { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
723 service.open = function(args) {
724 return $uibModal.open({
725 templateUrl: './share/t_copy_alert_manager_dialog',
726 controller: ['$scope','$q','$uibModalInstance',
727 function( $scope , $q , $uibModalInstance) {
729 function init(args) {
730 var defer = $q.defer();
732 service.get_user_copy_alerts(args.copy_id).then(function(aca) {
736 defer.resolve(args.alerts);
738 return defer.promise;
741 // returns a promise resolved with the list of circ statuses
742 $scope.get_copy_statuses = function() {
744 return $q.when(egCore.env.ccs.list);
746 return egCore.pcrud.retrieveAll('ccs', null, {atomic : true})
747 .then(function(list) {
748 egCore.env.absorbList(list, 'ccs');
753 $scope.mode = args.mode || 'checkin';
755 var next_statuses = [];
756 var seen_statuses = {};
757 $scope.next_statuses = [];
759 'the_next_status' : null
761 init(args).then(function(copy_alerts) {
762 $scope.alerts = copy_alerts;
763 angular.forEach($scope.alerts, function(copy_alert) {
764 var state = copy_alert.alert_type().state();
765 copy_alert.evt = copy_alert.alert_type().event();
767 copy_alert.message = copy_alert.note() ||
768 egCore.strings.ON_DEMAND_COPY_ALERT[copy_alert.evt][state];
770 if (copy_alert.temp() == 't') {
771 angular.forEach(copy_alert.alert_type().next_status(), function (st) {
772 if (!seen_statuses[st]) {
773 seen_statuses[st] = true;
774 next_statuses.push(st);
779 if ($scope.mode == 'checkin' && next_statuses.length > 0) {
780 $scope.get_copy_statuses().then(function() {
781 angular.forEach(next_statuses, function(st) {
782 if (egCore.env.ccs.map[st])
783 $scope.next_statuses.push(egCore.env.ccs.map[st]);
785 $scope.params.the_next_status = $scope.next_statuses[0].id();
790 $scope.isAcknowledged = function(copy_alert) {
791 return (copy_alert.acked);
793 $scope.canBeAcknowledged = function(copy_alert) {
794 return (!copy_alert.ack_time() && copy_alert.temp() == 't');
796 $scope.canBeRemoved = function(copy_alert) {
797 return (!copy_alert.ack_time() && copy_alert.temp() == 'f');
800 $scope.ok = function() {
802 angular.forEach($scope.alerts, function (copy_alert) {
803 if (copy_alert.acked) {
804 copy_alert.ack_time('now');
805 copy_alert.ack_staff(egCore.auth.user().id());
806 copy_alert.ischanged(true);
807 acks.push(copy_alert);
810 if (acks.length > 0) {
811 egCore.pcrud.apply(acks).finally(function() {
812 if (args.ok) args.ok($scope.params.the_next_status);
813 $uibModalInstance.close()
816 if (args.ok) args.ok($scope.params.the_next_status);
817 $uibModalInstance.close()
820 $scope.cancel = function() {
821 if (args.cancel) args.cancel();
822 $uibModalInstance.dismiss();
833 * egCopyAlertEditorDialog - manage copy alerts
835 .factory('egCopyAlertEditorDialog',
836 ['$uibModal','$interpolate','egCore',
837 function($uibModal , $interpolate , egCore) {
840 service.get_user_copy_alerts = function(copy_id) {
841 return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
842 { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
847 service.get_copy_alert_types = function() {
848 return egCore.pcrud.search('ccat',
855 service.open = function(args) {
856 return $uibModal.open({
857 templateUrl: './share/t_copy_alert_editor_dialog',
858 controller: ['$scope','$q','$uibModalInstance',
859 function( $scope , $q , $uibModalInstance) {
861 function init(args) {
862 var defer = $q.defer();
864 service.get_user_copy_alerts(args.copy_id).then(function(aca) {
868 defer.resolve(args.alerts);
870 return defer.promise;
873 init(args).then(function(copy_alerts) {
874 $scope.copy_alert_list = copy_alerts;
876 service.get_copy_alert_types().then(function(ccat) {
877 $scope.alert_types = ccat;
880 $scope.ok = function() {
881 egCore.pcrud.apply($scope.copy_alert_list).finally(function() {
882 $uibModalInstance.close();
885 $scope.cancel = function() {
886 if (args.cancel) args.cancel();
887 $uibModalInstance.dismiss();
896 .directive('aDisabled', function() {
899 compile: function(tElement, tAttrs, transclude) {
901 tAttrs["ngClick"] = ("ng-click", "!("+tAttrs["aDisabled"]+") && ("+tAttrs["ngClick"]+")");
903 //Toggle "disabled" to class when aDisabled becomes true
904 return function (scope, iElement, iAttrs) {
905 scope.$watch(iAttrs["aDisabled"], function(newValue) {
906 if (newValue !== undefined) {
907 iElement.toggleClass("disabled", newValue);
911 //Disable href on click
912 iElement.on("click", function(e) {
913 if (scope.$eval(iAttrs["aDisabled"])) {
922 .directive('egBasicComboBox', function() {
927 list: "=", // list of strings
936 '<div class="input-group">'+
937 '<input placeholder="{{placeholder}}" type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()" focus-me="focusMe">'+
938 '<div class="input-group-btn" uib-dropdown ng-class="{open:isopen}">'+
939 '<button type="button" ng-click="showAll()" ng-disabled="egDisabled" class="btn btn-default" uib-dropdown-toggle><span class="caret"></span></button>'+
940 '<ul dropdown-menu class="dropdown-menu-right">'+
941 '<li ng-repeat="item in list|filter:selected:compare"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
942 '<li ng-if="complete_list" class="divider"><span></span></li>'+
943 '<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
947 controller: ['$scope','$filter',
948 function( $scope , $filter) {
950 $scope.complete_list = false;
951 $scope.isopen = false;
952 $scope.clickedopen = false;
953 $scope.clickedclosed = null;
955 $scope.compare = function (ex, act) {
956 if (act === null || act === undefined) return true;
957 if (act.toString) act = act.toString();
958 return new RegExp(act.toLowerCase()).test(ex)
961 $scope.showAll = function () {
963 $scope.clickedopen = !$scope.clickedopen;
965 if ($scope.clickedclosed === null) {
966 if (!$scope.clickedopen) {
967 $scope.clickedclosed = true;
970 $scope.clickedclosed = !$scope.clickedopen;
973 if ($scope.selected && $scope.selected.length > 0) $scope.complete_list = true;
974 if (!$scope.selected || $scope.selected.length == 0) $scope.complete_list = false;
978 $scope.makeOpen = function () {
979 $scope.isopen = $scope.clickedopen || ($filter('filter')(
982 ).length > 0 && $scope.selected.length > 0);
983 if ($scope.clickedclosed) {
984 $scope.isopen = false;
985 $scope.clickedclosed = null;
989 $scope.changeValue = function (newVal) {
990 $scope.selected = newVal;
991 $scope.isopen = false;
992 $scope.clickedclosed = null;
993 $scope.clickedopen = false;
994 if ($scope.selected.length == 0) $scope.complete_list = false;
995 if ($scope.onSelect) $scope.onSelect();
1004 * Nested org unit selector modeled as a Bootstrap dropdown button.
1006 .directive('egOrgSelector', function() {
1010 replace : true, // makes styling easier
1012 selected : '=', // defaults to workstation or root org,
1013 // unless the nodefault attibute exists
1015 // Each org unit is passed into this function and, for
1016 // any org units where the response value is true, the
1017 // org unit will not be added to the selector.
1020 // Each org unit is passed into this function and, for
1021 // any org units where the response value is true, the
1022 // org unit will not be available for selection.
1025 // if set to true, disable the UI element altogether
1028 // Caller can either $watch(selected, ..) or register an
1029 // onchange handler.
1032 // optional primary drop-down button label
1035 // optional name of settings key for persisting
1036 // the last selected org unit
1040 // any reason to move this into a TT2 template?
1042 '<div class="btn-group eg-org-selector" uib-dropdown>'
1043 + '<button type="button" class="btn btn-default" uib-dropdown-toggle ng-disabled="disable_button">'
1044 + '<span style="padding-right: 5px;">{{getSelectedName()}}</span>'
1045 + '<span class="caret"></span>'
1047 + '<ul uib-dropdown-menu class="scrollable-menu">'
1048 + '<li ng-repeat="org in orgList" ng-hide="hiddenTest(org.id)">'
1049 + '<a href ng-click="orgChanged(org)" a-disabled="disableTest(org.id)" '
1050 + 'style="padding-left: {{org.depth * 10 + 5}}px">'
1051 + '{{org.shortname}}'
1057 controller : ['$scope','$timeout','egCore','egStartup','egLovefield','$q',
1058 function($scope , $timeout , egCore , egStartup , egLovefield , $q) {
1060 if ($scope.alldisabled) {
1061 $scope.disable_button = $scope.alldisabled == 'true' ? true : false;
1063 $scope.disable_button = false;
1066 // avoid linking the full fleshed tree to the scope by
1067 // tossing in a flattened list.
1069 // Run-time code referencing post-start data should be run
1070 // from within a startup block, otherwise accessing this
1071 // module before startup completes will lead to failure.
1073 // controller() runs before link().
1074 // This post-startup code runs after link().
1078 return egCore.env.classLoaders.aou();
1083 $scope.orgList = egCore.org.list().map(function(org) {
1086 shortname : org.shortname(),
1087 depth : org.ou_type().depth()
1092 // Apply default values
1094 if ($scope.stickySetting) {
1095 var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
1097 $scope.selected = egCore.org.get(orgId);
1101 if (!$scope.selected && !$scope.nodefault && egCore.auth.user()) {
1103 egCore.org.get(egCore.auth.user().ws_ou());
1106 fire_orgsel_onchange(); // no-op if nothing is selected
1111 * Fire onchange handler after a timeout, so the
1112 * $scope.selected value has a chance to propagate to
1113 * the page controllers before the onchange fires. This
1114 * way, the caller does not have to manually capture the
1115 * $scope.selected value during onchange.
1117 function fire_orgsel_onchange() {
1118 if (!$scope.selected || !$scope.onchange) return;
1119 $timeout(function() {
1121 'egOrgSelector onchange('+$scope.selected.id()+')');
1122 $scope.onchange($scope.selected)
1126 $scope.getSelectedName = function() {
1127 if ($scope.selected && $scope.selected.shortname)
1128 return $scope.selected.shortname();
1129 return $scope.label;
1132 $scope.orgChanged = function(org) {
1133 $scope.selected = egCore.org.get(org.id);
1134 if ($scope.stickySetting) {
1135 egCore.hatch.setLocalItem($scope.stickySetting, org.id);
1137 fire_orgsel_onchange();
1141 link : function(scope, element, attrs, egGridCtrl) {
1143 // boolean fields are presented as value-less attributes
1147 if (angular.isDefined(attrs[field]))
1148 scope[field] = true;
1150 scope[field] = false;
1157 .directive('nextOnEnter', function () {
1158 return function (scope, element, attrs) {
1159 element.bind("keydown keypress", function (event) {
1160 if(event.which === 13) {
1161 $('#'+attrs.nextOnEnter).focus();
1162 event.preventDefault();
1168 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
1169 .directive('egEnter', function () {
1170 return function (scope, element, attrs) {
1171 element.bind("keydown keypress", function (event) {
1172 if(event.which === 13) {
1173 scope.$apply(function (){
1174 scope.$eval(attrs.egEnter);
1177 event.preventDefault();
1184 * Handy wrapper directive for uib-datapicker-popup
1187 'egDateInput', ['egStrings', 'egCore',
1188 function(egStrings, egCore) {
1200 hideDatePicker : '=',
1206 templateUrl: './share/t_datetime',
1208 controller : ['$scope', function($scope) {
1210 minDate : $scope.minDate,
1211 maxDate : $scope.maxDate
1214 var maxDateObj = $scope.maxDate ? new Date($scope.maxDate) : null;
1215 var minDateObj = $scope.minDate ? new Date($scope.minDate) : null;
1217 if ($scope.outOfRange !== undefined && (maxDateObj || minDateObj)) {
1218 $scope.$watch('ngModel', function (n,o) {
1221 var newdate = new Date(n);
1222 if (maxDateObj && newdate.getTime() > maxDateObj.getTime()) bad = true;
1223 if (minDateObj && newdate.getTime() < minDateObj.getTime()) bad = true;
1224 $scope.outOfRange = bad;
1229 link : function(scope, elm, attrs) {
1230 if (!scope.closeText)
1231 scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
1233 if ('showTimePicker' in attrs)
1234 scope.showTimePicker = true;
1236 var default_format = 'mediumDate';
1237 egCore.org.settings(['format.date']).then(function(set) {
1238 default_format = set['format.date'];
1239 scope.date_format = (scope.dateFormat) ?
1249 * egFmValueSelector - widget for selecting a value from list specified
1252 .directive('egFmValueSelector', function() {
1260 // optional filter for refining the set of rows that
1261 // get returned. Example:
1263 // filter="{'column':{'=':null}}"
1266 // optional name of settings key for persisting
1267 // the last selected value
1268 stickySetting : '@',
1270 // optional OU setting for fetching default value;
1271 // used only if sticky setting not set
1275 templateUrl : './share/t_fm_value_selector',
1276 controller : ['$scope','egCore', function($scope , egCore) {
1278 $scope.org = egCore.org; // for use in the link function
1279 $scope.auth = egCore.auth; // for use in the link function
1280 $scope.hatch = egCore.hatch // for use in the link function
1282 function flatten_linked_values(cls, list) {
1284 var fields = egCore.idl.classes[cls].fields;
1287 angular.forEach(fields, function(fld) {
1288 if (fld.datatype == 'id') {
1289 id_field = fld.name;
1290 selector = fld.selector ? fld.selector : id_field;
1294 angular.forEach(list, function(item) {
1295 var rec = egCore.idl.toHash(item);
1298 name : rec[selector]
1305 search[egCore.idl.classes[$scope.idlClass].pkey] = {'!=' : null};
1306 if ($scope.filter) {
1307 angular.extend(search, $scope.filter);
1309 egCore.pcrud.search(
1310 $scope.idlClass, search, {}, {atomic : true}
1311 ).then(function(list) {
1312 $scope.linked_values = flatten_linked_values($scope.idlClass, list);
1315 $scope.handleChange = function(value) {
1316 if ($scope.stickySetting) {
1317 egCore.hatch.setLocalItem($scope.stickySetting, value);
1322 link : function(scope, element, attrs) {
1323 if (scope.stickySetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1324 var value = scope.hatch.getLocalItem(scope.stickySetting);
1325 scope.ngModel = value;
1327 if (scope.ouSetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1328 scope.org.settings([scope.ouSetting], scope.auth.user().ws_ou())
1329 .then(function(set) {
1330 var value = parseInt(set[scope.ouSetting]);
1332 scope.ngModel = value;
1340 * egShareDepthSelector - widget for selecting a share depth
1342 .directive('egShareDepthSelector', function() {
1350 templateUrl : './share/t_share_depth_selector',
1351 controller : ['$scope','egCore', function($scope , egCore) {
1353 egCore.pcrud.search('aout',
1354 { id : {'!=' : null} },
1355 { order_by : {aout : ['depth', 'name']} },
1357 ).then(function(list) {
1359 angular.forEach(list, function(aout) {
1360 var depth = parseInt(aout.depth());
1361 if (depth in scratch) {
1362 scratch[depth].push(aout.name());
1364 scratch[depth] = [ aout.name() ]
1367 scratch.forEach(function(val, idx) {
1368 $scope.values.push({ id : idx, name : scratch[idx].join(' / ') });
1376 * egHelpPopover - a helpful widget
1378 .directive('egHelpPopover', function() {
1386 templateUrl : './share/t_help_popover',
1387 controller : ['$scope','$sce', function($scope , $sce) {
1388 if ($scope.helpLink) {
1389 $scope.helpHtml = $sce.trustAsHtml(
1390 '<a target="_new" href="' + $scope.helpLink + '">' +
1391 $scope.helpText + '</a>'
1398 .factory('egWorkLog', ['egCore', function(egCore) {
1401 service.retrieve_all = function() {
1402 var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1403 var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1405 return { 'work_log' : workLog, 'patron_log' : patronLog };
1408 service.record = function(message,data) {
1411 if (typeof egCore != 'undefined') {
1412 if (typeof egCore.env != 'undefined') {
1413 if (typeof egCore.env.aous != 'undefined') {
1414 max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
1415 max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
1417 console.log('worklog: missing egCore.env.aous');
1420 console.log('worklog: missing egCore.env');
1423 console.log('worklog: missing egCore');
1426 if (typeof egCore.org != 'undefined') {
1427 if (typeof egCore.org.cachedSettings != 'undefined') {
1428 max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
1430 console.log('worklog: missing egCore.org.cachedSettings');
1433 console.log('worklog: missing egCore.org');
1437 if (typeof egCore.org != 'undefined') {
1438 if (typeof egCore.org.cachedSettings != 'undefined') {
1439 max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
1441 console.log('worklog: missing egCore.org.cachedSettings');
1444 console.log('worklog: missing egCore.org');
1449 console.log('worklog: defaulting to max_entries = ' + max_entries);
1453 console.log('worklog: defaulting to max_patrons = ' + max_patrons);
1456 var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1457 var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1459 'when' : new Date(),
1461 'action' : data.action,
1462 'actor' : egCore.auth.user().usrname()
1464 if (data.action == 'checkin') {
1465 entry['item'] = data.response.params.copy_barcode;
1466 entry['item_id'] = data.response.data.acp.id();
1467 if (data.response.data.au) {
1468 entry['user'] = data.response.data.au.family_name();
1469 entry['patron_id'] = data.response.data.au.id();
1472 if (data.action == 'checkout') {
1473 entry['item'] = data.response.params.copy_barcode;
1474 entry['user'] = data.response.data.au.family_name();
1475 entry['item_id'] = data.response.data.acp.id();
1476 entry['patron_id'] = data.response.data.au.id();
1478 if (data.action == 'noncat_checkout') {
1479 entry['user'] = data.response.data.au.family_name();
1480 entry['patron_id'] = data.response.data.au.id();
1482 if (data.action == 'renew') {
1483 entry['item'] = data.response.params.copy_barcode;
1484 entry['user'] = data.response.data.au.family_name();
1485 entry['item_id'] = data.response.data.acp.id();
1486 entry['patron_id'] = data.response.data.au.id();
1488 if (data.action == 'requested_hold'
1489 || data.action == 'edited_patron'
1490 || data.action == 'registered_patron'
1491 || data.action == 'paid_bill') {
1492 entry['patron_id'] = data.patron_id;
1494 if (data.action == 'requested_hold') {
1495 entry['hold_id'] = data.hold_id;
1497 if (data.action == 'paid_bill') {
1498 entry['amount'] = data.total_amount;
1501 workLog.push( entry );
1502 if (workLog.length > max_entries) workLog.shift();
1503 egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
1505 if (entry['patron_id']) {
1507 for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
1508 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
1511 if (temp.length > max_patrons) temp.shift();
1513 egCore.hatch.setLocalItem('eg.patron_log',patronLog);
1516 console.log('worklog',entry);