Evergreen repo with custom label printing toolbox
authorAdam Bowling <abowling@emeralddata.net>
Wed, 24 Oct 2018 03:57:23 +0000 (23:57 -0400)
committerKathy Lussier <klussier@masslnc.org>
Fri, 16 Nov 2018 18:00:50 +0000 (13:00 -0500)
Signed-off-by: Adam Bowling <abowling@emeralddata.net>
Open-ILS/src/templates/staff/cat/printlabels/t_view.tt2
Open-ILS/web/js/ui/default/staff/cat/printlabels/app.js

index 6ee6085..880eaed 100644 (file)
@@ -1,6 +1,10 @@
 <style>
   /* TODO: move me */
 
+  body {
+
+  }
+
   table.page-break {
   page-break-before: always;
   }
           content="print.template_content"
           context="preview_scope"></div>
     </div>
+    <!-- col -->
 </div>
 
index 3a3ea81..3d48c85 100644 (file)
-/**\r
- * Vol/Copy Editor\r
- */\r
-\r
-angular.module('egPrintLabels',\r
-    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])\r
-\r
-.config(function ($routeProvider, $locationProvider, $compileProvider) {\r
-    $locationProvider.html5Mode(true);\r
-    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export\r
-\r
-    var resolver = {\r
-        delay: ['egStartup', function (egStartup) { return egStartup.go(); }]\r
-    };\r
-\r
-    $routeProvider.when('/cat/printlabels/:dataKey', {\r
-        templateUrl: './cat/printlabels/t_view',\r
-        controller: 'LabelCtrl',\r
-        resolve: resolver\r
-    });\r
-\r
-})\r
-\r
-.factory('itemSvc',\r
-       ['egCore',\r
-function (egCore) {\r
-\r
-    var service = {\r
-        copies: [], // copy barcode search results\r
-        index: 0 // search grid index\r
-    };\r
-\r
-    service.flesh = {\r
-        flesh: 3,\r
-        flesh_fields: {\r
-            acp: ['call_number', 'location', 'status', 'location', 'floating', 'circ_modifier', 'age_protect'],\r
-            acn: ['record', 'prefix', 'suffix'],\r
-            bre: ['simple_record', 'creator', 'editor']\r
-        },\r
-        select: {\r
-            // avoid fleshing MARC on the bre\r
-            // note: don't add simple_record.. not sure why\r
-            bre: ['id', 'tcn_value', 'creator', 'editor'],\r
-        }\r
-    }\r
-\r
-    // resolved with the last received copy\r
-    service.fetch = function (barcode, id, noListDupes) {\r
-        var promise;\r
-\r
-        if (barcode) {\r
-            promise = egCore.pcrud.search('acp',\r
-                { barcode: barcode, deleted: 'f' }, service.flesh);\r
-        } else {\r
-            promise = egCore.pcrud.retrieve('acp', id, service.flesh);\r
-        }\r
-\r
-        var lastRes;\r
-        return promise.then(\r
-            function () { return lastRes },\r
-            null, // error\r
-\r
-            // notify reads the stream of copies, one at a time.\r
-            function (copy) {\r
-\r
-                var flatCopy;\r
-                if (noListDupes) {\r
-                    // use the existing copy if possible\r
-                    flatCopy = service.copies.filter(\r
-                        function (c) { return c.id == copy.id() })[0];\r
-                }\r
-\r
-                if (!flatCopy) {\r
-                    flatCopy = egCore.idl.toHash(copy, true);\r
-                    flatCopy.index = service.index++;\r
-                    service.copies.unshift(flatCopy);\r
-                }\r
-\r
-                return lastRes = {\r
-                    copy: copy,\r
-                    index: flatCopy.index\r
-                }\r
-            }\r
-        );\r
-    }\r
-\r
-    return service;\r
-}])\r
-\r
-/**\r
- * Label controller!\r
- */\r
-.controller('LabelCtrl',\r
-       ['$scope', '$q', '$window', '$routeParams', '$location', '$timeout', 'egCore', 'egNet', 'ngToast', 'itemSvc', 'labelOutputRowsFilter',\r
-function ($scope, $q, $window, $routeParams, $location, $timeout, egCore, egNet, ngToast, itemSvc, labelOutputRowsFilter) {\r
-\r
-    var dataKey = $routeParams.dataKey;\r
-    console.debug('dataKey: ' + dataKey);\r
-\r
-    $scope.print = {\r
-        template_name: 'item_label',\r
-        template_output: '',\r
-        template_context: 'default'\r
-    };\r
-\r
-    var toolbox_settings = {\r
-        feed_option: {\r
-            options: [\r
-                { label: "Continuous", value: "continuous" },\r
-                { label: "Sheet", value: "sheet" },\r
-            ],\r
-            selected: "continuous"\r
-        },\r
-        label_set: {\r
-            margin_between: 0,\r
-            size: 1\r
-        },\r
-        mode: {\r
-            options: [\r
-                { label: "Label 1 Only", value: "spine-only" },\r
-                { label: "Labels 1 & 2", value: "spine-pocket" }\r
-            ],\r
-            selected: "spine-pocket"\r
-        },\r
-        page: {\r
-            column_class: ["spine"],\r
-            dimensions: {\r
-                columns: 2,\r
-                rows: 1\r
-            },\r
-            label: {\r
-                gap: {\r
-                    size: 0\r
-                },\r
-                set: {\r
-                    size: 2\r
-                }\r
-            },\r
-            margins: {\r
-                top: { size: 0, label: "Top" },\r
-                left: { size: 0, label: "Left" },\r
-            },\r
-            space_between_labels: {\r
-                horizontal: { size: 0, label: "Horizontal" },\r
-                vertical: { size: 0, label: "Vertical" }\r
-            },\r
-            start_position: {\r
-                column: 1,\r
-                row: 1\r
-            }\r
-        }\r
-    };\r
-\r
-    if (dataKey && dataKey.length > 0) {\r
-\r
-        egNet.request(\r
-            'open-ils.actor',\r
-            'open-ils.actor.anon_cache.get_value',\r
-            dataKey, 'print-labels-these-copies'\r
-        ).then(function (data) {\r
-\r
-            if (data) {\r
-\r
-                $scope.preview_scope = {\r
-                    'copies': []\r
-                    , 'settings': {}\r
-                    , 'toolbox_settings': toolbox_settings\r
-                    , 'get_cn_for': function (copy) {\r
-                        var key = $scope.rendered_cn_key_by_copy_id[copy.id];\r
-                        if (key) {\r
-                            var manual_cn = $scope.rendered_call_number_set[key];\r
-                            if (manual_cn && manual_cn.value) {\r
-                                return manual_cn.value;\r
-                            } else {\r
-                                return '..';\r
-                            }\r
-                        } else {\r
-                            return '...';\r
-                        }\r
-                    }\r
-                    , 'get_bib_for': function (copy) {\r
-                        return $scope.record_details[copy['call_number.record.id']];\r
-                    }\r
-                    , 'get_cn_prefix': function (copy) {\r
-                        return copy['call_number.prefix.label'];\r
-                    }\r
-                    , 'get_cn_suffix': function (copy) {\r
-                        return copy['call_number.suffix.label'];\r
-                    }\r
-                    , 'get_location_prefix': function (copy) {\r
-                        return copy['location.label_prefix'];\r
-                    }\r
-                    , 'get_location_suffix': function (copy) {\r
-                        return copy['location.label_suffix'];\r
-                    }\r
-                    , 'get_cn_and_location_prefix': function (copy, separator) {\r
-                        var acpl_prefix = copy['location.label_prefix'] || '';\r
-                        var cn_prefix = copy['call_number.prefix.label'] || '';\r
-                        var prefix = acpl_prefix + ' ' + cn_prefix;\r
-                        prefix = prefix.trim();\r
-                        if (separator && prefix != '') { prefix += separator; }\r
-                        return prefix;\r
-                    }\r
-                    , 'get_cn_and_location_suffix': function (copy, separator) {\r
-                        var acpl_suffix = copy['location.label_suffix'] || '';\r
-                        var cn_suffix = copy['call_number.suffix.label'] || '';\r
-                        var suffix = cn_suffix + ' ' + acpl_suffix;\r
-                        suffix = suffix.trim();\r
-                        if (separator && suffix != '') { suffix = separator + suffix; }\r
-                        return suffix;\r
-                    }\r
-                    , 'valid_print_label_start_column': function () {\r
-                        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);\r
-                    }\r
-                    , 'valid_print_label_start_row': function () {\r
-                        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);\r
-                    }\r
-                };\r
-                $scope.record_details = {};\r
-                $scope.org_unit_settings = {};\r
-\r
-                var promises = [];\r
-                $scope.org_unit_setting_list = [\r
-                     'webstaff.cat.label.font.family'\r
-                    , 'webstaff.cat.label.font.size'\r
-                    , 'webstaff.cat.label.font.weight'\r
-                    , 'webstaff.cat.label.inline_css'\r
-                    , 'webstaff.cat.label.left_label.height'\r
-                    , 'webstaff.cat.label.left_label.left_margin'\r
-                    , 'webstaff.cat.label.left_label.width'\r
-                    , 'webstaff.cat.label.right_label.height'\r
-                    , 'webstaff.cat.label.right_label.left_margin'\r
-                    , 'webstaff.cat.label.right_label.width'\r
-                    , 'webstaff.cat.label.call_number_wrap_filter_height'\r
-                    , 'webstaff.cat.label.call_number_wrap_filter_width'\r
-                ];\r
-\r
-                promises.push(\r
-                    egCore.pcrud.search('coust', { name: $scope.org_unit_setting_list }).then(\r
-                         null\r
-                        , null\r
-                        , function (yaous) {\r
-                            $scope.org_unit_settings[yaous.name()] = egCore.idl.toHash(yaous, true);\r
-                        }\r
-                    )\r
-                );\r
-\r
-                promises.push(\r
-                    egCore.org.settings($scope.org_unit_setting_list).then(function (res) {\r
-                        $scope.preview_scope.settings = res;\r
-                        egCore.hatch.getItem('cat.printlabels.last_settings').then(function (last_settings) {\r
-                            if (last_settings) {\r
-                                for (s in last_settings) {\r
-                                    $scope.preview_scope.settings[s] = last_settings[s];\r
-                                }\r
-                            }\r
-                        });\r
-                    })\r
-                );\r
-\r
-                angular.forEach(data.copies, function (copy) {\r
-                    promises.push(\r
-                        itemSvc.fetch(null, copy).then(function (res) {\r
-                            var flat_copy = egCore.idl.toHash(res.copy, true);\r
-                            $scope.preview_scope.copies.push(flat_copy);\r
-                            $scope.record_details[flat_copy['call_number.record.id']] = 1;\r
-                        })\r
-                    )\r
-                });\r
-\r
-                $q.all(promises).then(function () {\r
-\r
-                    var promises2 = [];\r
-                    angular.forEach($scope.record_details, function (el, k, obj) {\r
-                        promises2.push(\r
-                            egNet.request(\r
-                                'open-ils.search',\r
-                                'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',\r
-                                k\r
-                            ).then(function (data) {\r
-                                obj[k] = egCore.idl.toHash(data, true);\r
-                            })\r
-                        );\r
-                    });\r
-\r
-                    $q.all(promises2).then(function () {\r
-                        // today, staff, current_location, etc.\r
-                        egCore.print.fleshPrintScope($scope.preview_scope);\r
-                        $scope.template_changed(); // load the default\r
-                        $scope.rebuild_cn_set();\r
-                    });\r
-\r
-                });\r
-            } else {\r
-                ngToast.danger(egCore.strings.KEY_EXPIRED);\r
-            }\r
-\r
-        });\r
-\r
-    }\r
-\r
-    $scope.fetchTemplates = function (set_default) {\r
-        return egCore.hatch.getItem('cat.printlabels.templates').then(function (t) {\r
-            if (t) {\r
-                $scope.templates = t;\r
-                $scope.template_name_list = Object.keys(t);\r
-                if (set_default) {\r
-                    egCore.hatch.getItem('cat.printlabels.default_template').then(function (d) {\r
-                        if ($scope.template_name_list.indexOf(d, 0) > -1) {\r
-                            $scope.template_name = d;\r
-                        }\r
-                    });\r
-                }\r
-            }\r
-        });\r
-    }\r
-    $scope.fetchTemplates(true);\r
-\r
-    $scope.applyTemplate = function (n) {\r
-        $scope.print.cn_template_content = $scope.templates[n].cn_content;\r
-        $scope.print.template_content = $scope.templates[n].content;\r
-        $scope.print.template_context = $scope.templates[n].context;\r
-        for (var s in $scope.templates[n].settings) {\r
-            $scope.preview_scope.settings[s] = $scope.templates[n].settings[s];\r
-        }\r
-        if ($scope.templates[n].toolbox_settings) {\r
-            $scope.preview_scope.toolbox_settings = $scope.templates[n].toolbox_settings;\r
-            $scope.create_print_label_table();\r
-        }\r
-        egCore.hatch.setItem('cat.printlabels.default_template', n);\r
-        $scope.save_locally();\r
-    }\r
-\r
-    $scope.deleteTemplate = function (n) {\r
-        if (n) {\r
-            delete $scope.templates[n]\r
-            $scope.template_name_list = Object.keys($scope.templates);\r
-            $scope.template_name = '';\r
-            egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);\r
-            $scope.fetchTemplates();\r
-            ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_DELETE);\r
-            egCore.hatch.getItem('cat.printlabels.default_template').then(function (d) {\r
-                if (d && d == n) {\r
-                    egCore.hatch.removeItem('cat.printlabels.default_template');\r
-                }\r
-            });\r
-        }\r
-    }\r
-\r
-    $scope.saveTemplate = function (n) {\r
-        if (n) {\r
-\r
-            $scope.templates[n] = {\r
-                content: $scope.print.template_content\r
-                , context: $scope.print.template_context\r
-                , cn_content: $scope.print.cn_template_content\r
-                , settings: $scope.preview_scope.settings\r
-                , toolbox_settings: $scope.preview_scope.toolbox_settings\r
-            };\r
-            $scope.template_name_list = Object.keys($scope.templates);\r
-\r
-            egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);\r
-            $scope.fetchTemplates();\r
-\r
-            $scope.dirty = false;\r
-        } else {\r
-            // save all templates, as we might do after an import\r
-            egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);\r
-            $scope.fetchTemplates();\r
-        }\r
-        ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_SAVE);\r
-    }\r
-\r
-    $scope.templates = {};\r
-    $scope.imported_templates = { data: '' };\r
-    $scope.template_name = '';\r
-    $scope.template_name_list = [];\r
-\r
-    $scope.print_labels = function () {\r
-        return egCore.print.print({\r
-            context: $scope.print.template_context,\r
-            template: $scope.print.template_name,\r
-            scope: $scope.preview_scope,\r
-        });\r
-    }\r
-\r
-    $scope.template_changed = function () {\r
-        $scope.print.load_failed = false;\r
-        egCore.print.getPrintTemplate('item_label')\r
-        .then(\r
-            function (html) {\r
-                $scope.print.template_content = html;\r
-            },\r
-            function () {\r
-                $scope.print.template_content = '';\r
-                $scope.print.load_failed = true;\r
-            }\r
-        );\r
-        egCore.print.getPrintTemplateContext('item_label')\r
-        .then(function (template_context) {\r
-            $scope.print.template_context = template_context;\r
-        });\r
-        egCore.print.getPrintTemplate('item_label_cn')\r
-        .then(\r
-            function (html) {\r
-                $scope.print.cn_template_content = html;\r
-            },\r
-            function () {\r
-                $scope.print.cn_template_content = '';\r
-                $scope.print.load_failed = true;\r
-            }\r
-        );\r
-        egCore.hatch.getItem('cat.printlabels.last_settings').then(function (s) {\r
-            if (s) {\r
-                $scope.preview_scope.settings = s;\r
-            }\r
-        });\r
-    }\r
-\r
-    $scope.reset_to_default = function () {\r
-        egCore.print.removePrintTemplate(\r
-            'item_label'\r
-        );\r
-        egCore.print.removePrintTemplateContext(\r
-            'item_label'\r
-        );\r
-        egCore.print.removePrintTemplate(\r
-            'item_label_cn'\r
-        );\r
-        egCore.hatch.removeItem('cat.printlabels.last_settings');\r
-        for (s in $scope.preview_scope.settings) {\r
-            $scope.preview_scope.settings[s] = undefined;\r
-        }\r
-        $scope.preview_scope.settings = {};\r
-        egCore.org.settings($scope.org_unit_setting_list).then(function (res) {\r
-            $scope.preview_scope.settings = res;\r
-        });\r
-\r
-        $scope.template_changed();\r
-    }\r
-\r
-    $scope.save_locally = function () {\r
-        egCore.print.storePrintTemplate(\r
-            'item_label',\r
-            $scope.print.template_content\r
-        );\r
-        egCore.print.storePrintTemplateContext(\r
-            'item_label',\r
-            $scope.print.template_context\r
-        );\r
-        egCore.print.storePrintTemplate(\r
-            'item_label_cn',\r
-            $scope.print.cn_template_content\r
-        );\r
-        egCore.hatch.setItem('cat.printlabels.last_settings', $scope.preview_scope.settings);\r
-    }\r
-\r
-    $scope.imported_print_templates = { data: '' };\r
-    $scope.$watch('imported_templates.data', function (newVal, oldVal) {\r
-        if (newVal && newVal != oldVal) {\r
-            try {\r
-                var data = JSON.parse(newVal);\r
-                angular.forEach(data, function (el, k) {\r
-                    $scope.templates[k] = {\r
-                        content: el.content\r
-                        , context: el.context\r
-                        , cn_content: el.cn_content\r
-                        , settings: el.settings\r
-                        , toolbox_settings: el.toolbox_settings\r
-                    };\r
-                });\r
-                $scope.saveTemplate();\r
-                $scope.template_changed(); // refresh\r
-                ngToast.create(egCore.strings.PRINT_TEMPLATES_SUCCESS_IMPORT);\r
-            } catch (E) {\r
-                ngToast.warning(egCore.strings.PRINT_TEMPLATES_FAIL_IMPORT);\r
-            }\r
-        }\r
-    });\r
-\r
-    $scope.rendered_call_number_set = {};\r
-    $scope.rendered_cn_key_by_copy_id = {};\r
-    $scope.rebuild_cn_set = function () {\r
-        $timeout(function () {\r
-            $scope.rendered_call_number_set = {};\r
-            $scope.rendered_cn_key_by_copy_id = {};\r
-            for (var i = 0; i < $scope.preview_scope.copies.length; i++) {\r
-                var copy = $scope.preview_scope.copies[i];\r
-                var rendered_cn = document.getElementById('cn_for_copy_' + copy.id);\r
-                if (rendered_cn && rendered_cn.textContent) {\r
-                    var key = rendered_cn.textContent;\r
-                    if (typeof $scope.rendered_call_number_set[key] == 'undefined') {\r
-                        $scope.rendered_call_number_set[key] = {\r
-                            value: key\r
-                        };\r
-                    }\r
-                    $scope.rendered_cn_key_by_copy_id[copy.id] = key;\r
-                }\r
-            }\r
-            $scope.preview_scope.tickle = Date() + ' ' + Math.random();\r
-        });\r
-    }\r
-\r
-    $scope.create_print_label_table = function () {\r
-        if ($scope.print_label_form.$valid && $scope.print.template_content && $scope.preview_scope) {\r
-            $scope.preview_scope.label_output_copies = labelOutputRowsFilter($scope.preview_scope.copies, $scope.preview_scope.toolbox_settings);\r
-            var html = $scope.print.template_content;\r
-            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.\r
-            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";\r
-            table += "<tr>\n";\r
-            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";\r
-            table += "<pre class=\"{{col.cls}}\" style=\"border: none; margin-bottom: 0; margin-top: 0; overflow: hidden;\" ng-if=\"col.cls === 'spine'\">\n";\r
-            table += "{{col.c ? get_cn_for(col.c) : ''}}";\r
-            table += "</pre>\n";\r
-            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";\r
-            table += "{{col.c ? col.c.barcode : ''}}\n";\r
-            table += "{{col.c ? col.c['call_number.label'] : ''}}\n";\r
-            table += "{{col.c ? get_bib_for(col.c).author : ''}}\n";\r
-            table += "{{col.c ? (get_bib_for(col.c).title | wrap:28:'once':'  ') : ''}}\n";\r
-            table += "</pre>\n";\r
-            table += "</td>\n"\r
-            table += "</tr>\n";\r
-            table += "</table>";\r
-            var comments = html.match(/\<\!\-\-(?:(?!\-\-\>)(?:.|\s))*\-\-\>\s*/g);\r
-            html = html.replace(/\<\!\-\-(?:(?!\-\-\>)(?:.|\s))*\-\-\>\s*/g, "");\r
-            var style = html.match(/\<style[^\>]*\>(?:(?!\<\/style\>)(?:.|\s))*\<\/style\>\s*/gi);\r
-            var output = (style ? style.join("\n") : "") + (comments ? comments.join("\n") : "") + table;\r
-            output = output.replace(/\n+/, "\n");\r
-            $scope.print.template_content = output;\r
-        }\r
-    }\r
-\r
-    $scope.redraw_label_table = function () {\r
-        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.\r
-        var table = "<table id=\"eg_plt_" + d.getTime().toString() + "\"\></table>\n";\r
-        $scope.print.template_content += table;\r
-        $scope.create_print_label_table();\r
-    }\r
-\r
-    $scope.$watch('preview_scope.toolbox_settings.page.dimensions.columns',\r
-        function (newVal, oldVal) {\r
-            if (newVal && newVal != oldVal && $scope.preview_scope) {\r
-                $scope.redraw_label_table();\r
-            }\r
-        }\r
-    );\r
-\r
-    $scope.$watch('print.cn_template_content', function (newVal, oldVal) {\r
-        if (newVal && newVal != oldVal) {\r
-            $scope.rebuild_cn_set();\r
-        }\r
-    });\r
-\r
-    $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_height']", function (newVal, oldVal) {\r
-        if (newVal && newVal != oldVal) {\r
-            $scope.rebuild_cn_set();\r
-        }\r
-    });\r
-\r
-    $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_width']", function (newVal, oldVal) {\r
-        if (newVal && newVal != oldVal) {\r
-            $scope.rebuild_cn_set();\r
-        }\r
-    });\r
-\r
-    $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) {\r
-        if (newVal && newVal != oldVal && $scope.preview_scope.label_output_copies) {\r
-            $scope.redraw_label_table();\r
-        }\r
-    });\r
-\r
-    $scope.$watch("preview_scope.toolbox_settings.mode.selected", function (newVal, oldVal) {\r
-        if (newVal && newVal != oldVal) {\r
-            var ts_p = $scope.preview_scope.toolbox_settings.page;\r
-            if (ts_p.label.set.size === 1) {\r
-                if (newVal === "spine-pocket") {\r
-                    ts_p.column_class = ["spine", "pocket"];\r
-                    ts_p.label.set.size = 2;\r
-                } else {\r
-                    ts_p.column_class = ["spine"];\r
-                }\r
-            } else {\r
-                if (newVal === "spine-only") {\r
-                    for (var i = 0; i < ts_p.label.set.size; i++) {\r
-                        ts_p.column_class[i] = "spine";\r
-                    }\r
-                } else {\r
-                    ts_p.label.set.size === 2 ? ts_p.column_class = ["spine", "pocket"] : false;\r
-                }\r
-            }\r
-            $scope.redraw_label_table();\r
-        }\r
-    });\r
-\r
-    $scope.$watch("preview_scope.toolbox_settings.page.label.set.size", function (newVal, oldVal) {\r
-        if (newVal && newVal != oldVal) {\r
-            var ts_p = $scope.preview_scope.toolbox_settings.page;\r
-            if (angular.isNumber(newVal)) {\r
-                while (ts_p.column_class.length > ts_p.label.set.size) {\r
-                    ts_p.column_class.splice((ts_p.column_class.length - 1), 1);\r
-                }\r
-                while (ts_p.column_class.length < ts_p.label.set.size) {\r
-                    ts_p.column_class.push("spine");\r
-                }\r
-            }\r
-            $scope.redraw_label_table();\r
-        }\r
-    });\r
-\r
-    $scope.current_tab = 'call_numbers';\r
-    $scope.set_tab = function (tab) {\r
-        $scope.current_tab = tab;\r
-    }\r
-\r
-}])\r
-\r
-.directive("egPrintLabelColumnBounds", function () {\r
-    return {\r
-        link: function (scope, element, attr, ctrl) {\r
-            function withinBounds(v) {\r
-                scope.$watch("preview_scope.toolbox_settings.page.dimensions.columns", function (newVal, oldVal) {\r
-                    ctrl.$setValidity("egWithinPrintColumnBounds", scope.preview_scope.valid_print_label_start_column())\r
-                });\r
-                return v;\r
-            }\r
-            ctrl.$parsers.push(withinBounds);\r
-            ctrl.$formatters.push(withinBounds);\r
-        },\r
-        require: "ngModel"\r
-    }\r
-})\r
-\r
-.directive("egPrintLabelRowBounds", function () {\r
-    return {\r
-        link: function (scope, element, attr, ctrl) {\r
-            function withinBounds(v) {\r
-                scope.$watch("preview_scope.toolbox_settings.page.dimensions.rows", function (newVal, oldVal) {\r
-                    ctrl.$setValidity("egWithinPrintRowBounds", scope.preview_scope.valid_print_label_start_row());\r
-                });\r
-                return v;\r
-            }\r
-            ctrl.$parsers.push(withinBounds);\r
-            ctrl.$formatters.push(withinBounds);\r
-        },\r
-        require: "ngModel"\r
-    }\r
-})\r
-\r
-.directive("egPrintLabelValidCss", function () {\r
-    return {\r
-        require: "ngModel",\r
-        link: function (scope, element, attr, ctrl) {\r
-            function floatValidation(v) {\r
-                ctrl.$setValidity("isFloat", v.toString().match(/^\-*(?:^0$|(?:\d+)(?:\.\d{1,})*([a-z]{2}))$/) ? true : false);\r
-                return v;\r
-            }\r
-            ctrl.$parsers.push(floatValidation);\r
-        }\r
-    }\r
-})\r
-\r
-.directive("egPrintLabelValidInt", function () {\r
-    return {\r
-        require: "ngModel",\r
-        link: function (scope, element, attr, ctrl) {\r
-            function intValidation(v) {\r
-                ctrl.$setValidity("isInteger", v.toString().match(/^\d+$/));\r
-                return v;\r
-            }\r
-            ctrl.$parsers.push(intValidation);\r
-        }\r
-    }\r
-})\r
-\r
-.directive('egPrintTemplateOutput', ['$compile', function ($compile) {\r
-    return function (scope, element, attrs) {\r
-        scope.$watch(\r
-            function (scope) {\r
-                return scope.$eval(attrs.content);\r
-            },\r
-            function (value) {\r
-                // create an isolate scope and copy the print context\r
-                // data into the new scope.\r
-                // TODO: see also print security concerns in egHatch\r
-                var result = element.html(value);\r
-                var context = scope.$eval(attrs.context);\r
-                var print_scope = scope.$new(true);\r
-                angular.forEach(context, function (val, key) {\r
-                    print_scope[key] = val;\r
-                })\r
-                $compile(element.contents())(print_scope);\r
-            }\r
-        );\r
-    };\r
-}])\r
-\r
-.filter('cn_wrap', function () {\r
-    return function (input, w, h, wrap_type) {\r
-        var names;\r
-        var prefix = input[0];\r
-        var callnum = input[1];\r
-        var suffix = input[2];\r
-\r
-        if (!w) { w = 8; }\r
-        if (!h) { h = 9; }\r
-\r
-        /* handle spine labels differently if using LC */\r
-        if (wrap_type == 'lc' || wrap_type == 3) {\r
-            /* Establish a pattern where every return value should be isolated on its own line \r
-               on the spine label: subclass letters, subclass numbers, cutter numbers, trailing stuff (date) */\r
-            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;\r
-            var result = callnum.match(patt1);\r
-            if (result) {\r
-                callnum = result.slice(1).join('\t');\r
-            } else {\r
-                callnum = callnum.split(/\s+/).join('\t');\r
-            }\r
-\r
-            /* If result is null, leave callnum alone. Can't parse this malformed call num */\r
-        } else {\r
-            callnum = callnum.split(/\s+/).join('\t');\r
-        }\r
-\r
-        if (prefix) {\r
-            callnum = prefix + '\t' + callnum;\r
-        }\r
-        if (suffix) {\r
-            callnum += '\t' + suffix;\r
-        }\r
-\r
-        /* At this point, the call number pieces are separated by tab characters.  This allows\r
-        *  some space-containing constructs like "v. 1" to appear on one line\r
-        */\r
-        callnum = callnum.replace(/\t\t/g, '\t');  /* Squeeze out empties */\r
-        names = callnum.split('\t');\r
-        var j = 0; var tb = [];\r
-        while (j < h) {\r
-\r
-            /* spine */\r
-            if (j < w) {\r
-\r
-                var name = names.shift();\r
-                if (name) {\r
-                    name = String(name);\r
-\r
-                    /* if the name is greater than the label width... */\r
-                    if (name.length > w) {\r
-                        /* then try to split it on periods */\r
-                        var sname = name.split(/\./);\r
-                        if (sname.length > 1) {\r
-                            /* if we can, then put the periods back in on each splitted element */\r
-                            if (name.match(/^\./)) sname[0] = '.' + sname[0];\r
-                            for (var k = 1; k < sname.length; k++) sname[k] = '.' + sname[k];\r
-                            /* and put all but the first one back into the names array */\r
-                            names = sname.slice(1).concat(names);\r
-                            /* if the name fragment is still greater than the label width... */\r
-                            if (sname[0].length > w) {\r
-                                /* then just truncate and throw the rest back into the names array */\r
-                                tb[j] = sname[0].substr(0, w);\r
-                                names = [sname[0].substr(w)].concat(names);\r
-                            } else {\r
-                                /* otherwise we're set */\r
-                                tb[j] = sname[0];\r
-                            }\r
-                        } else {\r
-                            /* if we can't split on periods, then just truncate and throw the rest back into the names array */\r
-                            tb[j] = name.substr(0, w);\r
-                            names = [name.substr(w)].concat(names);\r
-                        }\r
-                    } else {\r
-                        /* otherwise we're set */\r
-                        tb[j] = name;\r
-                    }\r
-                }\r
-            }\r
-            j++;\r
-        }\r
-        return tb.join('\n');\r
-    }\r
-})\r
-\r
-.filter("columnRowRange", function () {\r
-    return function (i) {\r
-        var res = [];\r
-        for (var j = 0; j < i; j++) {\r
-            res.push(j);\r
-        }\r
-        return res;\r
-    }\r
-})\r
-\r
-//Accepts $scope.preview_scope.copies and $scope.preview_scope.toolbox_settings as its parameters.\r
-.filter("labelOutputRows", function () {\r
-    return function (copies, settings) {\r
-        var cols = [], rows = [];\r
-        for (var j = 0; j < (settings.page.start_position.row - 1) ; j++) {\r
-            cols = [];\r
-            for (var k = 0; k < settings.page.dimensions.columns; k++) {\r
-                cols.push({ c: null, index: k, cls: getPrintLabelOutputClass(k, settings), styl: getPrintLabelStyle(k, settings) });\r
-            }\r
-            rows.push({ columns: cols });\r
-        }\r
-        cols = [];\r
-        for (var j = 0; j < (settings.page.start_position.column - 1) ; j++) {\r
-            cols.push({ c: null, index: j, cls: getPrintLabelOutputClass(j, settings), styl: getPrintLabelStyle(j, settings) });\r
-        }\r
-        var m = cols.length;\r
-        for (var j = 0; j < copies.length; j++) {\r
-            for (var n = 0; n < settings.page.label.set.size; n++) {\r
-                if (m < settings.page.dimensions.columns) {\r
-                    cols.push({ c: copies[j], index: cols.length, cls: getPrintLabelOutputClass(m, settings), styl: getPrintLabelStyle(m, settings) });\r
-                    m += 1;\r
-                }\r
-                if (m === settings.page.dimensions.columns) {\r
-                    m = 0;\r
-                    rows.push({ columns: cols });\r
-                    cols = [];\r
-                    n = settings.page.label.set.size;\r
-                }\r
-            }\r
-        }\r
-        cols.length > 0 ? rows.push({ columns: cols }) : false;\r
-        if (rows.length > 0) {\r
-            while ((rows[(rows.length - 1)].columns.length) < settings.page.dimensions.columns) {\r
-                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) });\r
-            }\r
-        }\r
-        return rows;\r
-    }\r
-})\r
-\r
-.filter('wrap', function () {\r
-    return function (input, w, wrap_type, indent) {\r
-        var output;\r
-\r
-        if (!w) return input;\r
-        if (!indent) indent = '';\r
-\r
-        function wrap_on_space(\r
-                text,\r
-                length,\r
-                wrap_just_once,\r
-                if_cant_wrap_then_truncate,\r
-                idx\r
-        ) {\r
-            if (idx > 10) {\r
-                console.log('possible infinite recursion, aborting');\r
-                return '';\r
-            }\r
-            if (String(text).length <= length) {\r
-                return text;\r
-            } else {\r
-                var truncated_text = String(text).substr(0, length);\r
-                var pivot_pos = truncated_text.lastIndexOf(' ');\r
-                var left_chunk = text.substr(0, pivot_pos).replace(/\s*$/, '');\r
-                var right_chunk = String(text).substr(pivot_pos + 1);\r
-\r
-                var wrapped_line;\r
-                if (left_chunk.length == 0) {\r
-                    if (if_cant_wrap_then_truncate) {\r
-                        wrapped_line = truncated_text;\r
-                    } else {\r
-                        wrapped_line = text;\r
-                    }\r
-                } else {\r
-                    wrapped_line =\r
-                        left_chunk + '\n'\r
-                        + indent + (\r
-                            wrap_just_once\r
-                            ? right_chunk\r
-                            : (\r
-                                right_chunk.length > length\r
-                                ? wrap_on_space(\r
-                                    right_chunk,\r
-                                    length,\r
-                                    false,\r
-                                    if_cant_wrap_then_truncate,\r
-                                    idx + 1)\r
-                                : right_chunk\r
-                            )\r
-                        )\r
-                    ;\r
-                }\r
-                return wrapped_line;\r
-            }\r
-        }\r
-\r
-        switch (wrap_type) {\r
-            case 'once':\r
-                output = wrap_on_space(input, w, true, false, 0);\r
-                break;\r
-            default:\r
-                output = wrap_on_space(input, w, false, false, 0);\r
-                break;\r
-        }\r
-\r
-        return output;\r
-    }\r
-});\r
-\r
-function getPrintLabelOutputClass(index, settings) {\r
-    return settings.page.column_class[index % settings.page.label.set.size];\r
-}\r
-\r
-function getPrintLabelStyle(index, settings) {\r
-    return index > 0 && (index % settings.page.label.set.size === 0) ? settings.page.label.gap.size : "";\r
+/**
+ * Vol/Copy Editor
+ */
+
+angular.module('egPrintLabels',
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
+
+.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(); }]
+    };
+
+    $routeProvider.when('/cat/printlabels/:dataKey', {
+        templateUrl: './cat/printlabels/t_view',
+        controller: 'LabelCtrl',
+        resolve: resolver
+    });
+
+})
+
+.factory('itemSvc',
+       ['egCore',
+function (egCore) {
+
+    var service = {
+        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'],
+            acn: ['record', 'prefix', 'suffix'],
+            bre: ['simple_record', 'creator', 'editor']
+        },
+        select: {
+            // avoid fleshing MARC on the bre
+            // note: don't add simple_record.. not sure why
+            bre: ['id', 'tcn_value', 'creator', 'editor'],
+        }
+    }
+
+    // resolved with the last received copy
+    service.fetch = function (barcode, id, noListDupes) {
+        var promise;
+
+        if (barcode) {
+            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 },
+            null, // error
+
+            // notify reads the stream of copies, one at a time.
+            function (copy) {
+
+                var flatCopy;
+                if (noListDupes) {
+                    // use the existing copy if possible
+                    flatCopy = service.copies.filter(
+                        function (c) { return c.id == copy.id() })[0];
+                }
+
+                if (!flatCopy) {
+                    flatCopy = egCore.idl.toHash(copy, true);
+                    flatCopy.index = service.index++;
+                    service.copies.unshift(flatCopy);
+                }
+
+                return lastRes = {
+                    copy: copy,
+                    index: flatCopy.index
+                }
+            }
+        );
+    }
+
+    return service;
+}])
+
+/**
+ * Label controller!
+ */
+.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'
+    };
+
+    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) {
+
+        egNet.request(
+            'open-ils.actor',
+            'open-ils.actor.anon_cache.get_value',
+            dataKey, 'print-labels-these-copies'
+        ).then(function (data) {
+
+            if (data) {
+
+                $scope.preview_scope = {
+                    '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];
+                            if (manual_cn && manual_cn.value) {
+                                return manual_cn.value;
+                            } else {
+                                return '..';
+                            }
+                        } else {
+                            return '...';
+                        }
+                    }
+                    , 'get_bib_for': function (copy) {
+                        return $scope.record_details[copy['call_number.record.id']];
+                    }
+                    , 'get_cn_prefix': function (copy) {
+                        return copy['call_number.prefix.label'];
+                    }
+                    , 'get_cn_suffix': function (copy) {
+                        return copy['call_number.suffix.label'];
+                    }
+                    , 'get_location_prefix': function (copy) {
+                        return copy['location.label_prefix'];
+                    }
+                    , 'get_location_suffix': function (copy) {
+                        return copy['location.label_suffix'];
+                    }
+                    , '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;
+                        prefix = prefix.trim();
+                        if (separator && prefix != '') { prefix += separator; }
+                        return prefix;
+                    }
+                    , '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;
+                        suffix = suffix.trim();
+                        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 = {};
+
+                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'
+                ];
+
+                promises.push(
+                    egCore.pcrud.search('coust', { name: $scope.org_unit_setting_list }).then(
+                         null
+                        , 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) {
+                        $scope.preview_scope.settings = res;
+                        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];
+                                }
+                            }
+                        });
+                    })
+                );
+
+                angular.forEach(data.copies, function (copy) {
+                    promises.push(
+                        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;
+                        })
+                    )
+                });
+
+                $q.all(promises).then(function () {
+
+                    var promises2 = [];
+                    angular.forEach($scope.record_details, function (el, k, obj) {
+                        promises2.push(
+                            egNet.request(
+                                'open-ils.search',
+                                'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',
+                                k
+                            ).then(function (data) {
+                                obj[k] = egCore.idl.toHash(data, true);
+                            })
+                        );
+                    });
+
+                    $q.all(promises2).then(function () {
+                        // today, staff, current_location, etc.
+                        egCore.print.fleshPrintScope($scope.preview_scope);
+                        $scope.template_changed(); // load the default
+                        $scope.rebuild_cn_set();
+                    });
+
+                });
+            } else {
+                ngToast.danger(egCore.strings.KEY_EXPIRED);
+            }
+
+        });
+
+    }
+
+    $scope.fetchTemplates = function (set_default) {
+        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) {
+                            $scope.template_name = d;
+                        }
+                    });
+                }
+            }
+        });
+    }
+    $scope.fetchTemplates(true);
+
+    $scope.applyTemplate = function (n) {
+        $scope.print.cn_template_content = $scope.templates[n].cn_content;
+        $scope.print.template_content = $scope.templates[n].content;
+        $scope.print.template_context = $scope.templates[n].context;
+        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();
+    }
+
+    $scope.deleteTemplate = function (n) {
+        if (n) {
+            delete $scope.templates[n]
+            $scope.template_name_list = Object.keys($scope.templates);
+            $scope.template_name = '';
+            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) {
+                if (d && d == n) {
+                    egCore.hatch.removeItem('cat.printlabels.default_template');
+                }
+            });
+        }
+    }
+
+    $scope.saveTemplate = function (n) {
+        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
+                , toolbox_settings: $scope.preview_scope.toolbox_settings
+            };
+            $scope.template_name_list = Object.keys($scope.templates);
+
+            egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
+            $scope.fetchTemplates();
+
+            $scope.dirty = false;
+        } else {
+            // save all templates, as we might do after an import
+            egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
+            $scope.fetchTemplates();
+        }
+        ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_SAVE);
+    }
+
+    $scope.templates = {};
+    $scope.imported_templates = { data: '' };
+    $scope.template_name = '';
+    $scope.template_name_list = [];
+
+    $scope.print_labels = function () {
+        return egCore.print.print({
+            context: $scope.print.template_context,
+            template: $scope.print.template_name,
+            scope: $scope.preview_scope,
+        });
+    }
+
+    $scope.template_changed = function () {
+        $scope.print.load_failed = false;
+        egCore.print.getPrintTemplate('item_label')
+        .then(
+            function (html) {
+                $scope.print.template_content = html;
+            },
+            function () {
+                $scope.print.template_content = '';
+                $scope.print.load_failed = true;
+            }
+        );
+        egCore.print.getPrintTemplateContext('item_label')
+        .then(function (template_context) {
+            $scope.print.template_context = template_context;
+        });
+        egCore.print.getPrintTemplate('item_label_cn')
+        .then(
+            function (html) {
+                $scope.print.cn_template_content = html;
+            },
+            function () {
+                $scope.print.cn_template_content = '';
+                $scope.print.load_failed = true;
+            }
+        );
+        egCore.hatch.getItem('cat.printlabels.last_settings').then(function (s) {
+            if (s) {
+                $scope.preview_scope.settings = s;
+            }
+        });
+    }
+
+    $scope.reset_to_default = function () {
+        egCore.print.removePrintTemplate(
+            'item_label'
+        );
+        egCore.print.removePrintTemplateContext(
+            'item_label'
+        );
+        egCore.print.removePrintTemplate(
+            'item_label_cn'
+        );
+        egCore.hatch.removeItem('cat.printlabels.last_settings');
+        for (s in $scope.preview_scope.settings) {
+            $scope.preview_scope.settings[s] = undefined;
+        }
+        $scope.preview_scope.settings = {};
+        egCore.org.settings($scope.org_unit_setting_list).then(function (res) {
+            $scope.preview_scope.settings = res;
+        });
+
+        $scope.template_changed();
+    }
+
+    $scope.save_locally = function () {
+        egCore.print.storePrintTemplate(
+            'item_label',
+            $scope.print.template_content
+        );
+        egCore.print.storePrintTemplateContext(
+            'item_label',
+            $scope.print.template_context
+        );
+        egCore.print.storePrintTemplate(
+            'item_label_cn',
+            $scope.print.cn_template_content
+        );
+        egCore.hatch.setItem('cat.printlabels.last_settings', $scope.preview_scope.settings);
+    }
+
+    $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) {
+                    $scope.templates[k] = {
+                        content: el.content
+                        , context: el.context
+                        , cn_content: el.cn_content
+                        , settings: el.settings
+                        , toolbox_settings: el.toolbox_settings
+                    };
+                });
+                $scope.saveTemplate();
+                $scope.template_changed(); // refresh
+                ngToast.create(egCore.strings.PRINT_TEMPLATES_SUCCESS_IMPORT);
+            } catch (E) {
+                ngToast.warning(egCore.strings.PRINT_TEMPLATES_FAIL_IMPORT);
+            }
+        }
+    });
+
+    $scope.rendered_call_number_set = {};
+    $scope.rendered_cn_key_by_copy_id = {};
+    $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);
+                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
+                        };
+                    }
+                    $scope.rendered_cn_key_by_copy_id[copy.id] = key;
+                }
+            }
+            $scope.preview_scope.tickle = Date() + ' ' + Math.random();
+        });
+    }
+
+    $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) {
+        if (newVal && newVal != oldVal) {
+            $scope.rebuild_cn_set();
+        }
+    });
+
+    $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.current_tab = tab;
+    }
+
+}])
+
+.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) {
+                return scope.$eval(attrs.content);
+            },
+            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) {
+                    print_scope[key] = val;
+                })
+                $compile(element.contents())(print_scope);
+            }
+        );
+    };
+}])
+
+.filter('cn_wrap', function () {
+    return function (input, w, h, wrap_type) {
+        var names;
+        var prefix = input[0];
+        var callnum = input[1];
+        var suffix = input[2];
+
+        if (!w) { w = 8; }
+        if (!h) { h = 9; }
+
+        /* handle spine labels differently if using LC */
+        if (wrap_type == 'lc' || wrap_type == 3) {
+            /* Establish a pattern where every return value should be isolated on its own line 
+               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');
+            } else {
+                callnum = callnum.split(/\s+/).join('\t');
+            }
+
+            /* If result is null, leave callnum alone. Can't parse this malformed call num */
+        } else {
+            callnum = callnum.split(/\s+/).join('\t');
+        }
+
+        if (prefix) {
+            callnum = prefix + '\t' + callnum;
+        }
+        if (suffix) {
+            callnum += '\t' + suffix;
+        }
+
+        /* 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 */
+        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);
+
+                    /* if the name is greater than the label width... */
+                    if (name.length > w) {
+                        /* then try to split it on periods */
+                        var sname = name.split(/\./);
+                        if (sname.length > 1) {
+                            /* if we can, then put the periods back in on each splitted element */
+                            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);
+                            /* 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);
+                            } 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);
+                        }
+                    } else {
+                        /* otherwise we're set */
+                        tb[j] = name;
+                    }
+                }
+            }
+            j++;
+        }
+        return tb.join('\n');
+    }
+})
+
+.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;
+        if (!indent) indent = '';
+
+        function wrap_on_space(
+                text,
+                length,
+                wrap_just_once,
+                if_cant_wrap_then_truncate,
+                idx
+        ) {
+            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 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 wrapped_line;
+                if (left_chunk.length == 0) {
+                    if (if_cant_wrap_then_truncate) {
+                        wrapped_line = truncated_text;
+                    } else {
+                        wrapped_line = text;
+                    }
+                } else {
+                    wrapped_line =
+                        left_chunk + '\n'
+                        + indent + (
+                            wrap_just_once
+                            ? right_chunk
+                            : (
+                                right_chunk.length > length
+                                ? wrap_on_space(
+                                    right_chunk,
+                                    length,
+                                    false,
+                                    if_cant_wrap_then_truncate,
+                                    idx + 1)
+                                : right_chunk
+                            )
+                        )
+                    ;
+                }
+                return wrapped_line;
+            }
+        }
+
+        switch (wrap_type) {
+            case 'once':
+                output = wrap_on_space(input, w, true, false, 0);
+                break;
+            default:
+                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 : "";
 }
\ No newline at end of file