webstaff: toward label printing
authorJason Etheridge <jason@esilibrary.com>
Mon, 6 Mar 2017 16:02:33 +0000 (11:02 -0500)
committerJason Etheridge <jason@esilibrary.com>
Fri, 14 Apr 2017 03:35:00 +0000 (23:35 -0400)
...better stock template for labels, and a | wrap filter
...pull in some Library Settings for Print Labels
...Reset to Default button for templates for both receipt and item print labels
...toward tabs for Print Label interface
...template management for print labels
...bundle the Call Number Template in with saved templates
...manual editing of cn's for print labels
...And affixes in the stock CN template.
...Settings tab for print labels

Signed-off-by: Jason Etheridge <jason@esilibrary.com>
Open-ILS/src/templates/staff/admin/workstation/t_print_templates.tt2
Open-ILS/src/templates/staff/cat/printlabels/index.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/cat/printlabels/t_view.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
Open-ILS/src/templates/staff/share/print_templates/t_item_label.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/share/print_templates/t_item_label_cn.tt2 [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
Open-ILS/web/js/ui/default/staff/cat/printlabels/app.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/services/print.js

index b049c56..064bfff 100644 (file)
@@ -46,6 +46,7 @@
     </div>
   </div>
   <div class="col-md-7">
+    <button class="btn btn-default pull-left" ng-click="reset_to_default()">[% l('Reset to Default') %]</button>
     <button class="btn btn-default pull-left" ng-click="save_locally()">[% l('Save Locally') %]</button>
     <div class="btn-group pull-right">
       <span class="btn btn-default btn-file">
diff --git a/Open-ILS/src/templates/staff/cat/printlabels/index.tt2 b/Open-ILS/src/templates/staff/cat/printlabels/index.tt2
new file mode 100644 (file)
index 0000000..ac89fc5
--- /dev/null
@@ -0,0 +1,30 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Print Item Labels"); 
+  ctx.page_app = "egPrintLabels";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/file.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/eframe.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/printlabels/app.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/record.js"></script>
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+    s.KEY_EXPIRED = "[% l('Key expired, please close this window; it no longer remembers which items you are printing labels for.') %]";
+    s.PRINT_LABEL_TEMPLATE_SUCCESS_SAVE = "[% l('Saved print label template(s)') %]";
+    s.PRINT_LABEL_TEMPLATE_SUCCESS_DELETE = "[% l('Deleted print label template') %]";
+}]);
+</script>
+[% END %]
+
+<style>
+</style>
+
+<div ng-view></div>
+
+[% END %]
+
+
diff --git a/Open-ILS/src/templates/staff/cat/printlabels/t_view.tt2 b/Open-ILS/src/templates/staff/cat/printlabels/t_view.tt2
new file mode 100644 (file)
index 0000000..8829e85
--- /dev/null
@@ -0,0 +1,232 @@
+<style>
+  /* TODO: move me */
+  .print-template-text {
+    height: 36em;
+    width: 100%;
+  }
+  .cn-template-text {
+    height: 12em;
+    width: 100%;
+  }
+</style>
+
+<h2>[% l('Print Item Labels') %]</h2>
+
+<div class="row bg-info">
+    <div class="col-md-6">
+        <div class="row">
+            <div class="col-md-1">
+                <span class="h4">[% l('Template') %]</span>
+            </div>
+            <div class="col-md-5">
+                <eg-basic-combo-box list="template_name_list" selected="template_name"></eg-basic-combo-box>
+            </div>
+            <div class="col-md-1">
+                <button class="btn btn-default" ng-click="applyTemplate(template_name)">[% l('Apply') %]</button>
+            </div>
+            <div class="col-md-1">
+                <span class="h4">[% l('Printer') %]</span>
+            </div>
+            <div class="col-md-4">
+                <select class="form-control" ng-model="print.template_context">
+                  <option value="default">[% l('Default') %]</option>
+                  <option value="receipt">[% l('Receipt') %]</option>
+                  <option value="label">[% l('Label') %]</option>
+                  <option value="mail">[% l('Mail') %]</option>
+                  <option value="offline">[% l('Offline') %]</option>
+                </select>
+            </div>
+        </div>
+    </div>
+    <div class="col-md-2">
+        <div class="btn-group">
+            <button class="btn btn-default" ng-click="saveTemplate(template_name)">[% l('Save') %]</button>
+            <button class="btn btn-default" ng-click="deleteTemplate(template_name)">[% l('Delete') %]</button>
+        </div>
+    </div>
+    <div class="col-md-3">
+        <div class="btn-group">
+            <span class="btn btn-default btn-file">
+                [% l('Import') %]
+                <input type="file" eg-file-reader container="imported_templates.data">
+            </span>
+            <label class="btn btn-default"
+                eg-json-exporter container="templates"
+                default-file-name="'[% l('exported_label_templates.json') %]'">
+                [% l('Export') %]
+            </label>
+            <label class="btn btn-default" ng-click="reset_to_default()">[% l('Default') %]</button>
+        </div>
+    </div>
+    <div class="col-md-1 pull-right">
+        <button class="btn btn-default" ng-click="print_labels()">[% l('Print') %]</button>
+    </div>
+</div>
+
+<hr/>
+
+<div class="row">
+  <div class="col-md-5">
+    <ul class="nav nav-tabs">
+        <li ng-class="{active : current_tab == 'cn_template'}">
+            <a ng-click="set_tab('cn_template')">
+                [% l('Call Number Template') %]
+            </a>
+        </li>
+        <li ng-class="{active : current_tab == 'call_numbers'}">
+            <a ng-click="set_tab('call_numbers')">
+                [% l('Call Numbers') %]
+            </a>
+        </li>
+        <li ng-class="{active : current_tab == 'settings'}">
+            <a ng-click="set_tab('settings')">
+                [% l('Settings') %]
+            </a>
+        </li>
+        <li ng-class="{active : current_tab == 'template'}">
+            <a ng-click="set_tab('template')">
+                [% l('Label Template') %]
+            </a>
+        </li>
+    </ul>
+    <div class="tab-content">
+        <div class="tab-pane active">
+            <div ng-show="current_tab == 'cn_template'">
+                <h4>
+                    [% l('Call Number Preview') %]
+                </h4>
+                <div eg-print-template-output ng-show="true"
+                    content="print.cn_template_content"
+                    context="{ copy : preview_scope.copies[0] }"></div>
+                <h4>
+                    [% l('Call Number Template') %]
+                </h4>
+                <div><span>[% l('Changes here will wipe out manual changes in the Call Numbers tab.') %]<br/></span></div>
+                <textarea ng-model="print.cn_template_content" class="print-template-text">
+                </textarea>
+                <div ng-repeat="copy in preview_scope.copies">
+                    <div id="cn_for_copy_{{copy.id}}" eg-print-template-output ng-show="false"
+                        content="print.cn_template_content"
+                        context="{ copy : copy }"></div>
+                </div>
+            </div>
+            <div ng-show="current_tab == 'call_numbers'">
+                <h4>
+                    [% l('Formatted Call Numbers') %]
+                </h4>
+                <div><span>[% l('Manual adjustments may be made here. These do not get saved with templates.') %]<br/></span></div>
+                <div ng-repeat="cn in rendered_call_number_set">
+                    <textarea ng-model="cn.value" class="cn-template-text">
+                    </textarea>
+                </div>
+            </div>
+            <div ng-show="current_tab == 'settings'">
+                <div><span>[% l('These settings do get saved with templates and will override corresponding Library Settings.') %]<br/></span></div>
+                <!-- FIXME: pull these labels and descriptions from the IDL -->
+                <div class="row" style="border-top: solid black">
+                    <div class="col-md-6" style="font-weight: bold">[% l('Spine and pocket label font family') %]</div>
+                    <div class="col-md-6"><input type="text" ng-model="preview_scope.settings['cat.label.font.family']"></input></div>
+                </div>
+
+                <div class="row">
+                    <div>[% l('Set the preferred font family for spine and pocket labels. You can specify a list of fonts, separated by commas, in order of preference; the system will use the first font it finds with a matching name. For example, "Arial, Helvetica, serif".') %]</div>
+                </div>
+
+                <div class="row" style="border-top: solid black">
+                    <div class="col-md-6" style="font-weight: bold">[% l('Spine and pocket label font size') %]</div>
+                    <div class="col-md-6"><input type="text" ng-model="preview_scope.settings['cat.label.font.size']"></input></div>
+                </div>
+
+                <div class="row">
+                    <div>[% l('Set the default font size for spine and pocket labels') %]</div>
+                </div>
+
+                <div class="row" style="border-top: solid black">
+                    <div class="col-md-6" style="font-weight: bold">[% l('Spine and pocket label font weight') %]</div>
+                    <div class="col-md-6"><input type="text" ng-model="preview_scope.settings['cat.label.font.weight']"></input></div>
+                </div>
+
+                <div class="row">
+                    <div>[% l('Set the preferred font weight for spine and pocket labels. You can specify "normal", "bold", "bolder", or "lighter".') %]</div>
+                </div>
+
+                <div class="row" style="border-top: solid black">
+                    <div class="col-md-6" style="font-weight: bold">[% l('Spine label maximum lines') %]</div>
+                    <div class="col-md-6"><input type="text" ng-model="preview_scope.settings['cat.spine.line.height']"></input></div>
+                </div>
+
+                <div class="row">
+                    <div>[% l('Set the default maximum number of lines for spine labels.') %]</div>
+                </div>
+
+                <div class="row" style="border-top: solid black">
+                    <div class="col-md-6" style="font-weight: bold">[% l('Spine label left margin') %]</div>
+                    <div class="col-md-6"><input type="text" ng-model="preview_scope.settings['cat.spine.line.margin']"></input></div>
+                </div>
+
+                <div class="row">
+                    <div>[% l('Set the left margin for spine labels in number of characters.') %]</div>
+                </div>
+
+                <div class="row" style="border-top: solid black">
+                    <div class="col-md-6" style="font-weight: bold">[% l('Spine label line width') %]</div>
+                    <div class="col-md-6"><input type="text" ng-model="preview_scope.settings['cat.spine.line.width']"></input></div>
+                </div>
+
+                <div class="row">
+                    <div>[% l('Set the default line width for spine labels in number of characters. This specifies the boundary at which lines must be wrapped.') %]</div>
+                </div>
+
+                <div class="row" style="border-top: solid black">
+                    <div class="col-md-6" style="font-weight: bold">[% l('Pocket label maximum lines') %]</div>
+                    <div class="col-md-6"><input type="text" ng-model="preview_scope.settings['cat.pocket.line.height']"></input></div>
+                </div>
+
+                <div class="row">
+                    <div>[% l('Set the default maximum number of lines for pocket labels.') %]</div>
+                </div>
+
+                <div class="row" style="border-top: solid black">
+                    <div class="col-md-6" style="font-weight: bold">[% l('Pocket label left margin') %]</div>
+                    <div class="col-md-6"><input type="text" ng-model="preview_scope.settings['cat.pocket.line.margin']"></input></div>
+                </div>
+
+                <div class="row">
+                    <div>[% l('Set the left margin for pocket labels in number of characters.') %]</div>
+                </div>
+
+                <div class="row" style="border-top: solid black">
+                    <div class="col-md-6" style="font-weight: bold">[% l('Pocket label line width') %]</div>
+                    <div class="col-md-6"><input type="text" ng-model="preview_scope.settings['cat.pocket.line.width']"></input></div>
+                </div>
+
+                <div class="row">
+                    <div>[% l('Set the default line width for pocket labels in number of characters. This specifies the boundary at which lines must be wrapped.') %]</div>
+                </div>
+
+            </div>
+            <div ng-show="current_tab == 'template'">
+                <div ng-if="print.load_failed" class="alert alert-danger">
+                  [% l(
+                    "Unable to load template '[_1]'.  The web server returned an error.",
+                    '{{print.template_name}}')
+                  %]
+                </div>
+                <div>
+                  <textarea ng-model="print.template_content" class="print-template-text">
+                  </textarea>
+                </div>
+            </div>
+        </div>
+    </div>
+  </div>
+  <div class="col-md-7">
+    <h3>
+        [% l('Label Preview') %]
+    </h3>
+    <div eg-print-template-output
+      content="print.template_content"
+      context="preview_scope"></div>
+  </div> <!-- col -->
+</div>
+
index 38dde8f..7a17c39 100644 (file)
@@ -33,7 +33,7 @@
             </div>
         </div>
         <div class="col-md-2">
