LP#1803406 Due date box in check out has display issues at wider resolutions
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / cat / printlabels / app.js
1 /**
2  * Vol/Copy Editor
3  */
4
5 angular.module('egPrintLabels',
6     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
7
8 .config(function ($routeProvider, $locationProvider, $compileProvider) {
9     $locationProvider.html5Mode(true);
10     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
11
12     var resolver = {
13         delay: ['egStartup', function (egStartup) { return egStartup.go(); }]
14     };
15
16     $routeProvider.when('/cat/printlabels/:dataKey', {
17         templateUrl: './cat/printlabels/t_view',
18         controller: 'LabelCtrl',
19         resolve: resolver
20     });
21
22 })
23
24 .factory('itemSvc',
25        ['egCore',
26 function (egCore) {
27
28     var service = {
29         copies: [], // copy barcode search results
30         index: 0 // search grid index
31     };
32
33     service.flesh = {
34         flesh: 3,
35         flesh_fields: {
36             acp: ['call_number', 'location', 'status', 'location', 'floating', 'circ_modifier', 'age_protect', 'circ_lib'],
37             acn: ['record', 'prefix', 'suffix', 'owning_lib'],
38             bre: ['simple_record', 'creator', 'editor']
39         },
40         select: {
41             // avoid fleshing MARC on the bre
42             // note: don't add simple_record.. not sure why
43             bre: ['id', 'tcn_value', 'creator', 'editor'],
44         }
45     }
46
47     // resolved with the last received copy
48     service.fetch = function (barcode, id, noListDupes) {
49         var promise;
50
51         if (barcode) {
52             promise = egCore.pcrud.search('acp',
53                 { barcode: barcode, deleted: 'f' }, service.flesh);
54         } else {
55             promise = egCore.pcrud.retrieve('acp', id, service.flesh);
56         }
57
58         var lastRes;
59         return promise.then(
60             function () { return lastRes },
61             null, // error
62
63             // notify reads the stream of copies, one at a time.
64             function (copy) {
65
66                 var flatCopy;
67                 if (noListDupes) {
68                     // use the existing copy if possible
69                     flatCopy = service.copies.filter(
70                         function (c) { return c.id == copy.id() })[0];
71                 }
72
73                 if (!flatCopy) {
74                     flatCopy = egCore.idl.toHash(copy, true);
75                     flatCopy.index = service.index++;
76                     service.copies.unshift(flatCopy);
77                 }
78
79                 return lastRes = {
80                     copy: copy,
81                     index: flatCopy.index
82                 }
83             }
84         );
85     }
86
87     return service;
88 }])
89
90 /**
91  * Label controller!
92  */
93 .controller('LabelCtrl',
94        ['$scope', '$q', '$window', '$routeParams', '$location', '$timeout', 'egCore', 'egNet', 'ngToast', 'itemSvc', 'labelOutputRowsFilter',
95 function ($scope, $q, $window, $routeParams, $location, $timeout, egCore, egNet, ngToast, itemSvc, labelOutputRowsFilter) {
96
97     var dataKey = $routeParams.dataKey;
98     console.debug('dataKey: ' + dataKey);
99
100     $scope.print = {
101         template_name: 'item_label',
102         template_output: '',
103         template_context: 'default'
104     };
105
106     var toolbox_settings = {
107         feed_option: {
108             options: [
109                 { label: "Continuous", value: "continuous" },
110                 { label: "Sheet", value: "sheet" },
111             ],
112             selected: "continuous"
113         },
114         label_set: {
115             margin_between: 0,
116             size: 1
117         },
118         mode: {
119             options: [
120                 { label: "Spine Label", value: "spine-only" },
121                 { label: "Pocket Label", value: "spine-pocket" }
122             ],
123             selected: "spine-pocket"
124         },
125         page: {
126             column_class: ["spine", "pocket"],
127             dimensions: {
128                 columns: 2,
129                 rows: 1
130             },
131             label: {
132                 gap: {
133                     size: 0
134                 },
135                 set: {
136                     size: 2
137                 }
138             },
139             margins: {
140                 top: { size: 0, label: "Top" },
141                 left: { size: 0, label: "Left" },
142             },
143             space_between_labels: {
144                 horizontal: { size: 0, label: "Horizontal" },
145                 vertical: { size: 0, label: "Vertical" }
146             },
147             start_position: {
148                 column: 1,
149                 row: 1
150             }
151         }
152     };
153
154     if (dataKey && dataKey.length > 0) {
155
156         egNet.request(
157             'open-ils.actor',
158             'open-ils.actor.anon_cache.get_value',
159             dataKey, 'print-labels-these-copies'
160         ).then(function (data) {
161
162             if (data) {
163
164                 $scope.preview_scope = {
165                     'copies': []
166                     , 'settings': {}
167                     , 'toolbox_settings': JSON.parse(JSON.stringify(toolbox_settings))
168                     , 'get_cn_for': function (copy) {
169                         var key = $scope.rendered_cn_key_by_copy_id[copy.id];
170                         if (key) {
171                             var manual_cn = $scope.rendered_call_number_set[key];
172                             if (manual_cn && manual_cn.value) {
173                                 return manual_cn.value;
174                             } else {
175                                 return '..';
176                             }
177                         } else {
178                             return '...';
179                         }
180                     }
181                     , 'get_bib_for': function (copy) {
182                         return $scope.record_details[copy['call_number.record.id']];
183                     }
184                     , 'get_cn_prefix': function (copy) {
185                         return copy['call_number.prefix.label'];
186                     }
187                     , 'get_cn_suffix': function (copy) {
188                         return copy['call_number.suffix.label'];
189                     }
190                     , 'get_location_prefix': function (copy) {
191                         return copy['location.label_prefix'];
192                     }
193                     , 'get_location_suffix': function (copy) {
194                         return copy['location.label_suffix'];
195                     }
196                     , 'get_cn_and_location_prefix': function (copy, separator) {
197                         var acpl_prefix = copy['location.label_prefix'] || '';
198                         var cn_prefix = copy['call_number.prefix.label'] || '';
199                         var prefix = acpl_prefix + ' ' + cn_prefix;
200                         prefix = prefix.trim();
201                         if (separator && prefix != '') { prefix += separator; }
202                         return prefix;
203                     }
204                     , 'get_cn_and_location_suffix': function (copy, separator) {
205                         var acpl_suffix = copy['location.label_suffix'] || '';
206                         var cn_suffix = copy['call_number.suffix.label'] || '';
207                         var suffix = cn_suffix + ' ' + acpl_suffix;
208                         suffix = suffix.trim();
209                         if (separator && suffix != '') { suffix = separator + suffix; }
210                         return suffix;
211                     }
212                     , 'valid_print_label_start_column': function () {
213                         return !angular.isNumber(toolbox_settings.page.dimensions.columns) || !angular.isNumber(toolbox_settings.page.start_position.column) ? false : (toolbox_settings.page.start_position.column <= toolbox_settings.page.dimensions.columns);
214                     }
215                     , 'valid_print_label_start_row': function () {
216                         return !angular.isNumber(toolbox_settings.page.dimensions.rows) || !angular.isNumber(toolbox_settings.page.start_position.row) ? false : (toolbox_settings.page.start_position.row <= toolbox_settings.page.dimensions.rows);
217                     }
218                 };
219                 $scope.record_details = {};
220                 $scope.org_unit_settings = {};
221
222                 var promises = [];
223                 $scope.org_unit_setting_list = [
224                      'webstaff.cat.label.font.family'
225                     , 'webstaff.cat.label.font.size'
226                     , 'webstaff.cat.label.font.weight'
227                     , 'webstaff.cat.label.inline_css'
228                     , 'webstaff.cat.label.left_label.height'
229                     , 'webstaff.cat.label.left_label.left_margin'
230                     , 'webstaff.cat.label.left_label.width'
231                     , 'webstaff.cat.label.right_label.height'
232                     , 'webstaff.cat.label.right_label.left_margin'
233                     , 'webstaff.cat.label.right_label.width'
234                     , 'webstaff.cat.label.call_number_wrap_filter_height'
235                     , 'webstaff.cat.label.call_number_wrap_filter_width'
236                 ];
237
238                 promises.push(
239                     egCore.pcrud.search('coust', { name: $scope.org_unit_setting_list }).then(
240                          null
241                         , null
242                         , function (yaous) {
243                             $scope.org_unit_settings[yaous.name()] = egCore.idl.toHash(yaous, true);
244                         }
245                     )
246                 );
247
248                 promises.push(
249                     egCore.org.settings($scope.org_unit_setting_list).then(function (res) {
250                         $scope.preview_scope.settings = res;
251                         egCore.hatch.getItem('cat.printlabels.last_settings').then(function (last_settings) {
252                             if (last_settings) {
253                                 for (s in last_settings) {
254                                     $scope.preview_scope.settings[s] = last_settings[s];
255                                 }
256                             }
257                         });
258                         egCore.hatch.getItem('cat.printlabels.last_toolbox_settings').then(function (last_toolbox_settings) {
259                             if (last_toolbox_settings) {
260                                 $scope.preview_scope.toolbox_settings = JSON.parse(JSON.stringify(last_toolbox_settings));
261                             }
262                         });
263                     })
264                 );
265
266                 angular.forEach(data.copies, function (copy) {
267                     promises.push(
268                         itemSvc.fetch(null, copy).then(function (res) {
269                             var flat_copy = egCore.idl.toHash(res.copy, true);
270                             $scope.preview_scope.copies.push(flat_copy);
271                             $scope.record_details[flat_copy['call_number.record.id']] = 1;
272                         })
273                     )
274                 });
275
276                 $q.all(promises).then(function () {
277
278                     var promises2 = [];
279                     angular.forEach($scope.record_details, function (el, k, obj) {
280                         promises2.push(
281                             egNet.request(
282                                 'open-ils.search',
283                                 'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',
284                                 k
285                             ).then(function (data) {
286                                 obj[k] = egCore.idl.toHash(data, true);
287                             })
288                         );
289                     });
290
291                     $q.all(promises2).then(function () {
292                         // today, staff, current_location, etc.
293                         egCore.print.fleshPrintScope($scope.preview_scope);
294                         $scope.template_changed(); // load the default
295                         $scope.rebuild_cn_set();
296                     });
297
298                 });
299             } else {
300                 ngToast.danger(egCore.strings.KEY_EXPIRED);
301             }
302
303         });
304
305     }
306
307     $scope.checkForToolboxCustomizations = function (tText, redraw) {
308         var re = /eg\_plt\_(\d+)/;
309         redraw ? $scope.redraw_label_table() : false;
310         return re.test(tText);
311     }
312
313     $scope.fetchTemplates = function (set_default) {
314         return egCore.hatch.getItem('cat.printlabels.templates').then(function (t) {
315             if (t) {
316                 $scope.templates = t;
317                 $scope.template_name_list = Object.keys(t);
318                 if (set_default) {
319                     egCore.hatch.getItem('cat.printlabels.default_template').then(function (d) {
320                         if ($scope.template_name_list.indexOf(d, 0) > -1) {
321                             $scope.template_name = d;
322                         }
323                     });
324                 }
325             }
326         });
327     }
328     $scope.fetchTemplates(true);
329
330     $scope.applyTemplate = function (n) {
331         $scope.print.cn_template_content = $scope.templates[n].cn_content;
332         $scope.print.template_content = $scope.templates[n].content;
333         $scope.print.template_context = $scope.templates[n].context;
334         for (var s in $scope.templates[n].settings) {
335             $scope.preview_scope.settings[s] = $scope.templates[n].settings[s];
336         }
337         if ($scope.templates[n].toolbox_settings) {
338             $scope.preview_scope.toolbox_settings = JSON.parse(JSON.stringify($scope.templates[n].toolbox_settings));
339         }
340         egCore.hatch.setItem('cat.printlabels.default_template', n);
341         $scope.save_locally();
342     }
343
344     $scope.deleteTemplate = function (n) {
345         if (n) {
346             delete $scope.templates[n]
347             $scope.template_name_list = Object.keys($scope.templates);
348             $scope.template_name = '';
349             egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
350             $scope.fetchTemplates();
351             ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_DELETE);
352             egCore.hatch.getItem('cat.printlabels.default_template').then(function (d) {
353                 if (d && d == n) {
354                     egCore.hatch.removeItem('cat.printlabels.default_template');
355                 }
356             });
357         }
358     }
359
360     $scope.saveTemplate = function (n) {
361         if (n) {
362
363             $scope.templates[n] = {
364                 content: $scope.print.template_content
365                 , context: $scope.print.template_context
366                 , cn_content: $scope.print.cn_template_content
367                 , settings: JSON.parse(JSON.stringify($scope.preview_scope.settings))
368                 , toolbox_settings: JSON.parse(JSON.stringify($scope.preview_scope.toolbox_settings))
369             };
370             $scope.template_name_list = Object.keys($scope.templates);
371
372             egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
373             $scope.fetchTemplates();
374
375             $scope.dirty = false;
376         } else {
377             // save all templates, as we might do after an import
378             egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
379             $scope.fetchTemplates();
380         }
381         ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_SAVE);
382     }
383
384     $scope.templates = {};
385     $scope.imported_templates = { data: '' };
386     $scope.template_name = '';
387     $scope.template_name_list = [];
388
389     $scope.print_labels = function () {
390         return egCore.print.print({
391             context: $scope.print.template_context,
392             template: $scope.print.template_name,
393             scope: $scope.preview_scope,
394         });
395     }
396
397     $scope.template_changed = function () {
398         $scope.print.load_failed = false;
399         egCore.print.getPrintTemplate('item_label')
400         .then(
401             function (html) {
402                 $scope.print.template_content = html;
403                 $scope.checkForToolboxCustomizations(html, true);
404             },
405             function () {
406                 $scope.print.template_content = '';
407                 $scope.print.load_failed = true;
408             }
409         );
410         egCore.print.getPrintTemplateContext('item_label')
411         .then(function (template_context) {
412             $scope.print.template_context = template_context;
413         });
414         egCore.print.getPrintTemplate('item_label_cn')
415         .then(
416             function (html) {
417                 $scope.print.cn_template_content = html;
418             },
419             function () {
420                 $scope.print.cn_template_content = '';
421                 $scope.print.load_failed = true;
422             }
423         );
424         egCore.hatch.getItem('cat.printlabels.last_settings').then(function (s) {
425             if (s) {
426                 $scope.preview_scope.settings = JSON.parse(JSON.stringify(s));
427             }
428         });
429         egCore.hatch.getItem('cat.printlabels.last_toolbox_settings').then(function (t) {
430             if (t) {
431                 $scope.preview_scope.toolbox_settings = JSON.parse(JSON.stringify(t));
432             }
433         });
434
435     }
436
437     $scope.reset_to_default = function () {
438         egCore.print.removePrintTemplate(
439             'item_label'
440         );
441         egCore.print.removePrintTemplateContext(
442             'item_label'
443         );
444         egCore.print.removePrintTemplate(
445             'item_label_cn'
446         );
447         egCore.hatch.removeItem('cat.printlabels.last_settings');
448         egCore.hatch.removeItem('cat.printlabels.last_toolbox_settings');
449         for (s in $scope.preview_scope.settings) {
450             $scope.preview_scope.settings[s] = undefined;
451         }
452         $scope.preview_scope.toolbox_settings = JSON.parse(JSON.stringify(toolbox_settings));
453
454         egCore.org.settings($scope.org_unit_setting_list).then(function (res) {
455             $scope.preview_scope.settings = res;
456         });
457
458         $scope.template_changed();
459     }
460
461     $scope.save_locally = function () {
462         egCore.print.storePrintTemplate(
463             'item_label',
464             $scope.print.template_content
465         );
466         egCore.print.storePrintTemplateContext(
467             'item_label',
468             $scope.print.template_context
469         );
470         egCore.print.storePrintTemplate(
471             'item_label_cn',
472             $scope.print.cn_template_content
473         );
474         egCore.hatch.setItem('cat.printlabels.last_settings', JSON.parse(JSON.stringify($scope.preview_scope.settings)));
475         egCore.hatch.setItem('cat.printlabels.last_toolbox_settings', JSON.parse(JSON.stringify($scope.preview_scope.toolbox_settings)));
476     }
477
478     $scope.imported_print_templates = { data: '' };
479     $scope.$watch('imported_templates.data', function (newVal, oldVal) {
480         if (newVal && newVal != oldVal) {
481             try {
482                 var data = JSON.parse(newVal);
483                 angular.forEach(data, function (el, k) {
484                     $scope.templates[k] = {
485                         content: el.content
486                         , context: el.context
487                         , cn_content: el.cn_content
488                         , settings: JSON.parse(JSON.stringify(el.settings))
489                     };
490                     if (el.toolbox_settings) {
491                         $scope.templates[k].toolbox_settings = JSON.parse(JSON.stringify(el.toolbox_settings));
492                     }
493                 });
494                 $scope.saveTemplate();
495                 $scope.template_changed(); // refresh
496                 ngToast.create(egCore.strings.PRINT_TEMPLATES_SUCCESS_IMPORT);
497             } catch (E) {
498                 ngToast.warning(egCore.strings.PRINT_TEMPLATES_FAIL_IMPORT);
499             }
500         }
501     });
502
503     $scope.rendered_call_number_set = {};
504     $scope.rendered_cn_key_by_copy_id = {};
505     $scope.rebuild_cn_set = function () {
506         $timeout(function () {
507             $scope.rendered_call_number_set = {};
508             $scope.rendered_cn_key_by_copy_id = {};
509             for (var i = 0; i < $scope.preview_scope.copies.length; i++) {
510                 var copy = $scope.preview_scope.copies[i];
511                 var rendered_cn = document.getElementById('cn_for_copy_' + copy.id);
512                 if (rendered_cn && rendered_cn.textContent) {
513                     var key = rendered_cn.textContent;
514                     if (typeof $scope.rendered_call_number_set[key] == 'undefined') {
515                         $scope.rendered_call_number_set[key] = {
516                             value: key
517                         };
518                     }
519                     $scope.rendered_cn_key_by_copy_id[copy.id] = key;
520                 }
521             }
522             $scope.preview_scope.tickle = Date() + ' ' + Math.random();
523         });
524     }
525
526     $scope.redraw_label_table = function () {
527         if ($scope.print_label_form.$valid && $scope.print.template_content && $scope.preview_scope) {
528             $scope.preview_scope.label_output_copies = labelOutputRowsFilter($scope.preview_scope.copies, $scope.preview_scope.toolbox_settings);
529             var d = new Date().getTime().toString();
530             var html = $scope.print.template_content;
531             if ($scope.checkForToolboxCustomizations(html)) {
532                 html = html.replace(/eg\_plt\_\d+/, "eg_plt_" + d);
533                 $scope.print.template_content = html;
534             } else {
535                 var table = "<table id=\"eg_plt_" + d + "_{{$index}}\" eg-print-label-table style=\"border-collapse: collapse; border: 0 solid transparent; border-spacing: 0; margin: {{$index === 0 ? toolbox_settings.page.margins.top.size : 0}} 0 0 0;\" class=\"custom-label-table{{$index % toolbox_settings.page.dimensions.rows === 0 && $index > 0 && toolbox_settings.feed_option.selected === 'sheet' ? ' page-break' : ''}}\" ng-init=\"parentIndex = $index\" ng-repeat=\"row in label_output_copies\">\n";
536                 table += "<tr>\n";
537                 table += "<td style=\"border: 0 solid transparent; padding: {{parentIndex % toolbox_settings.page.dimensions.rows === 0 && toolbox_settings.feed_option.selected === 'sheet' && parentIndex > 0 ? toolbox_settings.page.space_between_labels.vertical.size : parentIndex > 0 ? toolbox_settings.page.space_between_labels.vertical.size : 0}} 0 0 {{$index === 0 ? toolbox_settings.page.margins.left.size : col.styl ? col.styl : toolbox_settings.page.space_between_labels.horizontal.size}};\" ng-repeat=\"col in row.columns\">\n";
538                 table += "<pre class=\"{{col.cls}}\" style=\"border: none; margin-bottom: 0; margin-top: 0; overflow: hidden;\" ng-if=\"col.cls === 'spine'\">\n";
539                 table += "{{col.c ? get_cn_for(col.c) : ''}}";
540                 table += "</pre>\n";
541                 table += "<pre class=\"{{col.cls}}{{parentIndex % toolbox_settings.page.dimensions.rows === 0 && parentIndex > 0 && toolbox_settings.feed_option.selected === 'sheet' ? ' page-break' : ''}}\" style=\"border: none;  margin-bottom: 0; margin-top: 0; overflow: hidden;\" ng-if=\"col.cls === 'pocket'\">\n";
542                 table += "{{col.c ? col.c.barcode : ''}}\n";
543                 table += "{{col.c ? col.c['call_number.label'] : ''}}\n";
544                 table += "{{col.c ? get_bib_for(col.c).author : ''}}\n";
545                 table += "{{col.c ? (get_bib_for(col.c).title | wrap:28:'once':'  ') : ''}}\n";
546                 table += "</pre>\n";
547                 table += "</td>\n"
548                 table += "</tr>\n";
549                 table += "</table>";
550                 var comments = html.match(/\<\!\-\-(?:(?!\-\-\>)(?:.|\s))*\-\-\>\s*/g);
551                 html = html.replace(/\<\!\-\-(?:(?!\-\-\>)(?:.|\s))*\-\-\>\s*/g, '');
552                 var style = html.match(/\<style[^\>]*\>(?:(?!\<\/style\>)(?:.|\s))*\<\/style\>\s*/gi);
553                 var output = (comments ? comments.join("\n") : "") + (style ? style.join("\n") : "") + table;
554                 output = output.replace(/\n+/, "\n");
555                 $scope.print.template_content = output;
556             }
557         }
558     }
559
560     $scope.$watch('print.cn_template_content', function (newVal, oldVal) {
561         if (newVal && newVal != oldVal) {
562             $scope.rebuild_cn_set();
563         }
564     });
565
566     $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_height']", function (newVal, oldVal) {
567         if (newVal && newVal != oldVal) {
568             $scope.rebuild_cn_set();
569         }
570     });
571
572     $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_width']", function (newVal, oldVal) {
573         if (newVal && newVal != oldVal) {
574             $scope.rebuild_cn_set();
575         }
576     });
577
578     $scope.$watchGroup(['preview_scope.toolbox_settings.page.margins.top.size', 'preview_scope.toolbox_settings.page.margins.left.size', 'preview_scope.toolbox_settings.page.dimensions.rows', 'preview_scope.toolbox_settings.page.dimensions.columns', 'preview_scope.toolbox_settings.page.space_between_labels.horizontal.size', 'preview_scope.toolbox_settings.page.space_between_labels.vertical.size', 'preview_scope.toolbox_settings.page.start_position.row', 'preview_scope.toolbox_settings.page.start_position.column', 'preview_scope.toolbox_settings.page.label.gap.size'], function (newVal, oldVal) {
579         if (newVal && newVal != oldVal && $scope.preview_scope.label_output_copies) {
580             $scope.redraw_label_table();
581         }
582     });
583
584     $scope.$watch("preview_scope.toolbox_settings.mode.selected", function (newVal, oldVal) {
585         if (newVal && newVal != oldVal && $scope.preview_scope) {
586             var ts_p = $scope.preview_scope.toolbox_settings.page;
587             if (ts_p.label.set.size === 1) {
588                 if (newVal === "spine-pocket") {
589                     ts_p.column_class = ["spine", "pocket"];
590                     ts_p.label.set.size = 2;
591                 } else {
592                     ts_p.column_class = ["spine"];
593                 }
594             } else {
595                 if (newVal === "spine-only") {
596                     for (var i = 0; i < ts_p.label.set.size; i++) {
597                         ts_p.column_class[i] = "spine";
598                     }
599                 } else {
600                     ts_p.label.set.size === 2 ? ts_p.column_class = ["spine", "pocket"] : false;
601                 }
602             }
603             $scope.redraw_label_table();
604         }
605     });
606
607     $scope.$watch("preview_scope.toolbox_settings.page.label.set.size", function (newVal, oldVal) {
608         if (newVal && newVal != oldVal && oldVal) {
609             var ts_p = $scope.preview_scope.toolbox_settings.page;
610             if (angular.isNumber(newVal)) {
611                 while (ts_p.column_class.length > ts_p.label.set.size) {
612                     ts_p.column_class.splice((ts_p.column_class.length - 1), 1);
613                 }
614                 while (ts_p.column_class.length < ts_p.label.set.size) {
615                     ts_p.column_class.push("spine");
616                 }
617             }
618         }
619     });
620
621     $scope.$watch('print.cn_template_content', function (newVal, oldVal) {
622         if (newVal && newVal != oldVal) {
623             $scope.rebuild_cn_set();
624         }
625     });
626
627     $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_height']", function (newVal, oldVal) {
628         if (newVal && newVal != oldVal) {
629             $scope.rebuild_cn_set();
630         }
631     });
632
633     $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_width']", function (newVal, oldVal) {
634         if (newVal && newVal != oldVal) {
635             $scope.rebuild_cn_set();
636         }
637     });
638
639     $scope.current_tab = 'call_numbers';
640     $scope.set_tab = function (tab) {
641         $scope.current_tab = tab;
642     }
643
644 }])
645
646 .directive("egPrintLabelColumnBounds", function () {
647     return {
648         link: function (scope, element, attr, ctrl) {
649             function withinBounds(v) {
650                 scope.$watch("preview_scope.toolbox_settings.page.dimensions.columns", function (newVal, oldVal) {
651                     ctrl.$setValidity("egWithinPrintColumnBounds", scope.preview_scope.valid_print_label_start_column())
652                 });
653                 return v;
654             }
655             ctrl.$parsers.push(withinBounds);
656             ctrl.$formatters.push(withinBounds);
657         },
658         require: "ngModel"
659     }
660 })
661
662 .directive("egPrintLabelRowBounds", function () {
663     return {
664         link: function (scope, element, attr, ctrl) {
665             function withinBounds(v) {
666                 scope.$watch("preview_scope.toolbox_settings.page.dimensions.rows", function (newVal, oldVal) {
667                     ctrl.$setValidity("egWithinPrintRowBounds", scope.preview_scope.valid_print_label_start_row());
668                 });
669                 return v;
670             }
671             ctrl.$parsers.push(withinBounds);
672             ctrl.$formatters.push(withinBounds);
673         },
674         require: "ngModel"
675     }
676 })
677
678 .directive("egPrintLabelValidCss", function () {
679     return {
680         require: "ngModel",
681         link: function (scope, element, attr, ctrl) {
682             function floatValidation(v) {
683                 ctrl.$setValidity("isFloat", v.toString().match(/^\-*(?:^0$|(?:\d+)(?:\.\d{1,})*([a-z]{2}))$/) ? true : false);
684                 return v;
685             }
686             ctrl.$parsers.push(floatValidation);
687         }
688     }
689 })
690
691 .directive("egPrintLabelValidInt", function () {
692     return {
693         require: "ngModel",
694         link: function (scope, element, attr, ctrl) {
695             function intValidation(v) {
696                 ctrl.$setValidity("isInteger", v.toString().match(/^\d+$/));
697                 return v;
698             }
699             ctrl.$parsers.push(intValidation);
700         }
701     }
702 })
703
704 .directive('egPrintTemplateOutput', ['$compile', function ($compile) {
705     return function (scope, element, attrs) {
706         scope.$watch(
707             function (scope) {
708                 return scope.$eval(attrs.content);
709             },
710             function (value) {
711                 // create an isolate scope and copy the print context
712                 // data into the new scope.
713                 // TODO: see also print security concerns in egHatch
714                 var result = element.html(value);
715                 var context = scope.$eval(attrs.context);
716                 var print_scope = scope.$new(true);
717                 angular.forEach(context, function (val, key) {
718                     print_scope[key] = val;
719                 })
720                 $compile(element.contents())(print_scope);
721             }
722         );
723     };
724 }])
725
726 .filter('cn_wrap', function () {
727     return function (input, w, h, wrap_type) {
728         var names;
729         var prefix = input[0];
730         var callnum = input[1];
731         var suffix = input[2];
732
733         if (!w) { w = 8; }
734         if (!h) { h = 9; }
735
736         /* handle spine labels differently if using LC */
737         if (wrap_type == 'lc' || wrap_type == 3) {
738             /* Establish a pattern where every return value should be isolated on its own line 
739                on the spine label: subclass letters, subclass numbers, cutter numbers, trailing stuff (date) */
740             var patt1 = /^([A-Z]{1,3})\s*(\d+(?:\.\d+)?)\s*(\.[A-Z]\d*)\s*([A-Z]\d*)?\s*(\d\d\d\d(?:-\d\d\d\d)?)?\s*(.*)$/i;
741             var result = callnum.match(patt1);
742             if (result) {
743                 callnum = result.slice(1).join('\t');
744             } else {
745                 callnum = callnum.split(/\s+/).join('\t');
746             }
747
748             /* If result is null, leave callnum alone. Can't parse this malformed call num */
749         } else {
750             callnum = callnum.split(/\s+/).join('\t');
751         }
752
753         if (prefix) {
754             callnum = prefix + '\t' + callnum;
755         }
756         if (suffix) {
757             callnum += '\t' + suffix;
758         }
759
760         /* At this point, the call number pieces are separated by tab characters.  This allows
761         *  some space-containing constructs like "v. 1" to appear on one line
762         */
763         callnum = callnum.replace(/\t\t/g, '\t');  /* Squeeze out empties */
764         names = callnum.split('\t');
765         var j = 0; var tb = [];
766         while (j < h) {
767
768             /* spine */
769             if (j < w) {
770
771                 var name = names.shift();
772                 if (name) {
773                     name = String(name);
774
775                     /* if the name is greater than the label width... */
776                     if (name.length > w) {
777                         /* then try to split it on periods */
778                         var sname = name.split(/\./);
779                         if (sname.length > 1) {
780                             /* if we can, then put the periods back in on each splitted element */
781                             if (name.match(/^\./)) sname[0] = '.' + sname[0];
782                             for (var k = 1; k < sname.length; k++) sname[k] = '.' + sname[k];
783                             /* and put all but the first one back into the names array */
784                             names = sname.slice(1).concat(names);
785                             /* if the name fragment is still greater than the label width... */
786                             if (sname[0].length > w) {
787                                 /* then just truncate and throw the rest back into the names array */
788                                 tb[j] = sname[0].substr(0, w);
789                                 names = [sname[0].substr(w)].concat(names);
790                             } else {
791                                 /* otherwise we're set */
792                                 tb[j] = sname[0];
793                             }
794                         } else {
795                             /* if we can't split on periods, then just truncate and throw the rest back into the names array */
796                             tb[j] = name.substr(0, w);
797                             names = [name.substr(w)].concat(names);
798                         }
799                     } else {
800                         /* otherwise we're set */
801                         tb[j] = name;
802                     }
803                 }
804             }
805             j++;
806         }
807         return tb.join('\n');
808     }
809 })
810
811 .filter("columnRowRange", function () {
812     return function (i) {
813         var res = [];
814         for (var j = 0; j < i; j++) {
815             res.push(j);
816         }
817         return res;
818     }
819 })
820
821 //Accepts $scope.preview_scope.copies and $scope.preview_scope.toolbox_settings as its parameters.
822 .filter("labelOutputRows", function () {
823     return function (copies, settings) {
824         var cols = [], rows = [];
825         for (var j = 0; j < (settings.page.start_position.row - 1) ; j++) {
826             cols = [];
827             for (var k = 0; k < settings.page.dimensions.columns; k++) {
828                 cols.push({ c: null, index: k, cls: getPrintLabelOutputClass(k, settings), styl: getPrintLabelStyle(k, settings) });
829             }
830             rows.push({ columns: cols });
831         }
832         cols = [];
833         for (var j = 0; j < (settings.page.start_position.column - 1) ; j++) {
834             cols.push({ c: null, index: j, cls: getPrintLabelOutputClass(j, settings), styl: getPrintLabelStyle(j, settings) });
835         }
836         var m = cols.length;
837         for (var j = 0; j < copies.length; j++) {
838             for (var n = 0; n < settings.page.label.set.size; n++) {
839                 if (m < settings.page.dimensions.columns) {
840                     cols.push({ c: copies[j], index: cols.length, cls: getPrintLabelOutputClass(m, settings), styl: getPrintLabelStyle(m, settings) });
841                     m += 1;
842                 }
843                 if (m === settings.page.dimensions.columns) {
844                     m = 0;
845                     rows.push({ columns: cols });
846                     cols = [];
847                     n = settings.page.label.set.size;
848                 }
849             }
850         }
851         cols.length > 0 ? rows.push({ columns: cols }) : false;
852         if (rows.length > 0) {
853             while ((rows[(rows.length - 1)].columns.length) < settings.page.dimensions.columns) {
854                 rows[(rows.length - 1)].columns.push({ c: null, index: rows[(rows.length - 1)].columns.length, cls: getPrintLabelOutputClass(rows[(rows.length - 1)].columns.length, settings), styl: getPrintLabelStyle(rows[(rows.length - 1)].columns.length, settings) });
855             }
856         }
857         return rows;
858     }
859 })
860
861 .filter('wrap', function () {
862     return function (input, w, wrap_type, indent) {
863         var output;
864
865         if (!w) return input;
866         if (!indent) indent = '';
867
868         function wrap_on_space(
869                 text,
870                 length,
871                 wrap_just_once,
872                 if_cant_wrap_then_truncate,
873                 idx
874         ) {
875             if (idx > 10) {
876                 console.log('possible infinite recursion, aborting');
877                 return '';
878             }
879             if (String(text).length <= length) {
880                 return text;
881             } else {
882                 var truncated_text = String(text).substr(0, length);
883                 var pivot_pos = truncated_text.lastIndexOf(' ');
884                 var left_chunk = text.substr(0, pivot_pos).replace(/\s*$/, '');
885                 var right_chunk = String(text).substr(pivot_pos + 1);
886
887                 var wrapped_line;
888                 if (left_chunk.length == 0) {
889                     if (if_cant_wrap_then_truncate) {
890                         wrapped_line = truncated_text;
891                     } else {
892                         wrapped_line = text;
893                     }
894                 } else {
895                     wrapped_line =
896                         left_chunk + '\n'
897                         + indent + (
898                             wrap_just_once
899                             ? right_chunk
900                             : (
901                                 right_chunk.length > length
902                                 ? wrap_on_space(
903                                     right_chunk,
904                                     length,
905                                     false,
906                                     if_cant_wrap_then_truncate,
907                                     idx + 1)
908                                 : right_chunk
909                             )
910                         )
911                     ;
912                 }
913                 return wrapped_line;
914             }
915         }
916
917         switch (wrap_type) {
918             case 'once':
919                 output = wrap_on_space(input, w, true, false, 0);
920                 break;
921             default:
922                 output = wrap_on_space(input, w, false, false, 0);
923                 break;
924         }
925
926         return output;
927     }
928 });
929
930 function getPrintLabelOutputClass(index, settings) {
931     return settings.page.column_class[index % settings.page.label.set.size];
932 }
933
934 function getPrintLabelStyle(index, settings) {
935     return index > 0 && (index % settings.page.label.set.size === 0) ? settings.page.label.gap.size : "";
936 }