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 = 'short';
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;
415 if (args.label != undefined)
416 egProgressData.label = args.label;
419 // Increment the current value. If no amount is specified,
420 // it increments by 1. Calling increment() on an indetermite
421 // progress bar will force it to be a (semi-)determinate bar.
422 service.increment = function(amt) {
423 if (!Number.isInteger(amt)) amt = 1;
425 if (!egProgressData.hasvalue())
426 egProgressData.value = 0;
428 egProgressData.value += amt;
435 * egAlertDialog.open({message : 'hello {{name}}'}).result.then(
436 * function() { console.log('alert closed') });
438 .factory('egAlertDialog',
440 ['$uibModal','$interpolate',
441 function($uibModal , $interpolate) {
444 service.open = function(message, msg_scope) {
445 return $uibModal.open({
446 templateUrl: './share/t_alert_dialog',
448 controller: ['$scope', '$uibModalInstance',
449 function($scope, $uibModalInstance) {
450 $scope.message = $interpolate(message)(msg_scope);
451 $scope.ok = function() {
452 if (msg_scope && msg_scope.ok) msg_scope.ok();
453 $uibModalInstance.close()
464 * egConfirmDialog.open("some message goes {{here}}", {
465 * here : 'foo', ok : function() {}, cancel : function() {}},
468 .factory('egConfirmDialog',
470 ['$uibModal','$interpolate',
471 function($uibModal, $interpolate) {
474 service.open = function(title, message, msg_scope, ok_button_label, cancel_button_label) {
475 msg_scope = msg_scope || {};
476 return $uibModal.open({
477 templateUrl: './share/t_confirm_dialog',
479 controller: ['$scope', '$uibModalInstance',
480 function($scope, $uibModalInstance) {
481 $scope.title = $interpolate(title)(msg_scope);
482 $scope.message = $interpolate(message)(msg_scope);
483 $scope.ok_button_label = $interpolate(ok_button_label || '')(msg_scope);
484 $scope.cancel_button_label = $interpolate(cancel_button_label || '')(msg_scope);
485 $scope.ok = function() {
486 if (msg_scope.ok) msg_scope.ok();
487 $uibModalInstance.close()
489 $scope.cancel = function() {
490 if (msg_scope.cancel) msg_scope.cancel();
491 $uibModalInstance.dismiss();
502 * egPromptDialog.open(
503 * "prompt message goes {{here}}",
504 * promptValue, // optional
507 * ok : function(value) {console.log(value)},
508 * cancel : function() {console.log('prompt denied')}
512 .factory('egPromptDialog',
514 ['$uibModal','$interpolate',
515 function($uibModal, $interpolate) {
518 service.open = function(message, promptValue, msg_scope) {
519 return $uibModal.open({
520 templateUrl: './share/t_prompt_dialog',
522 controller: ['$scope', '$uibModalInstance',
523 function($scope, $uibModalInstance) {
524 $scope.message = $interpolate(message)(msg_scope);
525 $scope.args = {value : promptValue || ''};
527 $scope.ok = function() {
528 if (msg_scope && msg_scope.ok) msg_scope.ok($scope.args.value);
529 $uibModalInstance.close($scope.args);
531 $scope.cancel = function() {
532 if (msg_scope && msg_scope.cancel) msg_scope.cancel();
533 $uibModalInstance.dismiss();
544 * egSelectDialog.open(
545 * "message goes {{here}}",
546 * list, // ['values','for','dropdown'],
547 * selectedValue, // optional
550 * ok : function(value) {console.log(value)},
551 * cancel : function() {console.log('prompt denied')}
555 .factory('egSelectDialog',
557 ['$uibModal','$interpolate',
558 function($uibModal, $interpolate) {
561 service.open = function(message, inputList, selectedValue, msg_scope) {
562 return $uibModal.open({
563 templateUrl: './share/t_select_dialog',
565 controller: ['$scope', '$uibModalInstance',
566 function($scope, $uibModalInstance) {
567 $scope.message = $interpolate(message)(msg_scope);
570 value : selectedValue
573 $scope.ok = function() {
574 if (msg_scope.ok) msg_scope.ok($scope.args.value);
575 $uibModalInstance.close()
577 $scope.cancel = function() {
578 if (msg_scope.cancel) msg_scope.cancel();
579 $uibModalInstance.dismiss();
590 * Warn on page unload and give the user a chance to avoid navigating
591 * away from the current page.
592 * Only one handler is supported per page.
593 * NOTE: we can't use an egUnloadDialog as the dialog builder, because
594 * it renders asynchronously, which allows the page to redirect before
595 * the dialog appears.
597 .factory('egUnloadPrompt', [
598 '$window','egStrings',
599 function($window , egStrings) {
600 var service = {attached : false};
602 // attach a page/scope unload prompt
603 service.attach = function($scope, msg) {
604 if (service.attached) return;
605 service.attached = true;
607 // handle page change
608 $($window).on('beforeunload', function() {
610 return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
615 // If a scope was provided, attach a scope-change handler,
616 // similar to the page-page prompt.
617 service.locChangeCancel =
618 $scope.$on('$locationChangeStart', function(evt, next, current) {
619 if (confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) {
620 // user allowed the page to change.
621 // Clear the unload handler.
624 evt.preventDefault();
629 // remove the page unload prompt
630 service.clear = function() {
631 $($window).off('beforeunload');
632 if (service.locChangeCancel)
633 service.locChangeCancel();
634 service.attached = false;
641 * egAddCopyAlertDialog - manage copy alerts
643 .factory('egAddCopyAlertDialog',
644 ['$uibModal','$interpolate','egCore',
645 function($uibModal , $interpolate , egCore) {
648 service.open = function(args) {
649 return $uibModal.open({
650 templateUrl: './share/t_add_copy_alert_dialog',
651 controller: ['$scope','$q','$uibModalInstance',
652 function( $scope , $q , $uibModalInstance) {
654 $scope.copy_ids = args.copy_ids;
655 egCore.pcrud.search('ccat',
659 ).then(function (ccat) {
660 $scope.alert_types = ccat;
663 $scope.copy_alert = {
664 create_staff : egCore.auth.user().id(),
669 $scope.ok = function(copy_alert) {
670 if (typeof(copy_alert.note) != 'undefined' &&
671 copy_alert.note != '') {
673 angular.forEach($scope.copy_ids, function (cp_id) {
674 var a = new egCore.idl.aca();
676 a.create_staff(copy_alert.create_staff);
677 a.note(copy_alert.note);
678 a.temp(copy_alert.temp ? 't' : 'f');
682 $scope.alert_types.filter(function(at) {
683 return at.id() == copy_alert.alert_type;
686 copy_alerts.push( a );
688 if (copy_alerts.length > 0) {
689 egCore.pcrud.apply(copy_alerts).finally(function() {
690 if (args.ok) args.ok();
691 $uibModalInstance.close()
695 if (args.ok) args.ok();
696 $uibModalInstance.close()
699 $scope.cancel = function() {
700 if (args.cancel) args.cancel();
701 $uibModalInstance.dismiss();
712 * egCopyAlertManagerDialog - manage copy alerts
714 .factory('egCopyAlertManagerDialog',
715 ['$uibModal','$interpolate','egCore',
716 function($uibModal , $interpolate , egCore) {
719 service.get_user_copy_alerts = function(copy_id) {
720 return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
721 { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
726 service.open = function(args) {
727 return $uibModal.open({
728 templateUrl: './share/t_copy_alert_manager_dialog',
729 controller: ['$scope','$q','$uibModalInstance',
730 function( $scope , $q , $uibModalInstance) {
732 function init(args) {
733 var defer = $q.defer();
735 service.get_user_copy_alerts(args.copy_id).then(function(aca) {
739 defer.resolve(args.alerts);
741 return defer.promise;
744 // returns a promise resolved with the list of circ statuses
745 $scope.get_copy_statuses = function() {
747 return $q.when(egCore.env.ccs.list);
749 return egCore.pcrud.retrieveAll('ccs', null, {atomic : true})
750 .then(function(list) {
751 egCore.env.absorbList(list, 'ccs');
756 $scope.mode = args.mode || 'checkin';
758 var next_statuses = [];
759 var seen_statuses = {};
760 $scope.next_statuses = [];
762 'the_next_status' : null
764 init(args).then(function(copy_alerts) {
765 $scope.alerts = copy_alerts;
766 angular.forEach($scope.alerts, function(copy_alert) {
767 var state = copy_alert.alert_type().state();
768 copy_alert.evt = copy_alert.alert_type().event();
770 copy_alert.message = copy_alert.note() ||
771 egCore.strings.ON_DEMAND_COPY_ALERT[copy_alert.evt][state];
773 if (copy_alert.temp() == 't') {
774 angular.forEach(copy_alert.alert_type().next_status(), function (st) {
775 if (!seen_statuses[st]) {
776 seen_statuses[st] = true;
777 next_statuses.push(st);
782 if ($scope.mode == 'checkin' && next_statuses.length > 0) {
783 $scope.get_copy_statuses().then(function() {
784 angular.forEach(next_statuses, function(st) {
785 if (egCore.env.ccs.map[st])
786 $scope.next_statuses.push(egCore.env.ccs.map[st]);
788 $scope.params.the_next_status = $scope.next_statuses[0].id();
793 $scope.isAcknowledged = function(copy_alert) {
794 return (copy_alert.acked);
796 $scope.canBeAcknowledged = function(copy_alert) {
797 return (!copy_alert.ack_time() && copy_alert.temp() == 't');
799 $scope.canBeRemoved = function(copy_alert) {
800 return (!copy_alert.ack_time() && copy_alert.temp() == 'f');
803 $scope.ok = function() {
805 angular.forEach($scope.alerts, function (copy_alert) {
806 if (copy_alert.acked) {
807 copy_alert.ack_time('now');
808 copy_alert.ack_staff(egCore.auth.user().id());
809 copy_alert.ischanged(true);
810 acks.push(copy_alert);
813 if (acks.length > 0) {
814 egCore.pcrud.apply(acks).finally(function() {
815 if (args.ok) args.ok($scope.params.the_next_status);
816 $uibModalInstance.close()
819 if (args.ok) args.ok($scope.params.the_next_status);
820 $uibModalInstance.close()
823 $scope.cancel = function() {
824 if (args.cancel) args.cancel();
825 $uibModalInstance.dismiss();
836 * egCopyAlertEditorDialog - manage copy alerts
838 .factory('egCopyAlertEditorDialog',
839 ['$uibModal','$interpolate','egCore',
840 function($uibModal , $interpolate , egCore) {
843 service.get_user_copy_alerts = function(copy_id) {
844 return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
845 { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
850 service.get_copy_alert_types = function() {
851 return egCore.pcrud.search('ccat',
858 service.open = function(args) {
859 return $uibModal.open({
860 templateUrl: './share/t_copy_alert_editor_dialog',
861 controller: ['$scope','$q','$uibModalInstance',
862 function( $scope , $q , $uibModalInstance) {
864 function init(args) {
865 var defer = $q.defer();
867 service.get_user_copy_alerts(args.copy_id).then(function(aca) {
871 defer.resolve(args.alerts);
873 return defer.promise;
876 init(args).then(function(copy_alerts) {
877 $scope.copy_alert_list = copy_alerts;
879 service.get_copy_alert_types().then(function(ccat) {
880 $scope.alert_types = ccat;
883 $scope.ok = function() {
884 egCore.pcrud.apply($scope.copy_alert_list).finally(function() {
885 $uibModalInstance.close();
888 $scope.cancel = function() {
889 if (args.cancel) args.cancel();
890 $uibModalInstance.dismiss();
899 .directive('aDisabled', function() {
902 compile: function(tElement, tAttrs, transclude) {
904 tAttrs["ngClick"] = ("ng-click", "!("+tAttrs["aDisabled"]+") && ("+tAttrs["ngClick"]+")");
906 //Toggle "disabled" to class when aDisabled becomes true
907 return function (scope, iElement, iAttrs) {
908 scope.$watch(iAttrs["aDisabled"], function(newValue) {
909 if (newValue !== undefined) {
910 iElement.toggleClass("disabled", newValue);
914 //Disable href on click
915 iElement.on("click", function(e) {
916 if (scope.$eval(iAttrs["aDisabled"])) {
925 .directive('egBasicComboBox', function() {
930 list: "=", // list of strings
939 '<div class="input-group">'+
940 '<input placeholder="{{placeholder}}" type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()" focus-me="focusMe" ng-click="inputClick()">'+
941 '<div class="input-group-btn" uib-dropdown ng-class="{open:isopen}">'+
942 '<button type="button" ng-click="showAll()" ng-disabled="egDisabled" class="btn btn-default" uib-dropdown-toggle><span class="caret"></span></button>'+
943 '<ul uib-dropdown-menu class="dropdown-menu-right">'+
944 '<li ng-repeat="item in list|filter:selected:compare"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
945 '<li ng-if="complete_list" class="divider"><span></span></li>'+
946 '<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
950 controller: ['$scope','$filter',
951 function( $scope , $filter) {
953 $scope.complete_list = false;
954 $scope.isopen = false;
955 $scope.clickedopen = false;
956 $scope.clickedclosed = null;
958 $scope.compare = function (ex, act) {
959 if (act === null || act === undefined) return true;
961 act = act.toString();
962 act = act.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); // modify string to make sure characters like [ are accepted in the regex
964 return new RegExp(act.toLowerCase()).test(ex.toLowerCase());
967 $scope.showAll = function () {
969 $scope.clickedopen = !$scope.clickedopen;
971 if ($scope.clickedclosed === null) {
972 if (!$scope.clickedopen) {
973 $scope.clickedclosed = true;
976 $scope.clickedclosed = !$scope.clickedopen;
979 if ($scope.selected && $scope.selected.length > 0) $scope.complete_list = true;
980 if (!$scope.selected || $scope.selected.length == 0) $scope.complete_list = false;
984 $scope.inputClick = function() {
986 $scope.isopen = false;
987 $scope.clickedclosed = null;
991 $scope.makeOpen = function () {
992 $scope.isopen = $scope.clickedopen || ($filter('filter')(
995 ).length > 0 && $scope.selected && $scope.selected.length > 0);
996 if ($scope.clickedclosed) {
997 $scope.isopen = false;
998 $scope.clickedclosed = null;
1002 $scope.changeValue = function (newVal) {
1003 $scope.selected = newVal;
1004 $scope.isopen = false;
1005 $scope.clickedclosed = null;
1006 $scope.clickedopen = false;
1007 if ($scope.selected.length == 0) $scope.complete_list = false;
1008 if ($scope.onSelect) $scope.onSelect();
1016 .directive('egListCounts', function() {
1022 list: "=", // list of things
1023 render: "=", // function to turn thing into string; default to stringification
1024 onSelect: "=" // function to fire when option selected. passed one copy of the selected value
1026 templateUrl: './share/t_listcounts',
1027 controller: ['$scope','$timeout',
1028 function( $scope , $timeout ) {
1030 $scope.isopen = false;
1031 $scope.count_hash = {};
1033 $scope.renderer = $scope.render ? $scope.render : function (x) { return ""+x };
1035 $scope.$watchCollection('list',function() {
1036 $scope.count_hash = {};
1037 angular.forEach($scope.list, function (item) {
1038 var str = $scope.renderer(item);
1039 if (!$scope.count_hash[str]) {
1040 $scope.count_hash[str] = {
1046 $scope.count_hash[str].count++;
1051 $scope.selectValue = function (item) {
1052 if ($scope.onSelect) $scope.onSelect(item);
1061 * Nested org unit selector modeled as a Bootstrap dropdown button.
1063 .directive('egOrgSelector', function() {
1067 replace : true, // makes styling easier
1069 selected : '=', // defaults to workstation or root org,
1070 // unless the nodefault attibute exists
1072 // Each org unit is passed into this function and, for
1073 // any org units where the response value is true, the
1074 // org unit will not be added to the selector.
1077 // Each org unit is passed into this function and, for
1078 // any org units where the response value is true, the
1079 // org unit will not be available for selection.
1082 // if set to true, disable the UI element altogether
1085 // Caller can either $watch(selected, ..) or register an
1086 // onchange handler.
1089 // optional typeahead placeholder text
1092 // optional name of settings key for persisting
1093 // the last selected org unit
1097 templateUrl : './share/t_org_select',
1099 controller : ['$scope','$timeout','egCore','egStartup','$q',
1100 function($scope , $timeout , egCore , egStartup , $q) {
1102 // See emptyTypeahead directive below.
1103 var secretEmptyKey = '_INTERNAL_';
1105 function formatName(org) {
1106 return " ".repeat(org.ou_type().depth()) + org.shortname();
1109 // avoid linking the full fleshed tree to the scope by
1110 // tossing in a flattened list.
1112 // Run-time code referencing post-start data should be run
1113 // from within a startup block, otherwise accessing this
1114 // module before startup completes will lead to failure.
1116 // controller() runs before link().
1117 // This post-startup code runs after link().
1121 return egCore.env.classLoaders.aou();
1126 $scope.selecteName = '';
1128 $scope.shortNames = egCore.org.list()
1129 .filter(function(org) {
1131 $scope.hiddenTest &&
1132 $scope.hiddenTest(org.id())
1134 }).map(function(org) {
1135 return formatName(org);
1138 // Apply default values
1140 if ($scope.stickySetting) {
1141 var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
1143 var org = egCore.org.get(orgId);
1145 $scope.selected = org;
1146 $scope.selectedName = org.shortname();
1151 if (!$scope.selected && !$scope.nodefault && egCore.auth.user()) {
1152 var org = egCore.org.get(egCore.auth.user().ws_ou());
1153 $scope.selected = org;
1154 $scope.selectedName = org.shortname();
1157 fire_orgsel_onchange(); // no-op if nothing is selected
1158 watch_external_changes();
1163 * Fire onchange handler after a timeout, so the
1164 * $scope.selected value has a chance to propagate to
1165 * the page controllers before the onchange fires. This
1166 * way, the caller does not have to manually capture the
1167 * $scope.selected value during onchange.
1169 function fire_orgsel_onchange() {
1170 if (!$scope.selected || !$scope.onchange) return;
1171 $timeout(function() {
1173 'egOrgSelector onchange('+$scope.selected.id()+')');
1174 $scope.onchange($scope.selected)
1178 // Force the compare filter to run when the input is
1179 // clicked. This allows for displaying all values when
1180 // clicking on an empty input.
1181 $scope.handleClick = function (e) {
1182 $timeout(function () {
1183 var current = $scope.selectedName;
1185 // Force the input value to "" so when the compare
1186 // function runs it will see the special empty key
1187 // instead of the selected value.
1188 $(e.target).val('');
1189 $(e.target).trigger('input');
1190 // After the compare function runs, reset the the
1192 $scope.selectedName = current;
1196 $scope.compare = function(shortName, inputValue) {
1197 return inputValue === secretEmptyKey ||
1198 (shortName || '').toLowerCase().trim()
1199 .indexOf((inputValue || '').toLowerCase().trim()) > -1;
1202 // Trim leading tree-spaces before displaying selected value
1203 $scope.formatDisplayName = function(shortName) {
1204 return ($scope.selectedName || '').trim();
1207 $scope.orgIsDisabled = function(shortName) {
1208 if ($scope.alldisabled === 'true') return true;
1209 if (shortName && $scope.disableTest) {
1210 var org = egCore.org.list().filter(function(org) {
1211 return org.shortname() === shortName.trim();
1214 return org && $scope.disableTest(org.id());
1219 $scope.inputChanged = function(shortName) {
1220 // Avoid watching for changes on $scope.selected while
1221 // manually applying values below.
1222 unwatch_external_changes();
1224 // Manually prevent selection of disabled orgs
1225 if ($scope.selectedName &&
1226 !$scope.orgIsDisabled($scope.selectedName)) {
1227 $scope.selected = egCore.org.list().filter(function(org) {
1228 return org.shortname() === $scope.selectedName.trim()
1231 $scope.selected = null;
1233 if ($scope.selected && $scope.stickySetting) {
1234 egCore.hatch.setLocalItem(
1235 $scope.stickySetting, $scope.selected.id());
1238 fire_orgsel_onchange();
1239 $timeout(watch_external_changes);
1242 // Propagate external changes on $scope.selected to the typeahead
1244 function watch_external_changes() {
1245 dewatcher = $scope.$watch('selected', function(newVal, oldVal) {
1247 $scope.selectedName = newVal.shortname();
1249 $scope.selectedName = '';
1254 function unwatch_external_changes() {
1262 link : function(scope, element, attrs, egGridCtrl) {
1264 // boolean fields are presented as value-less attributes
1268 if (angular.isDefined(attrs[field]))
1269 scope[field] = true;
1271 scope[field] = false;
1279 https://stackoverflow.com/questions/24764802/angular-js-automatically-focus-input-and-show-typeahead-dropdown-ui-bootstra
1281 .directive('emptyTypeahead', function () {
1284 link: function(scope, element, attrs, modelCtrl) {
1286 var secretEmptyKey = '_INTERNAL_';
1288 // this parser run before typeahead's parser
1289 modelCtrl.$parsers.unshift(function (inputValue) {
1290 // replace empty string with secretEmptyKey to bypass typeahead-min-length check
1291 var value = (inputValue ? inputValue : secretEmptyKey);
1292 // this $viewValue must match the inputValue pass to typehead directive
1293 modelCtrl.$viewValue = value;
1297 // this parser run after typeahead's parser
1298 modelCtrl.$parsers.push(function (inputValue) {
1299 // set the secretEmptyKey back to empty string
1300 return inputValue === secretEmptyKey ? '' : inputValue;
1306 .directive('nextOnEnter', function () {
1307 return function (scope, element, attrs) {
1308 element.bind("keydown keypress", function (event) {
1309 if(event.which === 13) {
1310 $('#'+attrs.nextOnEnter).focus();
1311 event.preventDefault();
1317 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
1318 .directive('egEnter', function () {
1319 return function (scope, element, attrs) {
1320 element.bind("keydown keypress", function (event) {
1321 if(event.which === 13) {
1322 scope.$apply(function (){
1323 scope.$eval(attrs.egEnter);
1326 event.preventDefault();
1333 * Handy wrapper directive for uib-datapicker-popup
1336 'egDateInput', ['egStrings', 'egCore',
1337 function(egStrings, egCore) {
1349 hideDatePicker : '=',
1350 hideTimePicker : '=?',
1356 templateUrl: './share/t_datetime',
1358 controller : ['$scope', function($scope) {
1360 minDate : $scope.minDate,
1361 maxDate : $scope.maxDate
1364 var maxDateObj = $scope.maxDate ? new Date($scope.maxDate) : null;
1365 var minDateObj = $scope.minDate ? new Date($scope.minDate) : null;
1367 if ($scope.outOfRange !== undefined && (maxDateObj || minDateObj)) {
1368 $scope.$watch('ngModel', function (n,o) {
1370 var newdate = new Date(n);
1371 if (isNaN(newdate.getTime())) bad = true;
1372 if (maxDateObj && newdate.getTime() > maxDateObj.getTime()) bad = true;
1373 if (minDateObj && newdate.getTime() < minDateObj.getTime()) bad = true;
1374 $scope.outOfRange = bad;
1378 link : function(scope, elm, attrs) {
1379 if (!scope.closeText)
1380 scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
1382 if ('showTimePicker' in attrs)
1383 scope.showTimePicker = true;
1385 var default_format = 'mediumDate';
1386 egCore.org.settings(['format.date']).then(function(set) {
1387 if (set) default_format = set['format.date'];
1388 scope.date_format = (scope.dateFormat) ?
1398 * egFmValueSelector - widget for selecting a value from list specified
1401 .directive('egFmValueSelector', function() {
1409 // optional filter for refining the set of rows that
1410 // get returned. Example:
1412 // filter="{'column':{'=':null}}"
1415 // optional name of settings key for persisting
1416 // the last selected value
1417 stickySetting : '@',
1419 // optional OU setting for fetching default value;
1420 // used only if sticky setting not set
1424 templateUrl : './share/t_fm_value_selector',
1425 controller : ['$scope','egCore', function($scope , egCore) {
1427 $scope.org = egCore.org; // for use in the link function
1428 $scope.auth = egCore.auth; // for use in the link function
1429 $scope.hatch = egCore.hatch // for use in the link function
1431 function flatten_linked_values(cls, list) {
1433 var fields = egCore.idl.classes[cls].fields;
1436 angular.forEach(fields, function(fld) {
1437 if (fld.datatype == 'id') {
1438 id_field = fld.name;
1439 selector = fld.selector ? fld.selector : id_field;
1443 angular.forEach(list, function(item) {
1444 var rec = egCore.idl.toHash(item);
1447 name : rec[selector]
1454 search[egCore.idl.classes[$scope.idlClass].pkey] = {'!=' : null};
1455 if ($scope.filter) {
1456 angular.extend(search, $scope.filter);
1458 egCore.pcrud.search(
1459 $scope.idlClass, search, {}, {atomic : true}
1460 ).then(function(list) {
1461 $scope.linked_values = flatten_linked_values($scope.idlClass, list);
1464 $scope.handleChange = function(value) {
1465 if ($scope.stickySetting) {
1466 egCore.hatch.setLocalItem($scope.stickySetting, value);
1471 link : function(scope, element, attrs) {
1472 if (scope.stickySetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1473 var value = scope.hatch.getLocalItem(scope.stickySetting);
1474 scope.ngModel = value;
1476 if (scope.ouSetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1477 scope.org.settings([scope.ouSetting], scope.auth.user().ws_ou())
1478 .then(function(set) {
1479 var value = parseInt(set[scope.ouSetting]);
1481 scope.ngModel = value;
1489 * egShareDepthSelector - widget for selecting a share depth
1491 .directive('egShareDepthSelector', function() {
1501 templateUrl : './share/t_share_depth_selector',
1502 controller : ['$scope','egCore', function($scope , egCore) {
1504 egCore.pcrud.search('aout',
1505 { id : {'!=' : null} },
1506 { order_by : {aout : ['depth', 'name']} },
1508 ).then(function(list) {
1510 angular.forEach(list, function(aout) {
1511 var depth = parseInt(aout.depth());
1512 if (typeof $scope.maxDepth == 'undefined' || depth <= $scope.maxDepth) {
1513 var text = $scope.useOpacLabel ? aout.opac_label() : aout.name();
1514 if (depth in scratch) {
1515 scratch[depth].push(text);
1517 scratch[depth] = [ text ]
1521 scratch.forEach(function(val, idx) {
1522 $scope.values.push({ id : idx, name : scratch[idx].join(' / ') });
1526 link : function(scope, elm, attrs) {
1527 if ('useOpacLabel' in attrs)
1528 scope.useOpacLabel = true;
1529 if ('maxDepth' in attrs) // I feel like I'm doing this wrong :)
1530 scope.maxDepth = parseInt(attrs.maxdepth);
1536 * egHelpPopover - a helpful widget
1538 .directive('egHelpPopover', function() {
1546 templateUrl : './share/t_help_popover',
1547 controller : ['$scope','$sce', function($scope , $sce) {
1548 if ($scope.helpLink) {
1549 $scope.helpHtml = $sce.trustAsHtml(
1550 '<a target="_new" href="' + $scope.helpLink + '">' +
1551 $scope.helpText + '</a>'
1558 .factory('egWorkLog', ['egCore', function(egCore) {
1561 service.retrieve_all = function() {
1562 var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1563 var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1565 return { 'work_log' : workLog, 'patron_log' : patronLog };
1568 service.record = function(message,data) {
1571 if (typeof egCore != 'undefined') {
1572 if (typeof egCore.env != 'undefined') {
1573 if (typeof egCore.env.aous != 'undefined') {
1574 max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
1575 max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
1577 console.log('worklog: missing egCore.env.aous');
1580 console.log('worklog: missing egCore.env');
1583 console.log('worklog: missing egCore');
1586 if (typeof egCore.org != 'undefined') {
1587 if (typeof egCore.org.cachedSettings != 'undefined') {
1588 max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
1590 console.log('worklog: missing egCore.org.cachedSettings');
1593 console.log('worklog: missing egCore.org');
1597 if (typeof egCore.org != 'undefined') {
1598 if (typeof egCore.org.cachedSettings != 'undefined') {
1599 max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
1601 console.log('worklog: missing egCore.org.cachedSettings');
1604 console.log('worklog: missing egCore.org');
1609 console.log('worklog: defaulting to max_entries = ' + max_entries);
1613 console.log('worklog: defaulting to max_patrons = ' + max_patrons);
1616 var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1617 var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1619 'when' : new Date(),
1621 'action' : data.action,
1622 'actor' : egCore.auth.user().usrname()
1624 if (data.action == 'checkin') {
1625 entry['item'] = data.response.params.copy_barcode;
1626 entry['item_id'] = data.response.data.acp.id();
1627 if (data.response.data.au) {
1628 entry['user'] = data.response.data.au.family_name();
1629 entry['patron_id'] = data.response.data.au.id();
1632 if (data.action == 'checkout') {
1633 entry['item'] = data.response.params.copy_barcode;
1634 entry['user'] = data.response.data.au.family_name();
1635 entry['item_id'] = data.response.data.acp.id();
1636 entry['patron_id'] = data.response.data.au.id();
1638 if (data.action == 'noncat_checkout') {
1639 entry['user'] = data.response.data.au.family_name();
1640 entry['patron_id'] = data.response.data.au.id();
1642 if (data.action == 'renew') {
1643 entry['item'] = data.response.params.copy_barcode;
1644 entry['user'] = data.response.data.au.family_name();
1645 entry['item_id'] = data.response.data.acp.id();
1646 entry['patron_id'] = data.response.data.au.id();
1648 if (data.action == 'requested_hold'
1649 || data.action == 'edited_patron'
1650 || data.action == 'registered_patron'
1651 || data.action == 'paid_bill') {
1652 entry['patron_id'] = data.patron_id;
1654 if (data.action == 'requested_hold') {
1655 entry['hold_id'] = data.hold_id;
1657 if (data.action == 'paid_bill') {
1658 entry['amount'] = data.total_amount;
1661 workLog.push( entry );
1662 if (workLog.length > max_entries) workLog.shift();
1663 egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
1665 if (entry['patron_id']) {
1667 for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
1668 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
1671 if (temp.length > max_patrons) temp.shift();
1673 egCore.hatch.setLocalItem('eg.patron_log',patronLog);
1676 console.log('worklog',entry);