-            <button class="btn btn-default pull-right" ng-click="clearWorking()" type="button">Clear</button>
+            <button class="btn btn-default pull-right" ng-click="clearWorking()" type="button">[% l('Clear') %]</button>
         </div>
     </div>
 
diff --git a/Open-ILS/src/templates/staff/share/print_templates/t_item_label.tt2 b/Open-ILS/src/templates/staff/share/print_templates/t_item_label.tt2
new file mode 100644 (file)
index 0000000..fed5e33
--- /dev/null
@@ -0,0 +1,75 @@
+<!--
+Template for printing spine and pocket labels.  Available
+macros include:
+
+copies - list; each entry is a fleshed, flattened copy record
+with a variety of keys, including
+
+  barcode
+  call_number.record.simple_record.title (title)
+  call_number.label 
+  location.name
+
+Line-wrapping may be done with the | wrap filter.  The first
+parameter is the number of characters to target for the string
+width.  The second parameter is the wrapping algorithm to use.
+The third parameter is the prefix to use for the second and
+subsequent wrapped lines.  One use for this is indentation
+with spaces.
+
+Available wrapping algorithms:
+
+once    = Wraps a line just once
+default = Keeps wrapping a line until no more wraps are possible
+
+Certain library settings are also available:
+
+    Spine label maximum lines:           settings['cat.spine.line.height']
+    Spine label left margin:             settings['cat.spine.line.margin']
+    Spine label line width:              settings['cat.spine.line.width']
+    Spine and pocket label font family:  settings['cat.label.font.family']
+    Spine and pocket label font size:    settings['cat.label.font.size']
+    Spine and pocket label font weight:  settings['cat.label.font.weight']
+
+-->
+<style>
+    .spine {
+        font-family: {{settings['cat.label.font.family'] || 'monospace'}};
+        font-weight: {{settings['cat.label.font.weight'] || 'normal'}};
+        font-size: {{settings['cat.label.font.size'] || '10'}};
+        height: {{settings['cat.spine.line.height'] || '1in'}};
+        width: {{settings['cat.spine.line.width'] || '1in'}};
+        left-margin: {{settings['cat.spine.line.margin'] || '0in'}};
+    }
+    .pocket {
+        font-family: {{settings['cat.label.font.family'] || 'monospace'}};
+        font-weight: {{settings['cat.label.font.weight'] || 'normal'}};
+        font-size: {{settings['cat.label.font.size'] || '10'}};
+        height: {{settings['cat.pocket.line.height'] || '1in'}};
+        width: {{settings['cat.pocket.line.width'] || '3in'}};
+        left-margin: {{settings['cat.pocket.line.margin'] || '0in'}};
+    }
+    .labels ::-webkit-scrollbar { 
+        display: none; 
+    }
+</style>
+<table class="labels" style="page-break-after: always;" ng-repeat="copy in copies">
+<tr valign="top">
+<td>
+<!-- Spine Label contents -->
+<!-- The get_cn_for() function will retrieve edited call numbers from the Call Numbers tab -->
+<pre class="spine" style="border:none" ng-show="true">
+{{get_cn_for(copy)}}
+</pre>
+</td>
+<td>
+<!-- Pocket Label contents -->
+<pre class="pocket" style="border:none" ng-show="true">
+{{copy.barcode}}
+{{copy['call_number.label']}}
+{{copy['call_number.record.simple_record.author']}}
+{{copy['call_number.record.simple_record.title'] | wrap:28:'once':' '}}
+</pre>
+</td>
+</tr>
+</table>
diff --git a/Open-ILS/src/templates/staff/share/print_templates/t_item_label_cn.tt2 b/Open-ILS/src/templates/staff/share/print_templates/t_item_label_cn.tt2
new file mode 100644 (file)
index 0000000..8e16381
--- /dev/null
@@ -0,0 +1,11 @@
+<pre>
+{{copy['call_number.prefix.label'] || copy['location.label_prefix']}}
+{{
+       
+       copy['call_number.label']
+       
+    |   wrap:5
+
+}}
+{{copy['call_number.suffix.label'] || copy['location.label_suffix']}}
+</pre>
index a25d719..dd5ac60 100644 (file)
@@ -555,6 +555,16 @@ function($scope , $q , egCore , ngToast) {
         });
     }
 
