LP#1787479: Custom label printing toolbox
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / cat / printlabels / app.js
index 28b78d1..9d006b8 100644 (file)
@@ -5,69 +5,69 @@
 angular.module('egPrintLabels',
     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
 
-.config(function($routeProvider, $locationProvider, $compileProvider) {
+.config(function ($routeProvider, $locationProvider, $compileProvider) {
     $locationProvider.html5Mode(true);
     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
-       
+
     var resolver = {
-        delay : ['egStartup', function(egStartup) { return egStartup.go(); }]
+        delay: ['egStartup', function (egStartup) { return egStartup.go(); }]
     };
 
     $routeProvider.when('/cat/printlabels/:dataKey', {
         templateUrl: './cat/printlabels/t_view',
         controller: 'LabelCtrl',
-        resolve : resolver
+        resolve: resolver
     });
 
 })
 
-.factory('itemSvc', 
+.factory('itemSvc',
        ['egCore',
-function(egCore) {
+function (egCore) {
 
     var service = {
-        copies : [], // copy barcode search results
-        index : 0 // search grid index
+        copies: [], // copy barcode search results
+        index: 0 // search grid index
     };
 
-    service.flesh = {   
-        flesh : 3, 
-        flesh_fields : {
-            acp : ['call_number','location','status','location','floating','circ_modifier','age_protect','circ_lib'],
-            acn : ['record','prefix','suffix','owning_lib'],
-            bre : ['simple_record','creator','editor']
+    service.flesh = {
+        flesh: 3,
+        flesh_fields: {
+            acp: ['call_number', 'location', 'status', 'location', 'floating', 'circ_modifier', 'age_protect'],
+            acn: ['record', 'prefix', 'suffix'],
+            bre: ['simple_record', 'creator', 'editor']
         },
-        select : { 
+        select: {
             // avoid fleshing MARC on the bre
             // note: don't add simple_record.. not sure why
-            bre : ['id','tcn_value','creator','editor'],
-        } 
+            bre: ['id', 'tcn_value', 'creator', 'editor'],
+        }
     }
 
     // resolved with the last received copy
-    service.fetch = function(barcode, id, noListDupes) {
+    service.fetch = function (barcode, id, noListDupes) {
         var promise;
 
         if (barcode) {
-            promise = egCore.pcrud.search('acp', 
-                {barcode : barcode, deleted : 'f'}, service.flesh);
+            promise = egCore.pcrud.search('acp',
+                { barcode: barcode, deleted: 'f' }, service.flesh);
         } else {
             promise = egCore.pcrud.retrieve('acp', id, service.flesh);
         }
 
         var lastRes;
         return promise.then(
-            function() {return lastRes},
+            function () { return lastRes },
             null, // error
 
             // notify reads the stream of copies, one at a time.
-            function(copy) {
+            function (copy) {
 
                 var flatCopy;
                 if (noListDupes) {
                     // use the existing copy if possible
                     flatCopy = service.copies.filter(
-                        function(c) {return c.id == copy.id()})[0];
+                        function (c) { return c.id == copy.id() })[0];
                 }
 
                 if (!flatCopy) {
@@ -77,8 +77,8 @@ function(egCore) {
                 }
 
                 return lastRes = {
-                    copy : copy, 
-                    index : flatCopy.index
+                    copy: copy,
+                    index: flatCopy.index
                 }
             }
         );
@@ -90,19 +90,66 @@ function(egCore) {
 /**
  * Label controller!
  */
-.controller('LabelCtrl', 
-       ['$scope','$q','$window','$routeParams','$location','$timeout','egCore','egNet','ngToast','itemSvc',
-function($scope , $q , $window , $routeParams , $location , $timeout , egCore , egNet , ngToast , itemSvc ) {
+.controller('LabelCtrl',
+       ['$scope', '$q', '$window', '$routeParams', '$location', '$timeout', 'egCore', 'egNet', 'ngToast', 'itemSvc', 'labelOutputRowsFilter',
+function ($scope, $q, $window, $routeParams, $location, $timeout, egCore, egNet, ngToast, itemSvc, labelOutputRowsFilter) {
 
     var dataKey = $routeParams.dataKey;
     console.debug('dataKey: ' + dataKey);
 
     $scope.print = {
-        template_name : 'item_label',
-        template_output : '',
-        template_context : 'default'
+        template_name: 'item_label',
+        template_output: '',
+        template_context: 'default'
     };
 
+    var toolbox_settings = {
+        feed_option: {
+            options: [
+                { label: "Continuous", value: "continuous" },
+                { label: "Sheet", value: "sheet" },
+            ],
+            selected: "continuous"
+        },
+        label_set: {
+            margin_between: 0,
+            size: 1
+        },
+        mode: {
+            options: [
+                { label: "Label 1 Only", value: "spine-only" },
+                { label: "Labels 1 & 2", value: "spine-pocket" }
+            ],
+            selected: "spine-pocket"
+        },
+        page: {
+            column_class: ["spine"],
+            dimensions: {
+                columns: 2,
+                rows: 1
+            },
+            label: {
+                gap: {
+                    size: 0
+                },
+                set: {
+                    size: 2
+                }
+            },
+            margins: {
+                top: { size: 0, label: "Top" },
+                left: { size: 0, label: "Left" },
+            },
+            space_between_labels: {
+                horizontal: { size: 0, label: "Horizontal" },
+                vertical: { size: 0, label: "Vertical" }
+            },
+            start_position: {
+                column: 1,
+                row: 1
+            }
+        }
+    };
 
     if (dataKey && dataKey.length > 0) {
 
@@ -115,9 +162,10 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             if (data) {
 
                 $scope.preview_scope = {
-                     'copies' : []
-                    ,'settings' : {}
-                    ,'get_cn_for' : function(copy) {
+                    'copies': []
+                    , 'settings': {}
+                    , 'toolbox_settings': toolbox_settings
+                    , 'get_cn_for': function (copy) {
                         var key = $scope.rendered_cn_key_by_copy_id[copy.id];
                         if (key) {
                             var manual_cn = $scope.rendered_call_number_set[key];
@@ -130,22 +178,22 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                             return '...';
                         }
                     }
-                    ,'get_bib_for' : function(copy) {
+                    , 'get_bib_for': function (copy) {
                         return $scope.record_details[copy['call_number.record.id']];
                     }
-                    ,'get_cn_prefix' : function(copy) {
+                    , 'get_cn_prefix': function (copy) {
                         return copy['call_number.prefix.label'];
                     }
-                    ,'get_cn_suffix' : function(copy) {
+                    , 'get_cn_suffix': function (copy) {
                         return copy['call_number.suffix.label'];
                     }
-                    ,'get_location_prefix' : function(copy) {
+                    , 'get_location_prefix': function (copy) {
                         return copy['location.label_prefix'];
                     }
-                    ,'get_location_suffix' : function(copy) {
+                    , 'get_location_suffix': function (copy) {
                         return copy['location.label_suffix'];
                     }
-                    ,'get_cn_and_location_prefix' : function(copy,separator) {
+                    , 'get_cn_and_location_prefix': function (copy, separator) {
                         var acpl_prefix = copy['location.label_prefix'] || '';
                         var cn_prefix = copy['call_number.prefix.label'] || '';
                         var prefix = acpl_prefix + ' ' + cn_prefix;
@@ -153,7 +201,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                         if (separator && prefix != '') { prefix += separator; }
                         return prefix;
                     }
-                    ,'get_cn_and_location_suffix' : function(copy,separator) {
+                    , 'get_cn_and_location_suffix': function (copy, separator) {
                         var acpl_suffix = copy['location.label_suffix'] || '';
                         var cn_suffix = copy['call_number.suffix.label'] || '';
                         var suffix = cn_suffix + ' ' + acpl_suffix;
@@ -161,6 +209,12 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                         if (separator && suffix != '') { suffix = separator + suffix; }
                         return suffix;
                     }
+                    , 'valid_print_label_start_column': function () {
+                        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);
+                    }
+                    , 'valid_print_label_start_row': function () {
+                        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);
+                    }
                 };
                 $scope.record_details = {};
                 $scope.org_unit_settings = {};
@@ -168,33 +222,33 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                 var promises = [];
                 $scope.org_unit_setting_list = [
                      'webstaff.cat.label.font.family'
-                    ,'webstaff.cat.label.font.size'
-                    ,'webstaff.cat.label.font.weight'
-                    ,'webstaff.cat.label.inline_css'
-                    ,'webstaff.cat.label.left_label.height'
-                    ,'webstaff.cat.label.left_label.left_margin'
-                    ,'webstaff.cat.label.left_label.width'
-                    ,'webstaff.cat.label.right_label.height'
-                    ,'webstaff.cat.label.right_label.left_margin'
-                    ,'webstaff.cat.label.right_label.width'
-                    ,'webstaff.cat.label.call_number_wrap_filter_height'
-                    ,'webstaff.cat.label.call_number_wrap_filter_width'
+                    , 'webstaff.cat.label.font.size'
+                    , 'webstaff.cat.label.font.weight'
+                    , 'webstaff.cat.label.inline_css'
+                    , 'webstaff.cat.label.left_label.height'
+                    , 'webstaff.cat.label.left_label.left_margin'
+                    , 'webstaff.cat.label.left_label.width'
+                    , 'webstaff.cat.label.right_label.height'
+                    , 'webstaff.cat.label.right_label.left_margin'
+                    , 'webstaff.cat.label.right_label.width'
+                    , 'webstaff.cat.label.call_number_wrap_filter_height'
+                    , 'webstaff.cat.label.call_number_wrap_filter_width'
                 ];
 
                 promises.push(
-                    egCore.pcrud.search('coust',{name:$scope.org_unit_setting_list}).then(
+                    egCore.pcrud.search('coust', { name: $scope.org_unit_setting_list }).then(
                          null
-                        ,null
-                        ,function(yaous) {
+                        , null
+                        , function (yaous) {
                             $scope.org_unit_settings[yaous.name()] = egCore.idl.toHash(yaous, true);
                         }
                     )
                 );
 
                 promises.push(
-                    egCore.org.settings($scope.org_unit_setting_list).then(function(res) {
+                    egCore.org.settings($scope.org_unit_setting_list).then(function (res) {
                         $scope.preview_scope.settings = res;
-                        egCore.hatch.getItem('cat.printlabels.last_settings').then(function(last_settings) {
+                        egCore.hatch.getItem('cat.printlabels.last_settings').then(function (last_settings) {
                             if (last_settings) {
                                 for (s in last_settings) {
                                     $scope.preview_scope.settings[s] = last_settings[s];
@@ -204,20 +258,20 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                     })
                 );
 
-                angular.forEach(data.copies, function(copy) {
+                angular.forEach(data.copies, function (copy) {
                     promises.push(
-                        itemSvc.fetch(null,copy).then(function(res) {
+                        itemSvc.fetch(null, copy).then(function (res) {
                             var flat_copy = egCore.idl.toHash(res.copy, true);
                             $scope.preview_scope.copies.push(flat_copy);
-                            $scope.record_details[ flat_copy['call_number.record.id'] ] = 1;
+                            $scope.record_details[flat_copy['call_number.record.id']] = 1;
                         })
                     )
                 });
 
-                $q.all(promises).then(function() {
+                $q.all(promises).then(function () {
 
                     var promises2 = [];
-                    angular.forEach($scope.record_details, function(el,k,obj) {
+                    angular.forEach($scope.record_details, function (el, k, obj) {
                         promises2.push(
                             egNet.request(
                                 'open-ils.search',
@@ -229,7 +283,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                         );
                     });
 
-                    $q.all(promises2).then(function() {
+                    $q.all(promises2).then(function () {
                         // today, staff, current_location, etc.
                         egCore.print.fleshPrintScope($scope.preview_scope);
                         $scope.template_changed(); // load the default
@@ -246,13 +300,13 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
     }
 
     $scope.fetchTemplates = function (set_default) {
-        return egCore.hatch.getItem('cat.printlabels.templates').then(function(t) {
+        return egCore.hatch.getItem('cat.printlabels.templates').then(function (t) {
             if (t) {
                 $scope.templates = t;
                 $scope.template_name_list = Object.keys(t);
                 if (set_default) {
-                    egCore.hatch.getItem('cat.printlabels.default_template').then(function(d) {
-                        if ($scope.template_name_list.indexOf(d,0) > -1) {
+                    egCore.hatch.getItem('cat.printlabels.default_template').then(function (d) {
+                        if ($scope.template_name_list.indexOf(d, 0) > -1) {
                             $scope.template_name = d;
                         }
                     });
@@ -269,6 +323,10 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         for (var s in $scope.templates[n].settings) {
             $scope.preview_scope.settings[s] = $scope.templates[n].settings[s];
         }
+        if ($scope.templates[n].toolbox_settings) {
+            $scope.preview_scope.toolbox_settings = $scope.templates[n].toolbox_settings;
+            $scope.create_print_label_table();
+        }
         egCore.hatch.setItem('cat.printlabels.default_template', n);
         $scope.save_locally();
     }
@@ -281,7 +339,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
             $scope.fetchTemplates();
             ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_DELETE);
-            egCore.hatch.getItem('cat.printlabels.default_template').then(function(d) {
+            egCore.hatch.getItem('cat.printlabels.default_template').then(function (d) {
                 if (d && d == n) {
                     egCore.hatch.removeItem('cat.printlabels.default_template');
                 }
@@ -293,10 +351,11 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         if (n) {
 
             $scope.templates[n] = {
-                 content : $scope.print.template_content
-                ,context : $scope.print.template_context
-                ,cn_content : $scope.print.cn_template_content
-                ,settings : $scope.preview_scope.settings
+                content: $scope.print.template_content
+                , context: $scope.print.template_context
+                , cn_content: $scope.print.cn_template_content
+                , settings: $scope.preview_scope.settings
+                , toolbox_settings: $scope.preview_scope.toolbox_settings
             };
             $scope.template_name_list = Object.keys($scope.templates);
 
@@ -313,53 +372,52 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
     }
 
     $scope.templates = {};
-    $scope.imported_templates = { data : '' };
+    $scope.imported_templates = { data: '' };
     $scope.template_name = '';
     $scope.template_name_list = [];
 
-    $scope.print_labels = function() {
+    $scope.print_labels = function () {
         return egCore.print.print({
-            context : $scope.print.template_context,
-            template : $scope.print.template_name,
-            scope : $scope.preview_scope,
+            context: $scope.print.template_context,
+            template: $scope.print.template_name,
+            scope: $scope.preview_scope,
         });
     }
 
-    $scope.template_changed = function() {
+    $scope.template_changed = function () {
         $scope.print.load_failed = false;
         egCore.print.getPrintTemplate('item_label')
         .then(
-            function(html) { 
+            function (html) {
                 $scope.print.template_content = html;
             },
-            function() {
+            function () {
                 $scope.print.template_content = '';
                 $scope.print.load_failed = true;
             }
         );
         egCore.print.getPrintTemplateContext('item_label')
-        .then(function(template_context) {
+        .then(function (template_context) {
             $scope.print.template_context = template_context;
         });
         egCore.print.getPrintTemplate('item_label_cn')
         .then(
-            function(html) {
+            function (html) {
                 $scope.print.cn_template_content = html;
             },
-            function() {
+            function () {
                 $scope.print.cn_template_content = '';
                 $scope.print.load_failed = true;
             }
         );
-        egCore.hatch.getItem('cat.printlabels.last_settings').then(function(s) {
+        egCore.hatch.getItem('cat.printlabels.last_settings').then(function (s) {
             if (s) {
                 $scope.preview_scope.settings = s;
             }
         });
-
     }
 
-    $scope.reset_to_default = function() {
+    $scope.reset_to_default = function () {
         egCore.print.removePrintTemplate(
             'item_label'
         );
@@ -374,14 +432,14 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             $scope.preview_scope.settings[s] = undefined;
         }
         $scope.preview_scope.settings = {};
-        egCore.org.settings($scope.org_unit_setting_list).then(function(res) {
+        egCore.org.settings($scope.org_unit_setting_list).then(function (res) {
             $scope.preview_scope.settings = res;
         });
 
         $scope.template_changed();
     }
 
-    $scope.save_locally = function() {
+    $scope.save_locally = function () {
         egCore.print.storePrintTemplate(
             'item_label',
             $scope.print.template_content
@@ -397,17 +455,18 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         egCore.hatch.setItem('cat.printlabels.last_settings', $scope.preview_scope.settings);
     }
 
-    $scope.imported_print_templates = { data : '' };
-    $scope.$watch('imported_templates.data', function(newVal, oldVal) {
+    $scope.imported_print_templates = { data: '' };
+    $scope.$watch('imported_templates.data', function (newVal, oldVal) {
         if (newVal && newVal != oldVal) {
             try {
                 var data = JSON.parse(newVal);
-                angular.forEach(data, function(el,k) {
+                angular.forEach(data, function (el, k) {
                     $scope.templates[k] = {
-                         content : el.content
-                        ,context : el.context
-                        ,cn_content : el.cn_content
-                        ,settings : el.settings
+                        content: el.content
+                        , context: el.context
+                        , cn_content: el.cn_content
+                        , settings: el.settings
+                        , toolbox_settings: el.toolbox_settings
                     };
                 });
                 $scope.saveTemplate();
@@ -421,18 +480,18 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
 
     $scope.rendered_call_number_set = {};
     $scope.rendered_cn_key_by_copy_id = {};
-    $scope.rebuild_cn_set = function() {
-        $timeout(function(){
+    $scope.rebuild_cn_set = function () {
+        $timeout(function () {
             $scope.rendered_call_number_set = {};
             $scope.rendered_cn_key_by_copy_id = {};
             for (var i = 0; i < $scope.preview_scope.copies.length; i++) {
                 var copy = $scope.preview_scope.copies[i];
-                var rendered_cn = document.getElementById('cn_for_copy_'+copy.id);
+                var rendered_cn = document.getElementById('cn_for_copy_' + copy.id);
                 if (rendered_cn && rendered_cn.textContent) {
                     var key = rendered_cn.textContent;
                     if (typeof $scope.rendered_call_number_set[key] == 'undefined') {
                         $scope.rendered_call_number_set[key] = {
-                            value : key
+                            value: key
                         };
                     }
                     $scope.rendered_cn_key_by_copy_id[copy.id] = key;
@@ -442,46 +501,191 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         });
     }
 
-    $scope.$watch('print.cn_template_content', function(newVal, oldVal) {
+    $scope.create_print_label_table = function () {
+        if ($scope.print_label_form.$valid && $scope.print.template_content && $scope.preview_scope) {
+            $scope.preview_scope.label_output_copies = labelOutputRowsFilter($scope.preview_scope.copies, $scope.preview_scope.toolbox_settings);
+            var html = $scope.print.template_content;
+            var d = new Date(); //Added to table ID with 'eg_plt_' to cause $complie on $scope.print.template_content to fire due to template content change.
+            var table = "<table id=\"eg_plt_" + d.getTime().toString() + "_{{$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";
+            table += "<tr>\n";
+            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";
+            table += "<pre class=\"{{col.cls}}\" style=\"border: none; margin-bottom: 0; margin-top: 0; overflow: hidden;\" ng-if=\"col.cls === 'spine'\">\n";
+            table += "{{col.c ? get_cn_for(col.c) : ''}}";
+            table += "</pre>\n";
+            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";
+            table += "{{col.c ? col.c.barcode : ''}}\n";
+            table += "{{col.c ? col.c['call_number.label'] : ''}}\n";
+            table += "{{col.c ? get_bib_for(col.c).author : ''}}\n";
+            table += "{{col.c ? (get_bib_for(col.c).title | wrap:28:'once':'  ') : ''}}\n";
+            table += "</pre>\n";
+            table += "</td>\n"
+            table += "</tr>\n";
+            table += "</table>";
+            var comments = html.match(/\<\!\-\-(?:(?!\-\-\>)(?:.|\s))*\-\-\>\s*/g);
+            html = html.replace(/\<\!\-\-(?:(?!\-\-\>)(?:.|\s))*\-\-\>\s*/g, "");
+            var style = html.match(/\<style[^\>]*\>(?:(?!\<\/style\>)(?:.|\s))*\<\/style\>\s*/gi);
+            var output = (style ? style.join("\n") : "") + (comments ? comments.join("\n") : "") + table;
+            output = output.replace(/\n+/, "\n");
+            $scope.print.template_content = output;
+        }
+    }
+
+    $scope.redraw_label_table = function () {
+        var d = new Date(); //Added to table ID with 'eg_plt_' to cause $complie on $scope.print.template_content to fire due to template content change.
+        var table = "<table id=\"eg_plt_" + d.getTime().toString() + "\"\></table>\n";
+        $scope.print.template_content += table;
+        $scope.create_print_label_table();
+    }
+
+    $scope.$watch('preview_scope.toolbox_settings.page.dimensions.columns',
+        function (newVal, oldVal) {
+            if (newVal && newVal != oldVal && $scope.preview_scope) {
+                $scope.redraw_label_table();
+            }
+        }
+    );
+
+    $scope.$watch('print.cn_template_content', function (newVal, oldVal) {
         if (newVal && newVal != oldVal) {
             $scope.rebuild_cn_set();
         }
     });
 
-    $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_height']", function(newVal, oldVal) {
+    $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_height']", function (newVal, oldVal) {
         if (newVal && newVal != oldVal) {
             $scope.rebuild_cn_set();
         }
     });
 
-    $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_width']", function(newVal, oldVal) {
+    $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_width']", function (newVal, oldVal) {
         if (newVal && newVal != oldVal) {
             $scope.rebuild_cn_set();
         }
     });
 
+    $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.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) {
+        if (newVal && newVal != oldVal && $scope.preview_scope.label_output_copies) {
+            $scope.redraw_label_table();
+        }
+    });
+
+    $scope.$watch("preview_scope.toolbox_settings.mode.selected", function (newVal, oldVal) {
+        if (newVal && newVal != oldVal) {
+            var ts_p = $scope.preview_scope.toolbox_settings.page;
+            if (ts_p.label.set.size === 1) {
+                if (newVal === "spine-pocket") {
+                    ts_p.column_class = ["spine", "pocket"];
+                    ts_p.label.set.size = 2;
+                } else {
+                    ts_p.column_class = ["spine"];
+                }
+            } else {
+                if (newVal === "spine-only") {
+                    for (var i = 0; i < ts_p.label.set.size; i++) {
+                        ts_p.column_class[i] = "spine";
+                    }
+                } else {
+                    ts_p.label.set.size === 2 ? ts_p.column_class = ["spine", "pocket"] : false;
+                }
+            }
+            $scope.redraw_label_table();
+        }
+    });
+
+    $scope.$watch("preview_scope.toolbox_settings.page.label.set.size", function (newVal, oldVal) {
+        if (newVal && newVal != oldVal) {
+            var ts_p = $scope.preview_scope.toolbox_settings.page;
+            if (angular.isNumber(newVal)) {
+                while (ts_p.column_class.length > ts_p.label.set.size) {
+                    ts_p.column_class.splice((ts_p.column_class.length - 1), 1);
+                }
+                while (ts_p.column_class.length < ts_p.label.set.size) {
+                    ts_p.column_class.push("spine");
+                }
+            }
+            $scope.redraw_label_table();
+        }
+    });
+
     $scope.current_tab = 'call_numbers';
-    $scope.set_tab = function(tab) {
+    $scope.set_tab = function (tab) {
         $scope.current_tab = tab;
     }
 
 }])
 
-// 
-.directive('egPrintTemplateOutput', ['$compile',function($compile) {
-    return function(scope, element, attrs) {
+.directive("egPrintLabelColumnBounds", function () {
+    return {
+        link: function (scope, element, attr, ctrl) {
+            function withinBounds(v) {
+                scope.$watch("preview_scope.toolbox_settings.page.dimensions.columns", function (newVal, oldVal) {
+                    ctrl.$setValidity("egWithinPrintColumnBounds", scope.preview_scope.valid_print_label_start_column())
+                });
+                return v;
+            }
+            ctrl.$parsers.push(withinBounds);
+            ctrl.$formatters.push(withinBounds);
+        },
+        require: "ngModel"
+    }
+})
+
+.directive("egPrintLabelRowBounds", function () {
+    return {
+        link: function (scope, element, attr, ctrl) {
+            function withinBounds(v) {
+                scope.$watch("preview_scope.toolbox_settings.page.dimensions.rows", function (newVal, oldVal) {
+                    ctrl.$setValidity("egWithinPrintRowBounds", scope.preview_scope.valid_print_label_start_row());
+                });
+                return v;
+            }
+            ctrl.$parsers.push(withinBounds);
+            ctrl.$formatters.push(withinBounds);
+        },
+        require: "ngModel"
+    }
+})
+
+.directive("egPrintLabelValidCss", function () {
+    return {
+        require: "ngModel",
+        link: function (scope, element, attr, ctrl) {
+            function floatValidation(v) {
+                ctrl.$setValidity("isFloat", v.toString().match(/^\-*(?:^0$|(?:\d+)(?:\.\d{1,})*([a-z]{2}))$/) ? true : false);
+                return v;
+            }
+            ctrl.$parsers.push(floatValidation);
+        }
+    }
+})
+
+.directive("egPrintLabelValidInt", function () {
+    return {
+        require: "ngModel",
+        link: function (scope, element, attr, ctrl) {
+            function intValidation(v) {
+                ctrl.$setValidity("isInteger", v.toString().match(/^\d+$/));
+                return v;
+            }
+            ctrl.$parsers.push(intValidation);
+        }
+    }
+})
+
+.directive('egPrintTemplateOutput', ['$compile', function ($compile) {
+    return function (scope, element, attrs) {
         scope.$watch(
-            function(scope) {
+            function (scope) {
                 return scope.$eval(attrs.content);
             },
-            function(value) {
+            function (value) {
                 // create an isolate scope and copy the print context
                 // data into the new scope.
                 // TODO: see also print security concerns in egHatch
                 var result = element.html(value);
                 var context = scope.$eval(attrs.context);
                 var print_scope = scope.$new(true);
-                angular.forEach(context, function(val, key) {
+                angular.forEach(context, function (val, key) {
                     print_scope[key] = val;
                 })
                 $compile(element.contents())(print_scope);
@@ -490,8 +694,8 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
     };
 }])
 
-.filter('cn_wrap', function() {
-    return function(input, w, h, wrap_type) {
+.filter('cn_wrap', function () {
+    return function (input, w, h, wrap_type) {
         var names;
         var prefix = input[0];
         var callnum = input[1];
@@ -506,11 +710,11 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                on the spine label: subclass letters, subclass numbers, cutter numbers, trailing stuff (date) */
             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;
             var result = callnum.match(patt1);
-            if (result) { 
-                callnum = result.slice(1).join('\t');  
+            if (result) {
+                callnum = result.slice(1).join('\t');
             } else {
                 callnum = callnum.split(/\s+/).join('\t');
-            } 
+            }
 
             /* If result is null, leave callnum alone. Can't parse this malformed call num */
         } else {
@@ -527,17 +731,17 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         /* At this point, the call number pieces are separated by tab characters.  This allows
         *  some space-containing constructs like "v. 1" to appear on one line
         */
-        callnum = callnum.replace(/\t\t/g,'\t');  /* Squeeze out empties */ 
+        callnum = callnum.replace(/\t\t/g, '\t');  /* Squeeze out empties */
         names = callnum.split('\t');
         var j = 0; var tb = [];
         while (j < h) {
-            
+
             /* spine */
             if (j < w) {
 
                 var name = names.shift();
                 if (name) {
-                    name = String( name );
+                    name = String(name);
 
                     /* if the name is greater than the label width... */
                     if (name.length > w) {
@@ -548,20 +752,20 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                             if (name.match(/^\./)) sname[0] = '.' + sname[0];
                             for (var k = 1; k < sname.length; k++) sname[k] = '.' + sname[k];
                             /* and put all but the first one back into the names array */
-                            names = sname.slice(1).concat( names );
+                            names = sname.slice(1).concat(names);
                             /* if the name fragment is still greater than the label width... */
                             if (sname[0].length > w) {
                                 /* then just truncate and throw the rest back into the names array */
-                                tb[j] = sname[0].substr(0,w);
-                                names = [ sname[0].substr(w) ].concat( names );
+                                tb[j] = sname[0].substr(0, w);
+                                names = [sname[0].substr(w)].concat(names);
                             } else {
                                 /* otherwise we're set */
                                 tb[j] = sname[0];
                             }
                         } else {
                             /* if we can't split on periods, then just truncate and throw the rest back into the names array */
-                            tb[j] = name.substr(0,w);
-                            names = [ name.substr(w) ].concat( names );
+                            tb[j] = name.substr(0, w);
+                            names = [name.substr(w)].concat(names);
                         }
                     } else {
                         /* otherwise we're set */
@@ -575,8 +779,58 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
     }
 })
 
-.filter('wrap', function() {
-    return function(input, w, wrap_type, indent) {
+.filter("columnRowRange", function () {
+    return function (i) {
+        var res = [];
+        for (var j = 0; j < i; j++) {
+            res.push(j);
+        }
+        return res;
+    }
+})
+
+//Accepts $scope.preview_scope.copies and $scope.preview_scope.toolbox_settings as its parameters.
+.filter("labelOutputRows", function () {
+    return function (copies, settings) {
+        var cols = [], rows = [];
+        for (var j = 0; j < (settings.page.start_position.row - 1) ; j++) {
+            cols = [];
+            for (var k = 0; k < settings.page.dimensions.columns; k++) {
+                cols.push({ c: null, index: k, cls: getPrintLabelOutputClass(k, settings), styl: getPrintLabelStyle(k, settings) });
+            }
+            rows.push({ columns: cols });
+        }
+        cols = [];
+        for (var j = 0; j < (settings.page.start_position.column - 1) ; j++) {
+            cols.push({ c: null, index: j, cls: getPrintLabelOutputClass(j, settings), styl: getPrintLabelStyle(j, settings) });
+        }
+        var m = cols.length;
+        for (var j = 0; j < copies.length; j++) {
+            for (var n = 0; n < settings.page.label.set.size; n++) {
+                if (m < settings.page.dimensions.columns) {
+                    cols.push({ c: copies[j], index: cols.length, cls: getPrintLabelOutputClass(m, settings), styl: getPrintLabelStyle(m, settings) });
+                    m += 1;
+                }
+                if (m === settings.page.dimensions.columns) {
+                    m = 0;
+                    rows.push({ columns: cols });
+                    cols = [];
+                    n = settings.page.label.set.size;
+                }
+            }
+        }
+        cols.length > 0 ? rows.push({ columns: cols }) : false;
+        if (rows.length > 0) {
+            while ((rows[(rows.length - 1)].columns.length) < settings.page.dimensions.columns) {
+                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) });
+            }
+        }
+        return rows;
+    }
+})
+
+.filter('wrap', function () {
+    return function (input, w, wrap_type, indent) {
         var output;
 
         if (!w) return input;
@@ -589,17 +843,17 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                 if_cant_wrap_then_truncate,
                 idx
         ) {
-            if (idx>10) {
+            if (idx > 10) {
                 console.log('possible infinite recursion, aborting');
                 return '';
             }
             if (String(text).length <= length) {
                 return text;
             } else {
-                var truncated_text = String(text).substr(0,length);
+                var truncated_text = String(text).substr(0, length);
                 var pivot_pos = truncated_text.lastIndexOf(' ');
-                var left_chunk = text.substr(0,pivot_pos).replace(/\s*$/,'');
-                var right_chunk = String(text).substr(pivot_pos+1);
+                var left_chunk = text.substr(0, pivot_pos).replace(/\s*$/, '');
+                var right_chunk = String(text).substr(pivot_pos + 1);
 
                 var wrapped_line;
                 if (left_chunk.length == 0) {
@@ -621,7 +875,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                                     length,
                                     false,
                                     if_cant_wrap_then_truncate,
-                                    idx+1)
+                                    idx + 1)
                                 : right_chunk
                             )
                         )
@@ -631,16 +885,23 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             }
         }
 
-        switch(wrap_type) {
+        switch (wrap_type) {
             case 'once':
-                output = wrap_on_space(input,w,true,false,0);
-            break;
+                output = wrap_on_space(input, w, true, false, 0);
+                break;
             default:
-                output = wrap_on_space(input,w,false,false,0);
-            break;
+                output = wrap_on_space(input, w, false, false, 0);
+                break;
         }
 
         return output;
     }
-})
+});
+
+function getPrintLabelOutputClass(index, settings) {
+    return settings.page.column_class[index % settings.page.label.set.size];
+}
 
+function getPrintLabelStyle(index, settings) {
+    return index > 0 && (index % settings.page.label.set.size === 0) ? settings.page.label.gap.size : "";
+}