--- /dev/null
+<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>
+
--- /dev/null
+/**
+ * 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;
+ }
+})
+