+    $scope.reset_to_default = function() {
+        egCore.print.removePrintTemplate(
+            $scope.print.template_name
+        );
+        egCore.print.removePrintTemplateContext(
+            $scope.print.template_name
+        );
+        $scope.template_changed();
+    }
+
     $scope.save_locally = function() {
         egCore.print.storePrintTemplate(
             $scope.print.template_name,
diff --git a/Open-ILS/web/js/ui/default/staff/cat/printlabels/app.js b/Open-ILS/web/js/ui/default/staff/cat/printlabels/app.js
new file mode 100644 (file)
index 0000000..15a6a74
--- /dev/null
@@ -0,0 +1,477 @@
+/**
+ * Vol/Copy Editor
+ */
+
+angular.module('egPrintLabels',
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+    $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|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',
+function($scope , $q , $window , $routeParams , $location , $timeout , egCore , egNet , ngToast , itemSvc ) {
+
+    var dataKey = $routeParams.dataKey;
+    console.debug('dataKey: ' + dataKey);
+
+    $scope.print = {
+        template_name : 'item_label',
+        template_output : '',
+        template_context : 'default'
+    };
+
+
+    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' : {}
+                    ,'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 '...';
+                        }
+                    }
+                };
+
+                var promises = [];
+
+                promises.push(
+                    egCore.org.settings([
+                         'cat.label.font.family'
+                        ,'cat.label.font.size'
+                        ,'cat.label.font.weight'
+                        ,'cat.spine.line.height'
+                        ,'cat.spine.line.width'
+                        ,'cat.spine.line.margin'
+                    ]).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) {
+                            $scope.preview_scope.copies.push(egCore.idl.toHash(res.copy, true));
+                        })
+                    )
+                });
+
+                $q.all(promises).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 () {
+        egCore.hatch.getItem('cat.printlabels.templates').then(function(t) {
+            if (t) {
+                $scope.templates = t;
+                $scope.template_name_list = Object.keys(t);
+            }
+        });
+    }
+    $scope.fetchTemplates();
+
+    $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];
+        }
+    }
+
+    $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);
+        }
+    }
+
+    $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
+            };
+            $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() {
+        $scope.save_locally();
+        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([
+             'cat.label.font.family'
+            ,'cat.label.font.size'
+            ,'cat.label.font.weight'
+            ,'cat.spine.line.height'
+            ,'cat.spine.line.width'
+            ,'cat.spine.line.margin'
+        ]).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
+                    };
+                });
+                $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.$watch('print.cn_template_content', function(newVal, oldVal) {
+        if (newVal && newVal != oldVal) {
+            $scope.rebuild_cn_set();
+        }
+    });
+
+    $scope.current_tab = 'call_numbers';
+    $scope.set_tab = function(tab) {
+        $scope.current_tab = tab;
+    }
+
+}])
+
+// 
+.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('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;
+    }
+})
+
index 97f8333..1b950e3 100644 (file)
@@ -206,6 +206,10 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
         return egHatch.setItem('eg.print.template.' + name, html);
     }
 
+    service.removePrintTemplate = function(name) {
+        return egHatch.removeItem('eg.print.template.' + name);
+    }
+
     service.getPrintTemplateContext = function(name) {
         var deferred = $q.defer();
 
@@ -220,6 +224,9 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
     service.storePrintTemplateContext = function(name, context) {
         return egHatch.setItem('eg.print.template_context.' + name, context);
     }
+    service.removePrintTemplateContext = function(name) {
+        return egHatch.removeItem('eg.print.template_context.' + name);
+    }
 
     return service;
 }])