2 * UI tools and directives.
4 angular.module('egUiMod', ['egCoreMod', 'ui.bootstrap'])
8 * <input focus-me="iAmOpen"/>
9 * $scope.iAmOpen = true;
13 function($timeout , $parse) {
15 link: function(scope, element, attrs) {
16 var model = $parse(attrs.focusMe);
17 scope.$watch(model, function(value) {
19 $timeout(function() {element[0].focus()});
21 element.bind('blur', function() {
23 scope.$apply(model.assign(scope, false));
31 * <input blur-me="pleaseBlurMe"/>
32 * $scope.pleaseBlurMe = true
33 * Useful for de-focusing when no other obvious focus target exists
37 function($timeout , $parse) {
39 link: function(scope, element, attrs) {
40 var model = $parse(attrs.blurMe);
41 scope.$watch(model, function(value) {
43 $timeout(function() {element[0].blur()});
45 element.bind('focus', function() {
47 scope.$apply(model.assign(scope, false));
55 // <input select-me="iWantToBeSelected"/>
56 // $scope.iWantToBeSelected = true;
57 .directive('selectMe',
59 function($timeout , $parse) {
61 link: function(scope, element, attrs) {
62 var model = $parse(attrs.selectMe);
63 scope.$watch(model, function(value) {
65 $timeout(function() {element[0].select()});
67 element.bind('blur', function() {
69 scope.$apply(model.assign(scope, false));
78 // <div ng-repeat="item in items | reverse">{{item.name}}</div>
79 // http://stackoverflow.com/questions/15266671/angular-ng-repeat-in-reverse
80 // TODO: perhaps this should live elsewhere
81 .filter('reverse', function() {
82 return function(items) {
83 return items.slice().reverse();
88 // Overriding the core angular date filter with a moment-js based one for
89 // better timezone and formatting support.
90 .filter('date',function() {
107 var formatReplace = [
121 return function (date, format, tz) {
123 var fmt = formatMap[format] || format;
124 angular.forEach(formatReplace, function (r) {
125 fmt = fmt.replace(r[0],r[1]);
129 var d = moment(date);
130 if (tz && tz !== '-') d.tz(tz);
132 return d.isValid() ? d.format(fmt) : '';
137 // 'egOrgDate' filter
138 // Uses moment.js and moment-timezone.js to put dates into the most appropriate
139 // timezone for a given (optional) org unit based on its lib.timezone setting
140 .filter('egOrgDate',['$filter','egCore',
141 function($filter , egCore) {
145 function eg_date_filter (date, fmt, ouID) {
147 if (angular.isObject(ouID)) {
148 if (angular.isFunction(ouID.id)) {
155 if (!tzcache[ouID]) {
157 egCore.org.settings('lib.timezone', ouID)
159 tzcache[ouID] = s['lib.timezone'] || OpenSRF.tz;
164 return $filter('date')(date, fmt, tzcache[ouID]);
167 eg_date_filter.$stateful = true;
169 return eg_date_filter;
172 // 'egOrgDateInContext' filter
173 // Uses the egOrgDate filter to make time and date location aware, and further
174 // modifies the format if one of [short, medium, long, full] to show only the
175 // date if the optional interval parameter is day-granular. This is
176 // particularly useful for due dates on circulations.
177 .filter('egOrgDateInContext',['$filter','egCore',
178 function($filter , egCore) {
180 function eg_context_date_filter (date, format, orgID, interval) {
182 if (!fmt) fmt = 'shortDate';
184 // if this is a simple, one-word format, and it doesn't say "Date" in it...
185 if (['short','medium','long','full'].filter(function(x){return fmt == x}).length > 0 && interval) {
186 var secs = egCore.date.intervalToSeconds(interval);
187 if (secs !== null && secs % 86400 == 0) fmt += 'Date';
190 return $filter('egOrgDate')(date, fmt, orgID);
193 eg_context_date_filter.$stateful = true;
195 return eg_context_date_filter;
198 // 'egDueDate' filter
199 // Uses the egOrgDateInContext filter to make time and date location aware, but
200 // only if the supplied interval is day-granular. This is as wrapper for
201 // egOrgDateInContext to be used for circulation due date /only/.
202 .filter('egDueDate',['$filter','egCore',
203 function($filter , egCore) {
205 function eg_context_due_date_filter (date, format, orgID, interval) {
207 var secs = egCore.date.intervalToSeconds(interval);
208 if (secs === null || secs % 86400 != 0) {
213 return $filter('egOrgDateInContext')(date, format, orgID, interval);
216 eg_context_due_date_filter.$stateful = true;
218 return eg_context_due_date_filter;
224 * egProgressDialog.open();
225 * egProgressDialog.open({value : 0});
226 * egProgressDialog.open({value : 0, max : 123});
227 * egProgressDialog.increment();
228 * egProgressDialog.increment();
229 * egProgressDialog.close();
231 * Each dialog has 2 numbers, 'max' and 'value'.
232 * The content of these values determines how the dialog displays.
234 * There are 3 flavors:
236 * -- value is set, max is set
237 * determinate: shows a progression with a percent complete.
239 * -- value is set, max is unset
240 * semi-determinate, with a value report. Shows a value-less
241 * <progress/>, but shows the value as a number in the dialog.
243 * This is useful in cases where the total number of items to retrieve
244 * from the server is unknown, but we know how many items we've
245 * retrieved thus far. It helps to reinforce that something specific
246 * is happening, but we don't know when it will end.
249 * indeterminate: shows a generic value-less <progress/> with no
250 * clear indication of progress.
252 * Only 1 egProgressDialog instance will be activate at a time.
253 * Each invocation of .open() destroys any existing instance.
256 /* Simple storage class for egProgressDialog data maintenance.
257 * This data lives outside of egProgressDialog so it can be
258 * directly imported into egProgressDialog's $uibModalInstance.
260 .factory('egProgressData', [
262 var service = {}; // max/value initially unset
264 service.reset = function() {
266 delete service.value;
269 service.hasvalue = function() {
270 return Number.isInteger(service.value);
273 service.hasmax = function() {
274 return Number.isInteger(service.max);
277 service.percent = function() {
278 if (service.hasvalue() &&
281 service.value <= service.max)
282 return Math.floor((service.value / service.max) * 100);
290 .factory('egProgressDialog', [
291 'egProgressData','$uibModal',
292 function(egProgressData , $uibModal) {
295 service.open = function(args) {
296 service.close(); // force-kill existing instances.
298 // Reset to an indeterminate progress bar,
299 // overlay with caller values.
300 egProgressData.reset();
301 service.update(angular.extend({}, args));
303 return $uibModal.open({
304 templateUrl: './share/t_progress_dialog',
305 controller: ['$scope','$uibModalInstance','egProgressData',
306 function( $scope , $uibModalInstance , egProgressData) {
307 service.currentInstance = $uibModalInstance;
308 $scope.data = egProgressData; // tiny service
314 service.close = function() {
315 if (service.currentInstance) {
316 service.currentInstance.close();
317 delete service.currentInstance;
321 // Set the current state of the progress bar.
322 service.update = function(args) {
323 if (args.max != undefined)
324 egProgressData.max = args.max;
325 if (args.value != undefined)
326 egProgressData.value = args.value;
329 // Increment the current value. If no amount is specified,
330 // it increments by 1. Calling increment() on an indetermite
331 // progress bar will force it to be a (semi-)determinate bar.
332 service.increment = function(amt) {
333 if (!Number.isInteger(amt)) amt = 1;
335 if (!egProgressData.hasvalue())
336 egProgressData.value = 0;
338 egProgressData.value += amt;
345 * egAlertDialog.open({message : 'hello {{name}}'}).result.then(
346 * function() { console.log('alert closed') });
348 .factory('egAlertDialog',
350 ['$uibModal','$interpolate',
351 function($uibModal , $interpolate) {
354 service.open = function(message, msg_scope) {
355 return $uibModal.open({
356 templateUrl: './share/t_alert_dialog',
357 controller: ['$scope', '$uibModalInstance',
358 function($scope, $uibModalInstance) {
359 $scope.message = $interpolate(message)(msg_scope);
360 $scope.ok = function() {
361 if (msg_scope && msg_scope.ok) msg_scope.ok();
362 $uibModalInstance.close()
373 * egConfirmDialog.open("some message goes {{here}}", {
374 * here : 'foo', ok : function() {}, cancel : function() {}},
377 .factory('egConfirmDialog',
379 ['$uibModal','$interpolate',
380 function($uibModal, $interpolate) {
383 service.open = function(title, message, msg_scope, ok_button_label, cancel_button_label) {
384 return $uibModal.open({
385 templateUrl: './share/t_confirm_dialog',
386 controller: ['$scope', '$uibModalInstance',
387 function($scope, $uibModalInstance) {
388 $scope.title = $interpolate(title)(msg_scope);
389 $scope.message = $interpolate(message)(msg_scope);
390 $scope.ok_button_label = $interpolate(ok_button_label || '')(msg_scope);
391 $scope.cancel_button_label = $interpolate(cancel_button_label || '')(msg_scope);
392 $scope.ok = function() {
393 if (msg_scope.ok) msg_scope.ok();
394 $uibModalInstance.close()
396 $scope.cancel = function() {
397 if (msg_scope.cancel) msg_scope.cancel();
398 $uibModalInstance.dismiss();
409 * egPromptDialog.open(
410 * "prompt message goes {{here}}",
411 * promptValue, // optional
414 * ok : function(value) {console.log(value)},
415 * cancel : function() {console.log('prompt denied')}
419 .factory('egPromptDialog',
421 ['$uibModal','$interpolate',
422 function($uibModal, $interpolate) {
425 service.open = function(message, promptValue, msg_scope) {
426 return $uibModal.open({
427 templateUrl: './share/t_prompt_dialog',
428 controller: ['$scope', '$uibModalInstance',
429 function($scope, $uibModalInstance) {
430 $scope.message = $interpolate(message)(msg_scope);
431 $scope.args = {value : promptValue || ''};
433 $scope.ok = function() {
434 if (msg_scope.ok) msg_scope.ok($scope.args.value);
435 $uibModalInstance.close()
437 $scope.cancel = function() {
438 if (msg_scope.cancel) msg_scope.cancel();
439 $uibModalInstance.dismiss();
450 * egSelectDialog.open(
451 * "message goes {{here}}",
452 * list, // ['values','for','dropdown'],
453 * selectedValue, // optional
456 * ok : function(value) {console.log(value)},
457 * cancel : function() {console.log('prompt denied')}
461 .factory('egSelectDialog',
463 ['$uibModal','$interpolate',
464 function($uibModal, $interpolate) {
467 service.open = function(message, inputList, selectedValue, msg_scope) {
468 return $uibModal.open({
469 templateUrl: './share/t_select_dialog',
470 controller: ['$scope', '$uibModalInstance',
471 function($scope, $uibModalInstance) {
472 $scope.message = $interpolate(message)(msg_scope);
475 value : selectedValue
478 $scope.ok = function() {
479 if (msg_scope.ok) msg_scope.ok($scope.args.value);
480 $uibModalInstance.close()
482 $scope.cancel = function() {
483 if (msg_scope.cancel) msg_scope.cancel();
484 $uibModalInstance.dismiss();
495 * Warn on page unload and give the user a chance to avoid navigating
496 * away from the current page.
497 * Only one handler is supported per page.
498 * NOTE: we can't use an egUnloadDialog as the dialog builder, because
499 * it renders asynchronously, which allows the page to redirect before
500 * the dialog appears.
502 .factory('egUnloadPrompt', [
503 '$window','egStrings',
504 function($window , egStrings) {
505 var service = {attached : false};
507 // attach a page/scope unload prompt
508 service.attach = function($scope, msg) {
509 if (service.attached) return;
510 service.attached = true;
512 // handle page change
513 $($window).on('beforeunload', function() {
515 return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
520 // If a scope was provided, attach a scope-change handler,
521 // similar to the page-page prompt.
522 service.locChangeCancel =
523 $scope.$on('$locationChangeStart', function(evt, next, current) {
524 if (confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) {
525 // user allowed the page to change.
526 // Clear the unload handler.
529 evt.preventDefault();
534 // remove the page unload prompt
535 service.clear = function() {
536 $($window).off('beforeunload');
537 if (service.locChangeCancel)
538 service.locChangeCancel();
539 service.attached = false;
545 .directive('aDisabled', function() {
548 compile: function(tElement, tAttrs, transclude) {
550 tAttrs["ngClick"] = ("ng-click", "!("+tAttrs["aDisabled"]+") && ("+tAttrs["ngClick"]+")");
552 //Toggle "disabled" to class when aDisabled becomes true
553 return function (scope, iElement, iAttrs) {
554 scope.$watch(iAttrs["aDisabled"], function(newValue) {
555 if (newValue !== undefined) {
556 iElement.toggleClass("disabled", newValue);
560 //Disable href on click
561 iElement.on("click", function(e) {
562 if (scope.$eval(iAttrs["aDisabled"])) {
571 .directive('egBasicComboBox', function() {
576 list: "=", // list of strings
582 '<div class="input-group">'+
583 '<input type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()">'+
584 '<div class="input-group-btn" dropdown ng-class="{open:isopen}">'+
585 '<button type="button" ng-click="showAll()" class="btn btn-default dropdown-toggle"><span class="caret"></span></button>'+
586 '<ul class="dropdown-menu dropdown-menu-right">'+
587 '<li ng-repeat="item in list|filter:selected"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
588 '<li ng-if="complete_list" class="divider"><span></span></li>'+
589 '<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
593 controller: ['$scope','$filter',
594 function( $scope , $filter) {
596 $scope.complete_list = false;
597 $scope.isopen = false;
598 $scope.clickedopen = false;
599 $scope.clickedclosed = null;
601 $scope.showAll = function () {
603 $scope.clickedopen = !$scope.clickedopen;
605 if ($scope.clickedclosed === null) {
606 if (!$scope.clickedopen) {
607 $scope.clickedclosed = true;
610 $scope.clickedclosed = !$scope.clickedopen;
613 if ($scope.selected.length > 0) $scope.complete_list = true;
614 if ($scope.selected.length == 0) $scope.complete_list = false;
618 $scope.makeOpen = function () {
619 $scope.isopen = $scope.clickedopen || ($filter('filter')(
622 ).length > 0 && $scope.selected.length > 0);
623 if ($scope.clickedclosed) $scope.isopen = false;
626 $scope.changeValue = function (newVal) {
627 $scope.selected = newVal;
628 $scope.isopen = false;
629 $scope.clickedclosed = null;
630 $scope.clickedopen = false;
631 if ($scope.selected.length == 0) $scope.complete_list = false;
640 * Nested org unit selector modeled as a Bootstrap dropdown button.
642 .directive('egOrgSelector', function() {
646 replace : true, // makes styling easier
648 selected : '=', // defaults to workstation or root org,
649 // unless the nodefault attibute exists
651 // Each org unit is passed into this function and, for
652 // any org units where the response value is true, the
653 // org unit will not be added to the selector.
656 // Each org unit is passed into this function and, for
657 // any org units where the response value is true, the
658 // org unit will not be available for selection.
661 // if set to true, disable the UI element altogether
664 // Caller can either $watch(selected, ..) or register an
668 // optional primary drop-down button label
671 // optional name of settings key for persisting
672 // the last selected org unit
676 // any reason to move this into a TT2 template?
678 '<div class="btn-group eg-org-selector" uib-dropdown>'
679 + '<button type="button" class="btn btn-default" uib-dropdown-toggle ng-disabled="disable_button">'
680 + '<span style="padding-right: 5px;">{{getSelectedName()}}</span>'
681 + '<span class="caret"></span>'
683 + '<ul uib-dropdown-menu class="scrollable-menu">'
684 + '<li ng-repeat="org in orgList" ng-hide="hiddenTest(org.id)">'
685 + '<a href ng-click="orgChanged(org)" a-disabled="disableTest(org.id)" '
686 + 'style="padding-left: {{org.depth * 10 + 5}}px">'
687 + '{{org.shortname}}'
693 controller : ['$scope','$timeout','egCore','egStartup',
694 function($scope , $timeout , egCore , egStartup) {
696 if ($scope.alldisabled) {
697 $scope.disable_button = $scope.alldisabled == 'true' ? true : false;
699 $scope.disable_button = false;
702 // avoid linking the full fleshed tree to the scope by
703 // tossing in a flattened list.
705 // Run-time code referencing post-start data should be run
706 // from within a startup block, otherwise accessing this
707 // module before startup completes will lead to failure.
709 // controller() runs before link().
710 // This post-startup code runs after link().
711 egStartup.go().then(function() {
713 $scope.orgList = egCore.org.list().map(function(org) {
716 shortname : org.shortname(),
717 depth : org.ou_type().depth()
721 // Apply default values
723 if ($scope.stickySetting) {
724 var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
726 $scope.selected = egCore.org.get(orgId);
730 if (!$scope.selected && !$scope.nodefault) {
732 egCore.org.get(egCore.auth.user().ws_ou());
735 fire_orgsel_onchange(); // no-op if nothing is selected
739 * Fire onchange handler after a timeout, so the
740 * $scope.selected value has a chance to propagate to
741 * the page controllers before the onchange fires. This
742 * way, the caller does not have to manually capture the
743 * $scope.selected value during onchange.
745 function fire_orgsel_onchange() {
746 if (!$scope.selected || !$scope.onchange) return;
747 $timeout(function() {
749 'egOrgSelector onchange('+$scope.selected.id()+')');
750 $scope.onchange($scope.selected)
754 $scope.getSelectedName = function() {
755 if ($scope.selected && $scope.selected.shortname)
756 return $scope.selected.shortname();
760 $scope.orgChanged = function(org) {
761 $scope.selected = egCore.org.get(org.id);
762 if ($scope.stickySetting) {
763 egCore.hatch.setLocalItem($scope.stickySetting, org.id);
765 fire_orgsel_onchange();
769 link : function(scope, element, attrs, egGridCtrl) {
771 // boolean fields are presented as value-less attributes
775 if (angular.isDefined(attrs[field]))
778 scope[field] = false;
785 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
786 .directive('egEnter', function () {
787 return function (scope, element, attrs) {
788 element.bind("keydown keypress", function (event) {
789 if(event.which === 13) {
790 scope.$apply(function (){
791 scope.$eval(attrs.egEnter);
794 event.preventDefault();
801 * Handy wrapper directive for uib-datapicker-popup
804 'egDateInput', ['egStrings', 'egCore',
805 function(egStrings, egCore) {
814 hideDatePicker : '=',
818 templateUrl: './share/t_datetime',
820 link : function(scope, elm, attrs) {
821 if (!scope.closeText)
822 scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
824 if ('showTimePicker' in attrs)
825 scope.showTimePicker = true;
827 var default_format = 'mediumDate';
828 egCore.org.settings(['format.date']).then(function(set) {
829 default_format = set['format.date'];
830 scope.date_format = (scope.dateFormat) ?
840 * egFmValueSelector - widget for selecting a value from list specified
843 .directive('egFmValueSelector', function() {
851 // optional filter for refining the set of rows that
852 // get returned. Example:
854 // filter="{'column':{'=':null}}"
857 // optional name of settings key for persisting
858 // the last selected value
861 // optional OU setting for fetching default value;
862 // used only if sticky setting not set
866 templateUrl : './share/t_fm_value_selector',
867 controller : ['$scope','egCore', function($scope , egCore) {
869 $scope.org = egCore.org; // for use in the link function
870 $scope.auth = egCore.auth; // for use in the link function
871 $scope.hatch = egCore.hatch // for use in the link function
873 function flatten_linked_values(cls, list) {
875 var fields = egCore.idl.classes[cls].fields;
878 angular.forEach(fields, function(fld) {
879 if (fld.datatype == 'id') {
881 selector = fld.selector ? fld.selector : id_field;
885 angular.forEach(list, function(item) {
886 var rec = egCore.idl.toHash(item);
896 search[egCore.idl.classes[$scope.idlClass].pkey] = {'!=' : null};
898 angular.extend(search, $scope.filter);
901 $scope.idlClass, search, {}, {atomic : true}
902 ).then(function(list) {
903 $scope.linked_values = flatten_linked_values($scope.idlClass, list);
906 $scope.handleChange = function(value) {
907 if ($scope.stickySetting) {
908 egCore.hatch.setLocalItem($scope.stickySetting, value);
913 link : function(scope, element, attrs) {
914 if (scope.stickySetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
915 var value = scope.hatch.getLocalItem(scope.stickySetting);
916 scope.ngModel = value;
918 if (scope.ouSetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
919 scope.org.settings([scope.ouSetting], scope.auth.user().ws_ou())
920 .then(function(set) {
921 var value = parseInt(set[scope.ouSetting]);
923 scope.ngModel = value;
930 .factory('egWorkLog', ['egCore', function(egCore) {
933 service.retrieve_all = function() {
934 var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
935 var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
937 return { 'work_log' : workLog, 'patron_log' : patronLog };
940 service.record = function(message,data) {
943 if (typeof egCore != 'undefined') {
944 if (typeof egCore.env != 'undefined') {
945 if (typeof egCore.env.aous != 'undefined') {
946 max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
947 max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
949 console.log('worklog: missing egCore.env.aous');
952 console.log('worklog: missing egCore.env');
955 console.log('worklog: missing egCore');
958 if (typeof egCore.org != 'undefined') {
959 if (typeof egCore.org.cachedSettings != 'undefined') {
960 max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
962 console.log('worklog: missing egCore.org.cachedSettings');
965 console.log('worklog: missing egCore.org');
969 if (typeof egCore.org != 'undefined') {
970 if (typeof egCore.org.cachedSettings != 'undefined') {
971 max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
973 console.log('worklog: missing egCore.org.cachedSettings');
976 console.log('worklog: missing egCore.org');
981 console.log('worklog: defaulting to max_entries = ' + max_entries);
985 console.log('worklog: defaulting to max_patrons = ' + max_patrons);
988 var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
989 var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
993 'action' : data.action,
994 'actor' : egCore.auth.user().usrname()
996 if (data.action == 'checkin') {
997 entry['item'] = data.response.params.copy_barcode;
998 entry['item_id'] = data.response.data.acp.id();
999 if (data.response.data.au) {
1000 entry['user'] = data.response.data.au.family_name();
1001 entry['patron_id'] = data.response.data.au.id();
1004 if (data.action == 'checkout') {
1005 entry['item'] = data.response.params.copy_barcode;
1006 entry['user'] = data.response.data.au.family_name();
1007 entry['item_id'] = data.response.data.acp.id();
1008 entry['patron_id'] = data.response.data.au.id();
1010 if (data.action == 'noncat_checkout') {
1011 entry['user'] = data.response.data.au.family_name();
1012 entry['patron_id'] = data.response.data.au.id();
1014 if (data.action == 'renew') {
1015 entry['item'] = data.response.params.copy_barcode;
1016 entry['user'] = data.response.data.au.family_name();
1017 entry['item_id'] = data.response.data.acp.id();
1018 entry['patron_id'] = data.response.data.au.id();
1020 if (data.action == 'requested_hold'
1021 || data.action == 'edited_patron'
1022 || data.action == 'registered_patron'
1023 || data.action == 'paid_bill') {
1024 entry['patron_id'] = data.patron_id;
1026 if (data.action == 'requested_hold') {
1027 entry['hold_id'] = data.hold_id;
1029 if (data.action == 'paid_bill') {
1030 entry['amount'] = data.total_amount;
1033 workLog.push( entry );
1034 if (workLog.length > max_entries) workLog.shift();
1035 egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
1037 if (entry['patron_id']) {
1039 for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
1040 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
1043 if (temp.length > max_patrons) temp.shift();
1045 egCore.hatch.setLocalItem('eg.patron_log',patronLog);
1048 console.log('worklog',entry);