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