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 if (model.assign && typeof model.assign == 'function')
24 scope.$apply(model.assign(scope, false));
32 * <input blur-me="pleaseBlurMe"/>
33 * $scope.pleaseBlurMe = true
34 * Useful for de-focusing when no other obvious focus target exists
38 function($timeout , $parse) {
40 link: function(scope, element, attrs) {
41 var model = $parse(attrs.blurMe);
42 scope.$watch(model, function(value) {
44 $timeout(function() {element[0].blur()});
46 element.bind('focus', function() {
48 scope.$apply(model.assign(scope, false));
56 // <input select-me="iWantToBeSelected"/>
57 // $scope.iWantToBeSelected = true;
58 .directive('selectMe',
60 function($timeout , $parse) {
62 link: function(scope, element, attrs) {
63 var model = $parse(attrs.selectMe);
64 scope.$watch(model, function(value) {
66 $timeout(function() {element[0].select()});
68 element.bind('blur', function() {
70 scope.$apply(model.assign(scope, false));
77 // <select int-to-str ><option value="1">Value</option></select>
78 // use integer models for string values
79 .directive('intToStr', function() {
83 link: function(scope, element, attrs, ngModel) {
84 ngModel.$parsers.push(function(value) {
85 return parseInt(value);
87 ngModel.$formatters.push(function(value) {
94 // <input str-to-int value="10"/>
95 .directive('strToInt', function() {
99 link: function(scope, element, attrs, ngModel) {
100 ngModel.$parsers.push(function(value) {
103 ngModel.$formatters.push(function(value) {
104 return parseInt(value);
110 // <input float-to-str
111 .directive('floatToStr', function() {
115 link: function(scope, element, attrs, ngModel) {
116 ngModel.$parsers.push(function(value) {
117 return parseFloat(value);
119 ngModel.$formatters.push(function(value) {
126 .directive('strToFloat', function() {
130 link: function(scope, element, attrs, ngModel) {
131 ngModel.$parsers.push(function(value) {
134 ngModel.$formatters.push(function(value) {
135 return parseFloat(value);
142 // <div ng-repeat="item in items | reverse">{{item.name}}</div>
143 // http://stackoverflow.com/questions/15266671/angular-ng-repeat-in-reverse
144 // TODO: perhaps this should live elsewhere
145 .filter('reverse', function() {
146 return function(items) {
147 return items.slice().reverse();
152 // Overriding the core angular date filter with a moment-js based one for
153 // better timezone and formatting support.
154 .filter('date',function() {
171 var formatReplace = [
185 return function (date, format, tz) {
186 if (!date) return '';
189 date = new Date().toISOString();
192 var fmt = formatMap[format] || format;
193 angular.forEach(formatReplace, function (r) {
194 fmt = fmt.replace(r[0],r[1]);
198 var d = moment(date);
199 if (tz && tz !== '-') d.tz(tz);
201 return d.isValid() ? d.format(fmt) : '';
206 // 'egOrgDate' filter
207 // Uses moment.js and moment-timezone.js to put dates into the most appropriate
208 // timezone for a given (optional) org unit based on its lib.timezone setting
209 .filter('egOrgDate',['$filter','egCore',
210 function($filter , egCore) {
214 function eg_date_filter (date, fmt, ouID) {
216 if (angular.isObject(ouID)) {
217 if (angular.isFunction(ouID.id)) {
224 if (!tzcache[ouID]) {
226 egCore.org.settings('lib.timezone', ouID)
228 tzcache[ouID] = s['lib.timezone'] || OpenSRF.tz;
233 return $filter('date')(date, fmt, tzcache[ouID]);
236 eg_date_filter.$stateful = true;
238 return eg_date_filter;
241 // 'egOrgDateInContext' filter
242 // Uses the egOrgDate filter to make time and date location aware, and further
243 // modifies the format if one of [short, medium, long, full] to show only the
244 // date if the optional interval parameter is day-granular. This is
245 // particularly useful for due dates on circulations.
246 .filter('egOrgDateInContext',['$filter','egCore',
247 function($filter , egCore) {
249 function eg_context_date_filter (date, format, orgID, interval) {
251 if (!fmt) fmt = 'shortDate';
253 // if this is a simple, one-word format, and it doesn't say "Date" in it...
254 if (['short','medium','long','full'].filter(function(x){return fmt == x}).length > 0 && interval) {
255 var secs = egCore.date.intervalToSeconds(interval);
256 if (secs !== null && secs % 86400 == 0) fmt += 'Date';
259 return $filter('egOrgDate')(date, fmt, orgID);
262 eg_context_date_filter.$stateful = true;
264 return eg_context_date_filter;
267 // 'egDueDate' filter
268 // Uses the egOrgDateInContext filter to make time and date location aware, but
269 // only if the supplied interval is day-granular. This is as wrapper for
270 // egOrgDateInContext to be used for circulation due date /only/.
271 .filter('egDueDate',['$filter','egCore',
272 function($filter , egCore) {
274 function eg_context_due_date_filter (date, format, orgID, interval) {
276 var secs = egCore.date.intervalToSeconds(interval);
277 if (secs === null || secs % 86400 != 0) {
282 return $filter('egOrgDateInContext')(date, format, orgID, interval);
285 eg_context_due_date_filter.$stateful = true;
287 return eg_context_due_date_filter;
291 // TODO: perhaps this should live elsewhere
292 .filter('join', function() {
293 return function(arr,sep) {
294 if (typeof arr == 'object' && arr.constructor == Array) {
295 return arr.join(sep || ',');
305 * egProgressDialog.open();
306 * egProgressDialog.open({value : 0});
307 * egProgressDialog.open({value : 0, max : 123});
308 * egProgressDialog.increment();
309 * egProgressDialog.increment();
310 * egProgressDialog.close();
312 * Each dialog has 2 numbers, 'max' and 'value'.
313 * The content of these values determines how the dialog displays.
315 * There are 3 flavors:
317 * -- value is set, max is set
318 * determinate: shows a progression with a percent complete.
320 * -- value is set, max is unset
321 * semi-determinate, with a value report. Shows a value-less
322 * <progress/>, but shows the value as a number in the dialog.
324 * This is useful in cases where the total number of items to retrieve
325 * from the server is unknown, but we know how many items we've
326 * retrieved thus far. It helps to reinforce that something specific
327 * is happening, but we don't know when it will end.
330 * indeterminate: shows a generic value-less <progress/> with no
331 * clear indication of progress.
333 * Only 1 egProgressDialog instance will be activate at a time.
334 * Each invocation of .open() destroys any existing instance.
337 /* Simple storage class for egProgressDialog data maintenance.
338 * This data lives outside of egProgressDialog so it can be
339 * directly imported into egProgressDialog's $uibModalInstance.
341 .factory('egProgressData', [
343 var service = {}; // max/value initially unset
345 service.reset = function() {
347 delete service.value;
350 service.hasvalue = function() {
351 return Number.isInteger(service.value);
354 service.hasmax = function() {
355 return Number.isInteger(service.max);
358 service.percent = function() {
359 if (service.hasvalue() &&
362 service.value <= service.max)
363 return Math.floor((service.value / service.max) * 100);
371 .factory('egProgressDialog', [
372 'egProgressData','$uibModal',
373 function(egProgressData , $uibModal) {
376 service.open = function(args) {
377 return $uibModal.open({
378 templateUrl: './share/t_progress_dialog',
379 /* backdrop: 'static', */ /* allow 'cancelling' of progress dialog */
380 controller: ['$scope','$uibModalInstance','egProgressData',
381 function( $scope , $uibModalInstance , egProgressData) {
382 // Once the new modal instance is available, force-
383 // kill any other instances
386 // Reset to an indeterminate progress bar,
387 // overlay with caller values.
388 egProgressData.reset();
389 service.update(angular.extend({}, args));
391 service.currentInstance = $uibModalInstance;
392 $scope.data = egProgressData; // tiny service
398 service.close = function(warn) {
399 if (service.currentInstance) {
401 console.warn("egProgressDialog replacing existing instance. "
402 + "Only one may be open at a time.");
404 service.currentInstance.close();
405 delete service.currentInstance;
409 // Set the current state of the progress bar.
410 service.update = function(args) {
411 if (args.max != undefined)
412 egProgressData.max = args.max;
413 if (args.value != undefined)
414 egProgressData.value = args.value;
417 // Increment the current value. If no amount is specified,
418 // it increments by 1. Calling increment() on an indetermite
419 // progress bar will force it to be a (semi-)determinate bar.
420 service.increment = function(amt) {
421 if (!Number.isInteger(amt)) amt = 1;
423 if (!egProgressData.hasvalue())
424 egProgressData.value = 0;
426 egProgressData.value += amt;
433 * egAlertDialog.open({message : 'hello {{name}}'}).result.then(
434 * function() { console.log('alert closed') });
436 .factory('egAlertDialog',
438 ['$uibModal','$interpolate',
439 function($uibModal , $interpolate) {
442 service.open = function(message, msg_scope) {
443 return $uibModal.open({
444 templateUrl: './share/t_alert_dialog',
446 controller: ['$scope', '$uibModalInstance',
447 function($scope, $uibModalInstance) {
448 $scope.message = $interpolate(message)(msg_scope);
449 $scope.ok = function() {
450 if (msg_scope && msg_scope.ok) msg_scope.ok();
451 $uibModalInstance.close()
462 * egConfirmDialog.open("some message goes {{here}}", {
463 * here : 'foo', ok : function() {}, cancel : function() {}},
466 .factory('egConfirmDialog',
468 ['$uibModal','$interpolate',
469 function($uibModal, $interpolate) {
472 service.open = function(title, message, msg_scope, ok_button_label, cancel_button_label) {
473 msg_scope = msg_scope || {};
474 return $uibModal.open({
475 templateUrl: './share/t_confirm_dialog',
477 controller: ['$scope', '$uibModalInstance',
478 function($scope, $uibModalInstance) {
479 $scope.title = $interpolate(title)(msg_scope);
480 $scope.message = $interpolate(message)(msg_scope);
481 $scope.ok_button_label = $interpolate(ok_button_label || '')(msg_scope);
482 $scope.cancel_button_label = $interpolate(cancel_button_label || '')(msg_scope);
483 $scope.ok = function() {
484 if (msg_scope.ok) msg_scope.ok();
485 $uibModalInstance.close()
487 $scope.cancel = function() {
488 if (msg_scope.cancel) msg_scope.cancel();
489 $uibModalInstance.dismiss();
500 * egPromptDialog.open(
501 * "prompt message goes {{here}}",
502 * promptValue, // optional
505 * ok : function(value) {console.log(value)},
506 * cancel : function() {console.log('prompt denied')}
510 .factory('egPromptDialog',
512 ['$uibModal','$interpolate',
513 function($uibModal, $interpolate) {
516 service.open = function(message, promptValue, msg_scope) {
517 return $uibModal.open({
518 templateUrl: './share/t_prompt_dialog',
520 controller: ['$scope', '$uibModalInstance',
521 function($scope, $uibModalInstance) {
522 $scope.message = $interpolate(message)(msg_scope);
523 $scope.args = {value : promptValue || ''};
525 $scope.ok = function() {
526 if (msg_scope && msg_scope.ok) msg_scope.ok($scope.args.value);
527 $uibModalInstance.close($scope.args);
529 $scope.cancel = function() {
530 if (msg_scope && msg_scope.cancel) msg_scope.cancel();
531 $uibModalInstance.dismiss();
542 * egSelectDialog.open(
543 * "message goes {{here}}",
544 * list, // ['values','for','dropdown'],
545 * selectedValue, // optional
548 * ok : function(value) {console.log(value)},
549 * cancel : function() {console.log('prompt denied')}
553 .factory('egSelectDialog',
555 ['$uibModal','$interpolate',
556 function($uibModal, $interpolate) {
559 service.open = function(message, inputList, selectedValue, msg_scope) {
560 return $uibModal.open({
561 templateUrl: './share/t_select_dialog',
563 controller: ['$scope', '$uibModalInstance',
564 function($scope, $uibModalInstance) {
565 $scope.message = $interpolate(message)(msg_scope);
568 value : selectedValue
571 $scope.ok = function() {
572 if (msg_scope.ok) msg_scope.ok($scope.args.value);
573 $uibModalInstance.close()
575 $scope.cancel = function() {
576 if (msg_scope.cancel) msg_scope.cancel();
577 $uibModalInstance.dismiss();
588 * Warn on page unload and give the user a chance to avoid navigating
589 * away from the current page.
590 * Only one handler is supported per page.
591 * NOTE: we can't use an egUnloadDialog as the dialog builder, because
592 * it renders asynchronously, which allows the page to redirect before
593 * the dialog appears.
595 .factory('egUnloadPrompt', [
596 '$window','egStrings',
597 function($window , egStrings) {
598 var service = {attached : false};
600 // attach a page/scope unload prompt
601 service.attach = function($scope, msg) {
602 if (service.attached) return;
603 service.attached = true;
605 // handle page change
606 $($window).on('beforeunload', function() {
608 return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
613 // If a scope was provided, attach a scope-change handler,
614 // similar to the page-page prompt.
615 service.locChangeCancel =
616 $scope.$on('$locationChangeStart', function(evt, next, current) {
617 if (confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) {
618 // user allowed the page to change.
619 // Clear the unload handler.
622 evt.preventDefault();
627 // remove the page unload prompt
628 service.clear = function() {
629 $($window).off('beforeunload');
630 if (service.locChangeCancel)
631 service.locChangeCancel();
632 service.attached = false;
639 * egAddCopyAlertDialog - manage copy alerts
641 .factory('egAddCopyAlertDialog',
642 ['$uibModal','$interpolate','egCore',
643 function($uibModal , $interpolate , egCore) {
646 service.open = function(args) {
647 return $uibModal.open({
648 templateUrl: './share/t_add_copy_alert_dialog',
649 controller: ['$scope','$q','$uibModalInstance',
650 function( $scope , $q , $uibModalInstance) {
652 $scope.copy_ids = args.copy_ids;
653 egCore.pcrud.search('ccat',
657 ).then(function (ccat) {
658 $scope.alert_types = ccat;
661 $scope.copy_alert = {
662 create_staff : egCore.auth.user().id(),
667 $scope.ok = function(copy_alert) {
668 if (typeof(copy_alert.note) != 'undefined' &&
669 copy_alert.note != '') {
671 angular.forEach($scope.copy_ids, function (cp_id) {
672 var a = new egCore.idl.aca();
674 a.create_staff(copy_alert.create_staff);
675 a.note(copy_alert.note);
676 a.temp(copy_alert.temp ? 't' : 'f');
680 $scope.alert_types.filter(function(at) {
681 return at.id() == copy_alert.alert_type;
684 copy_alerts.push( a );
686 if (copy_alerts.length > 0) {
687 egCore.pcrud.apply(copy_alerts).finally(function() {
688 if (args.ok) args.ok();
689 $uibModalInstance.close()
693 if (args.ok) args.ok();
694 $uibModalInstance.close()
697 $scope.cancel = function() {
698 if (args.cancel) args.cancel();
699 $uibModalInstance.dismiss();
710 * egCopyAlertManagerDialog - manage copy alerts
712 .factory('egCopyAlertManagerDialog',
713 ['$uibModal','$interpolate','egCore',
714 function($uibModal , $interpolate , egCore) {
717 service.get_user_copy_alerts = function(copy_id) {
718 return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
719 { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
724 service.open = function(args) {
725 return $uibModal.open({
726 templateUrl: './share/t_copy_alert_manager_dialog',
727 controller: ['$scope','$q','$uibModalInstance',
728 function( $scope , $q , $uibModalInstance) {
730 function init(args) {
731 var defer = $q.defer();
733 service.get_user_copy_alerts(args.copy_id).then(function(aca) {
737 defer.resolve(args.alerts);
739 return defer.promise;
742 // returns a promise resolved with the list of circ statuses
743 $scope.get_copy_statuses = function() {
745 return $q.when(egCore.env.ccs.list);
747 return egCore.pcrud.retrieveAll('ccs', null, {atomic : true})
748 .then(function(list) {
749 egCore.env.absorbList(list, 'ccs');
754 $scope.mode = args.mode || 'checkin';
756 var next_statuses = [];
757 var seen_statuses = {};
758 $scope.next_statuses = [];
760 'the_next_status' : null
762 init(args).then(function(copy_alerts) {
763 $scope.alerts = copy_alerts;
764 angular.forEach($scope.alerts, function(copy_alert) {
765 var state = copy_alert.alert_type().state();
766 copy_alert.evt = copy_alert.alert_type().event();
768 copy_alert.message = copy_alert.note() ||
769 egCore.strings.ON_DEMAND_COPY_ALERT[copy_alert.evt][state];
771 if (copy_alert.temp() == 't') {
772 angular.forEach(copy_alert.alert_type().next_status(), function (st) {
773 if (!seen_statuses[st]) {
774 seen_statuses[st] = true;
775 next_statuses.push(st);
780 if ($scope.mode == 'checkin' && next_statuses.length > 0) {
781 $scope.get_copy_statuses().then(function() {
782 angular.forEach(next_statuses, function(st) {
783 if (egCore.env.ccs.map[st])
784 $scope.next_statuses.push(egCore.env.ccs.map[st]);
786 $scope.params.the_next_status = $scope.next_statuses[0].id();
791 $scope.isAcknowledged = function(copy_alert) {
792 return (copy_alert.acked);
794 $scope.canBeAcknowledged = function(copy_alert) {
795 return (!copy_alert.ack_time() && copy_alert.temp() == 't');
797 $scope.canBeRemoved = function(copy_alert) {
798 return (!copy_alert.ack_time() && copy_alert.temp() == 'f');
801 $scope.ok = function() {
803 angular.forEach($scope.alerts, function (copy_alert) {
804 if (copy_alert.acked) {
805 copy_alert.ack_time('now');
806 copy_alert.ack_staff(egCore.auth.user().id());
807 copy_alert.ischanged(true);
808 acks.push(copy_alert);
811 if (acks.length > 0) {
812 egCore.pcrud.apply(acks).finally(function() {
813 if (args.ok) args.ok($scope.params.the_next_status);
814 $uibModalInstance.close()
817 if (args.ok) args.ok($scope.params.the_next_status);
818 $uibModalInstance.close()
821 $scope.cancel = function() {
822 if (args.cancel) args.cancel();
823 $uibModalInstance.dismiss();
834 * egCopyAlertEditorDialog - manage copy alerts
836 .factory('egCopyAlertEditorDialog',
837 ['$uibModal','$interpolate','egCore',
838 function($uibModal , $interpolate , egCore) {
841 service.get_user_copy_alerts = function(copy_id) {
842 return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
843 { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
848 service.get_copy_alert_types = function() {
849 return egCore.pcrud.search('ccat',
856 service.open = function(args) {
857 return $uibModal.open({
858 templateUrl: './share/t_copy_alert_editor_dialog',
859 controller: ['$scope','$q','$uibModalInstance',
860 function( $scope , $q , $uibModalInstance) {
862 function init(args) {
863 var defer = $q.defer();
865 service.get_user_copy_alerts(args.copy_id).then(function(aca) {
869 defer.resolve(args.alerts);
871 return defer.promise;
874 init(args).then(function(copy_alerts) {
875 $scope.copy_alert_list = copy_alerts;
877 service.get_copy_alert_types().then(function(ccat) {
878 $scope.alert_types = ccat;
881 $scope.ok = function() {
882 egCore.pcrud.apply($scope.copy_alert_list).finally(function() {
883 $uibModalInstance.close();
886 $scope.cancel = function() {
887 if (args.cancel) args.cancel();
888 $uibModalInstance.dismiss();
897 .directive('aDisabled', function() {
900 compile: function(tElement, tAttrs, transclude) {
902 tAttrs["ngClick"] = ("ng-click", "!("+tAttrs["aDisabled"]+") && ("+tAttrs["ngClick"]+")");
904 //Toggle "disabled" to class when aDisabled becomes true
905 return function (scope, iElement, iAttrs) {
906 scope.$watch(iAttrs["aDisabled"], function(newValue) {
907 if (newValue !== undefined) {
908 iElement.toggleClass("disabled", newValue);
912 //Disable href on click
913 iElement.on("click", function(e) {
914 if (scope.$eval(iAttrs["aDisabled"])) {
923 .directive('egBasicComboBox', function() {
928 list: "=", // list of strings
937 '<div class="input-group">'+
938 '<input placeholder="{{placeholder}}" type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()" focus-me="focusMe">'+
939 '<div class="input-group-btn" uib-dropdown ng-class="{open:isopen}">'+
940 '<button type="button" ng-click="showAll()" ng-disabled="egDisabled" class="btn btn-default" uib-dropdown-toggle><span class="caret"></span></button>'+
941 '<ul dropdown-menu class="dropdown-menu-right">'+
942 '<li ng-repeat="item in list|filter:selected:compare"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
943 '<li ng-if="complete_list" class="divider"><span></span></li>'+
944 '<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
948 controller: ['$scope','$filter',
949 function( $scope , $filter) {
951 $scope.complete_list = false;
952 $scope.isopen = false;
953 $scope.clickedopen = false;
954 $scope.clickedclosed = null;
956 $scope.compare = function (ex, act) {
957 if (act === null || act === undefined) return true;
958 if (act.toString) act = act.toString();
959 return new RegExp(act.toLowerCase()).test(ex)
962 $scope.showAll = function () {
964 $scope.clickedopen = !$scope.clickedopen;
966 if ($scope.clickedclosed === null) {
967 if (!$scope.clickedopen) {
968 $scope.clickedclosed = true;
971 $scope.clickedclosed = !$scope.clickedopen;
974 if ($scope.selected && $scope.selected.length > 0) $scope.complete_list = true;
975 if (!$scope.selected || $scope.selected.length == 0) $scope.complete_list = false;
979 $scope.makeOpen = function () {
980 $scope.isopen = $scope.clickedopen || ($filter('filter')(
983 ).length > 0 && $scope.selected.length > 0);
984 if ($scope.clickedclosed) {
985 $scope.isopen = false;
986 $scope.clickedclosed = null;
990 $scope.changeValue = function (newVal) {
991 $scope.selected = newVal;
992 $scope.isopen = false;
993 $scope.clickedclosed = null;
994 $scope.clickedopen = false;
995 if ($scope.selected.length == 0) $scope.complete_list = false;
996 if ($scope.onSelect) $scope.onSelect();
1005 * Nested org unit selector modeled as a Bootstrap dropdown button.
1007 .directive('egOrgSelector', function() {
1011 replace : true, // makes styling easier
1013 selected : '=', // defaults to workstation or root org,
1014 // unless the nodefault attibute exists
1016 // Each org unit is passed into this function and, for
1017 // any org units where the response value is true, the
1018 // org unit will not be added to the selector.
1021 // Each org unit is passed into this function and, for
1022 // any org units where the response value is true, the
1023 // org unit will not be available for selection.
1026 // if set to true, disable the UI element altogether
1029 // Caller can either $watch(selected, ..) or register an
1030 // onchange handler.
1033 // optional primary drop-down button label
1036 // optional name of settings key for persisting
1037 // the last selected org unit
1041 // any reason to move this into a TT2 template?
1043 '<div class="btn-group eg-org-selector" uib-dropdown>'
1044 + '<button type="button" class="btn btn-default" uib-dropdown-toggle ng-disabled="disable_button">'
1045 + '<span style="padding-right: 5px;">{{getSelectedName()}}</span>'
1046 + '<span class="caret"></span>'
1048 + '<ul uib-dropdown-menu class="scrollable-menu">'
1049 + '<li ng-repeat="org in orgList" ng-hide="hiddenTest(org.id)">'
1050 + '<a href ng-click="orgChanged(org)" a-disabled="disableTest(org.id)" '
1051 + 'style="padding-left: {{org.depth * 10 + 5}}px">'
1052 + '{{org.shortname}}'
1058 controller : ['$scope','$timeout','egCore','egStartup','egLovefield','$q',
1059 function($scope , $timeout , egCore , egStartup , egLovefield , $q) {
1061 if ($scope.alldisabled) {
1062 $scope.disable_button = $scope.alldisabled == 'true' ? true : false;
1064 $scope.disable_button = false;
1067 // avoid linking the full fleshed tree to the scope by
1068 // tossing in a flattened list.
1070 // Run-time code referencing post-start data should be run
1071 // from within a startup block, otherwise accessing this
1072 // module before startup completes will lead to failure.
1074 // controller() runs before link().
1075 // This post-startup code runs after link().
1079 return egCore.env.classLoaders.aou();
1084 $scope.orgList = egCore.org.list().map(function(org) {
1087 shortname : org.shortname(),
1088 depth : org.ou_type().depth()
1093 // Apply default values
1095 if ($scope.stickySetting) {
1096 var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
1098 $scope.selected = egCore.org.get(orgId);
1102 if (!$scope.selected && !$scope.nodefault && egCore.auth.user()) {
1104 egCore.org.get(egCore.auth.user().ws_ou());
1107 fire_orgsel_onchange(); // no-op if nothing is selected
1112 * Fire onchange handler after a timeout, so the
1113 * $scope.selected value has a chance to propagate to
1114 * the page controllers before the onchange fires. This
1115 * way, the caller does not have to manually capture the
1116 * $scope.selected value during onchange.
1118 function fire_orgsel_onchange() {
1119 if (!$scope.selected || !$scope.onchange) return;
1120 $timeout(function() {
1122 'egOrgSelector onchange('+$scope.selected.id()+')');
1123 $scope.onchange($scope.selected)
1127 $scope.getSelectedName = function() {
1128 if ($scope.selected && $scope.selected.shortname)
1129 return $scope.selected.shortname();
1130 return $scope.label;
1133 $scope.orgChanged = function(org) {
1134 $scope.selected = egCore.org.get(org.id);
1135 if ($scope.stickySetting) {
1136 egCore.hatch.setLocalItem($scope.stickySetting, org.id);
1138 fire_orgsel_onchange();
1142 link : function(scope, element, attrs, egGridCtrl) {
1144 // boolean fields are presented as value-less attributes
1148 if (angular.isDefined(attrs[field]))
1149 scope[field] = true;
1151 scope[field] = false;
1158 .directive('nextOnEnter', function () {
1159 return function (scope, element, attrs) {
1160 element.bind("keydown keypress", function (event) {
1161 if(event.which === 13) {
1162 $('#'+attrs.nextOnEnter).focus();
1163 event.preventDefault();
1169 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
1170 .directive('egEnter', function () {
1171 return function (scope, element, attrs) {
1172 element.bind("keydown keypress", function (event) {
1173 if(event.which === 13) {
1174 scope.$apply(function (){
1175 scope.$eval(attrs.egEnter);
1178 event.preventDefault();
1185 * Handy wrapper directive for uib-datapicker-popup
1188 'egDateInput', ['egStrings', 'egCore',
1189 function(egStrings, egCore) {
1201 hideDatePicker : '=',
1207 templateUrl: './share/t_datetime',
1209 controller : ['$scope', function($scope) {
1211 minDate : $scope.minDate,
1212 maxDate : $scope.maxDate
1215 var maxDateObj = $scope.maxDate ? new Date($scope.maxDate) : null;
1216 var minDateObj = $scope.minDate ? new Date($scope.minDate) : null;
1218 if ($scope.outOfRange !== undefined && (maxDateObj || minDateObj)) {
1219 $scope.$watch('ngModel', function (n,o) {
1222 var newdate = new Date(n);
1223 if (maxDateObj && newdate.getTime() > maxDateObj.getTime()) bad = true;
1224 if (minDateObj && newdate.getTime() < minDateObj.getTime()) bad = true;
1225 $scope.outOfRange = bad;
1230 link : function(scope, elm, attrs) {
1231 if (!scope.closeText)
1232 scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
1234 if ('showTimePicker' in attrs)
1235 scope.showTimePicker = true;
1237 var default_format = 'mediumDate';
1238 egCore.org.settings(['format.date']).then(function(set) {
1239 default_format = set['format.date'];
1240 scope.date_format = (scope.dateFormat) ?
1250 * egFmValueSelector - widget for selecting a value from list specified
1253 .directive('egFmValueSelector', function() {
1261 // optional filter for refining the set of rows that
1262 // get returned. Example:
1264 // filter="{'column':{'=':null}}"
1267 // optional name of settings key for persisting
1268 // the last selected value
1269 stickySetting : '@',
1271 // optional OU setting for fetching default value;
1272 // used only if sticky setting not set
1276 templateUrl : './share/t_fm_value_selector',
1277 controller : ['$scope','egCore', function($scope , egCore) {
1279 $scope.org = egCore.org; // for use in the link function
1280 $scope.auth = egCore.auth; // for use in the link function
1281 $scope.hatch = egCore.hatch // for use in the link function
1283 function flatten_linked_values(cls, list) {
1285 var fields = egCore.idl.classes[cls].fields;
1288 angular.forEach(fields, function(fld) {
1289 if (fld.datatype == 'id') {
1290 id_field = fld.name;
1291 selector = fld.selector ? fld.selector : id_field;
1295 angular.forEach(list, function(item) {
1296 var rec = egCore.idl.toHash(item);
1299 name : rec[selector]
1306 search[egCore.idl.classes[$scope.idlClass].pkey] = {'!=' : null};
1307 if ($scope.filter) {
1308 angular.extend(search, $scope.filter);
1310 egCore.pcrud.search(
1311 $scope.idlClass, search, {}, {atomic : true}
1312 ).then(function(list) {
1313 $scope.linked_values = flatten_linked_values($scope.idlClass, list);
1316 $scope.handleChange = function(value) {
1317 if ($scope.stickySetting) {
1318 egCore.hatch.setLocalItem($scope.stickySetting, value);
1323 link : function(scope, element, attrs) {
1324 if (scope.stickySetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1325 var value = scope.hatch.getLocalItem(scope.stickySetting);
1326 scope.ngModel = value;
1328 if (scope.ouSetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1329 scope.org.settings([scope.ouSetting], scope.auth.user().ws_ou())
1330 .then(function(set) {
1331 var value = parseInt(set[scope.ouSetting]);
1333 scope.ngModel = value;
1341 * egShareDepthSelector - widget for selecting a share depth
1343 .directive('egShareDepthSelector', function() {
1351 templateUrl : './share/t_share_depth_selector',
1352 controller : ['$scope','egCore', function($scope , egCore) {
1354 egCore.pcrud.search('aout',
1355 { id : {'!=' : null} },
1356 { order_by : {aout : ['depth', 'name']} },
1358 ).then(function(list) {
1360 angular.forEach(list, function(aout) {
1361 var depth = parseInt(aout.depth());
1362 if (depth in scratch) {
1363 scratch[depth].push(aout.name());
1365 scratch[depth] = [ aout.name() ]
1368 scratch.forEach(function(val, idx) {
1369 $scope.values.push({ id : idx, name : scratch[idx].join(' / ') });
1377 * egHelpPopover - a helpful widget
1379 .directive('egHelpPopover', function() {
1387 templateUrl : './share/t_help_popover',
1388 controller : ['$scope','$sce', function($scope , $sce) {
1389 if ($scope.helpLink) {
1390 $scope.helpHtml = $sce.trustAsHtml(
1391 '<a target="_new" href="' + $scope.helpLink + '">' +
1392 $scope.helpText + '</a>'
1399 .factory('egWorkLog', ['egCore', function(egCore) {
1402 service.retrieve_all = function() {
1403 var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1404 var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1406 return { 'work_log' : workLog, 'patron_log' : patronLog };
1409 service.record = function(message,data) {
1412 if (typeof egCore != 'undefined') {
1413 if (typeof egCore.env != 'undefined') {
1414 if (typeof egCore.env.aous != 'undefined') {
1415 max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
1416 max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
1418 console.log('worklog: missing egCore.env.aous');
1421 console.log('worklog: missing egCore.env');
1424 console.log('worklog: missing egCore');
1427 if (typeof egCore.org != 'undefined') {
1428 if (typeof egCore.org.cachedSettings != 'undefined') {
1429 max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
1431 console.log('worklog: missing egCore.org.cachedSettings');
1434 console.log('worklog: missing egCore.org');
1438 if (typeof egCore.org != 'undefined') {
1439 if (typeof egCore.org.cachedSettings != 'undefined') {
1440 max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
1442 console.log('worklog: missing egCore.org.cachedSettings');
1445 console.log('worklog: missing egCore.org');
1450 console.log('worklog: defaulting to max_entries = ' + max_entries);
1454 console.log('worklog: defaulting to max_patrons = ' + max_patrons);
1457 var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1458 var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1460 'when' : new Date(),
1462 'action' : data.action,
1463 'actor' : egCore.auth.user().usrname()
1465 if (data.action == 'checkin') {
1466 entry['item'] = data.response.params.copy_barcode;
1467 entry['item_id'] = data.response.data.acp.id();
1468 if (data.response.data.au) {
1469 entry['user'] = data.response.data.au.family_name();
1470 entry['patron_id'] = data.response.data.au.id();
1473 if (data.action == 'checkout') {
1474 entry['item'] = data.response.params.copy_barcode;
1475 entry['user'] = data.response.data.au.family_name();
1476 entry['item_id'] = data.response.data.acp.id();
1477 entry['patron_id'] = data.response.data.au.id();
1479 if (data.action == 'noncat_checkout') {
1480 entry['user'] = data.response.data.au.family_name();
1481 entry['patron_id'] = data.response.data.au.id();
1483 if (data.action == 'renew') {
1484 entry['item'] = data.response.params.copy_barcode;
1485 entry['user'] = data.response.data.au.family_name();
1486 entry['item_id'] = data.response.data.acp.id();
1487 entry['patron_id'] = data.response.data.au.id();
1489 if (data.action == 'requested_hold'
1490 || data.action == 'edited_patron'
1491 || data.action == 'registered_patron'
1492 || data.action == 'paid_bill') {
1493 entry['patron_id'] = data.patron_id;
1495 if (data.action == 'requested_hold') {
1496 entry['hold_id'] = data.hold_id;
1498 if (data.action == 'paid_bill') {
1499 entry['amount'] = data.total_amount;
1502 workLog.push( entry );
1503 if (workLog.length > max_entries) workLog.shift();
1504 egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
1506 if (entry['patron_id']) {
1508 for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
1509 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
1512 if (temp.length > max_patrons) temp.shift();
1514 egCore.hatch.setLocalItem('eg.patron_log',patronLog);
1517 console.log('worklog',entry);