Bug 11559: Rancor: advanced cataloging interface
[koha.git] / koha-tmpl / intranet-tmpl / prog / en / includes / cateditor-ui.inc
1 <script src="/intranet-tmpl/lib/codemirror/codemirror-compressed.js"></script>
2 <script src="/intranet-tmpl/lib/filesaver.js"></script>
3 <script src="/intranet-tmpl/lib/koha/cateditor/marc-mode.js"></script>
4 <script src="/intranet-tmpl/lib/require.js"></script>
5 <script>
6 require.config( {
7     baseUrl: '/intranet-tmpl/lib/koha/cateditor/',
8     config: {
9         resources: {
10             themelang: '[% themelang %]',
11         },
12     },
13     waitSeconds: 30,
14 } );
15 </script>
16
17 [% IF marcflavour == 'MARC21' %]
18 [% PROCESS 'cateditor-widgets-marc21.inc' %]
19 [% ELSE %]
20 <script>var editorWidgets = {};</script>
21 [% END %]
22
23 <script>
24 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'preferences', 'resources', 'text-marc', 'widget' ], function( KohaBackend, Search, Macros, MARCEditor, MARC, Preferences, Resources, TextMARC, Widget ) {
25     var z3950Servers = {
26         'koha:biblioserver': {
27             name: _("Local catalog"),
28             recordtype: 'biblio',
29             checked: false,
30         },
31         [%- FOREACH server = z3950_servers -%]
32             [% server.id %]: {
33                 name: '[% server.servername %]',
34                 recordtype: '[% server.recordtype %]',
35                 checked: [% server.checked ? 'true' : 'false' %],
36             },
37         [%- END -%]
38     };
39
40     // The columns that should show up in a search, in order, and keyed by the corresponding <metadata> tag in the XSL and Pazpar2 config
41     var z3950Labels = [
42                 [ "local_number", _("Local number") ],
43                 [ "title", _("Title") ],
44                 [ "series", _("Series title") ],
45                 [ "author", _("Author") ],
46                 [ "lccn", _("LCCN") ],
47                 [ "isbn", _("ISBN") ],
48                 [ "issn", _("ISSN") ],
49                 [ "medium", _("Medium") ],
50                 [ "edition", _("Edition") ],
51                 [ "notes", _("Notes") ],
52     ];
53
54     var state = {
55         backend: '',
56         saveBackend: 'catalog',
57         recordID: undefined
58     };
59
60     var editor;
61     var macroEditor;
62
63     function makeAuthorisedValueWidgets( frameworkCode ) {
64         $.each( KohaBackend.GetAllTagsInfo( frameworkCode ), function( tag, tagInfo ) {
65             $.each( tagInfo.subfields, function( subfield, subfieldInfo ) {
66                 if ( !subfieldInfo.authorised_value ) return;
67                 var authvals = KohaBackend.GetAuthorisedValues( subfieldInfo.authorised_value );
68                 if ( !authvals ) return;
69
70                 var defaultvalue = subfield.defaultvalue || authvals[0].value;
71
72                 Widget.Register( tag + subfield, {
73                     init: function() {
74                         var $result = $( '<span class="subfield-widget"></span>' );
75
76                         return $result[0];
77                     },
78                     postCreate: function() {
79                         this.setText( defaultvalue );
80
81                         $( '<select></select>' ).appendTo( this.node );
82                         var $node = $( this.node ).find( 'select' );
83                         $.each( authvals, function( undef, authval ) {
84                             $node.append( '<option value="' + authval.value + '"' + (authval.value == defaultvalue ? ' selected="selected"' : '') + '>' + authval.lib + '</option>' );
85                         } );
86                         $node.val( this.text );
87
88                         $node.change( $.proxy( function() {
89                             this.setText( $node.val() );
90                         }, this ) );
91                     },
92                     makeTemplate: function() {
93                         return defaultvalue;
94                     },
95                 } );
96             } );
97         } );
98     }
99
100     function bindGlobalKeys() {
101         shortcut.add( 'ctrl+s', function(event) {
102             $( '#save-record' ).click();
103
104             event.preventDefault();
105         } );
106
107         shortcut.add( 'alt+ctrl+k', function(event) {
108             if ( Search.IsAvailable() ) $( '#search-by-keywords' ).focus();
109
110             return false;
111         } );
112
113         shortcut.add( 'alt+ctrl+a', function(event) {
114             if ( Search.IsAvailable() ) $( '#search-by-author' ).focus();
115
116             return false;
117         } );
118
119         shortcut.add( 'alt+ctrl+i', function(event) {
120             if ( Search.IsAvailable() ) $( '#search-by-isbn' ).focus();
121
122             return false;
123         } );
124
125         shortcut.add( 'alt+ctrl+t', function(event) {
126             if ( Search.IsAvailable() ) $( '#search-by-title' ).focus();
127
128             return false;
129         } );
130
131         shortcut.add( 'ctrl+h', function() {
132             var field = editor.getCurrentField();
133
134             if ( !field ) return;
135
136             window.open( getFieldHelpURL( field.tag ) );
137         } );
138
139         $('#quicksearch .search-box').each( function() {
140             shortcut.add( 'enter', $.proxy( function() {
141                 var terms = [];
142
143                 $('#quicksearch .search-box').each( function() {
144                     if ( !this.value ) return;
145
146                     terms.push( [ $(this).data('qualifier'), this.value ] );
147                 } );
148
149                 if ( !terms.length ) return;
150
151                 if ( Search.Run( z3950Servers, Search.JoinTerms(terms) ) ) {
152                     $("#search-overlay").show();
153                     showResultsBox();
154                 }
155
156                 return false;
157             }, this), { target: this, type: 'keypress' } );
158         } );
159     }
160
161     function getFieldHelpURL( tag ) {
162         [% IF ( marcflavour == 'MARC21' ) %]
163             if ( tag == '000' ) {
164                 return "http://www.loc.gov/marc/bibliographic/bdleader.html";
165             } else if ( tag < '900' ) {
166                 return "http://www.loc.gov/marc/bibliographic/bd" + tag + ".html";
167             } else {
168                 return "http://www.loc.gov/marc/bibliographic/bd9xx.html";
169             }
170         [% ELSIF ( marcflavour == 'UNIMARC' ) %]
171             /* http://archive.ifla.org/VI/3/p1996-1/ is an outdated version of UNIMARC, but
172                seems to be the only version available that can be linked to per tag.  More recent
173                versions of the UNIMARC standard are available on the IFLA website only as
174                PDFs!
175             */
176             if ( tag == '000' ) {
177                return  "http://archive.ifla.org/VI/3/p1996-1/uni.htm";
178             } else {
179                 var first = tag[0];
180                 var url = "http://archive.ifla.org/VI/3/p1996-1/uni" + first + ".htm#";
181                 if ( first == '0' ) url += "b";
182                 if ( first != '9' ) url += field;
183
184                 return url;
185             }
186         [% END %]
187     }
188
189     // Record loading
190     var backends = {
191        'new': {
192             titleForRecord: _("Editing new record"),
193             get: function( id, callback ) {
194                 record = new MARC.Record();
195                 KohaBackend.FillRecord( '', record );
196
197                 callback( record );
198             },
199         },
200         'new-full': {
201             titleForRecord: _("Editing new full record"),
202             get: function( id, callback ) {
203                 record = new MARC.Record();
204                 KohaBackend.FillRecord( '', record, true );
205
206                 callback( record );
207             },
208         },
209         'catalog': {
210             titleForRecord: _("Editing catalog record #{ID}"),
211             links: [
212                 { title: _("view"), href: "/cgi-bin/koha/catalogue/detail.pl?biblionumber={ID}" },
213                 { title: _("edit items"), href: "/cgi-bin/koha/cataloguing/additem.pl?biblionumber={ID}" },
214             ],
215             saveLabel: _("Save to catalog"),
216             get: function( id, callback ) {
217                 if ( !id ) return false;
218
219                 KohaBackend.GetRecord( id, callback );
220             },
221             save: function( id, record, done ) {
222                 function finishCb( data ) {
223                     done( { error: data.error, newRecord: data.marcxml && data.marcxml[0], newId: data.biblionumber && [ 'catalog', data.biblionumber ] } );
224                 }
225
226                 if ( id ) {
227                     KohaBackend.SaveRecord( id, record, finishCb );
228                 } else {
229                     KohaBackend.CreateRecord( record, finishCb );
230                 }
231             }
232         },
233         'iso2709': {
234             saveLabel: _("Save as ISO2709 (.mrc) file"),
235             save: function( id, record, done ) {
236                 saveAs( new Blob( [record.toISO2709()], { 'type': 'application/octet-stream;charset=utf-8' } ), 'record.mrc' );
237
238                 done( {} );
239             }
240         },
241         'marcxml': {
242             saveLabel: _("Save as MARCXML (.xml) file"),
243             save: function( id, record, done ) {
244                 saveAs( new Blob( [record.toXML()], { 'type': 'application/octet-stream;charset=utf-8' } ), 'record.xml' );
245
246                 done( {} );
247             }
248         },
249         'search': {
250             titleForRecord: _("Editing search result"),
251             get: function( id, callback ) {
252                 if ( !id ) return false;
253                 if ( !backends.search.records[ id ] ) {
254                     callback( { error: _( "Invalid record" ) } );
255                     return false;
256                 }
257
258                 callback( backends.search.records[ id ] );
259             },
260             records: {},
261         },
262     };
263
264     function setSource(parts) {
265         state.backend = parts[0];
266         state.recordID = parts[1];
267         state.canSave = backends[ state.backend ].save != null;
268         state.saveBackend = state.canSave ? state.backend : 'catalog';
269
270         var backend = backends[state.backend];
271
272         document.location.hash = '#' + parts[0] + '/' + parts[1];
273
274         $('#title').text( backend.titleForRecord.replace( '{ID}', parts[1] ) );
275
276         $.each( backend.links || [], function( i, link ) {
277             $('#title').append(' <a target="_blank" href="' + link.href.replace( '{ID}', parts[1] ) + '">(' + link.title + ')</a>' );
278         } );
279         $( 'title', document.head ).html( _("Koha &rsaquo; Cataloging &rsaquo; ") + backend.titleForRecord.replace( '{ID}', parts[1] ) );
280         $('#save-record span').text( backends[ state.saveBackend ].saveLabel );
281     }
282
283     function saveRecord( recid, editor, callback ) {
284         var parts = recid.split('/');
285         if ( parts.length != 2 ) return false;
286
287         if ( !backends[ parts[0] ] || !backends[ parts[0] ].save ) return false;
288
289         editor.removeErrors();
290         var record = editor.getRecord();
291
292         if ( record.errors ) {
293             state.saving = false;
294             callback( { error: 'syntax', errors: record.errors } );
295             return;
296         }
297
298         var errors = KohaBackend.ValidateRecord( '', record );
299         if ( errors.length ) {
300             state.saving = false;
301             callback( { error: 'invalid', errors: errors } );
302             return;
303         }
304
305         backends[ parts[0] ].save( parts[1], record, function(data) {
306             state.saving = false;
307
308             if (data.newRecord) {
309                 var record = new MARC.Record();
310                 record.loadMARCXML(data.newRecord);
311                 editor.displayRecord( record );
312             }
313
314             if (data.newId) {
315                 setSource(data.newId);
316             } else {
317                 setSource( [ state.backend, state.recordID ] );
318             }
319
320             if (callback) callback( data );
321         } );
322     }
323
324     function loadRecord( recid, editor, callback ) {
325         var parts = recid.split('/');
326         if ( parts.length != 2 ) return false;
327
328         if ( !backends[ parts[0] ] || !backends[ parts[0] ].get ) return false;
329
330         backends[ parts[0] ].get( parts[1], function( record ) {
331             if ( !record.error ) {
332                 editor.displayRecord( record );
333                 editor.focus();
334             }
335
336             if (callback) callback(record);
337         } );
338
339         return true;
340     }
341
342     function openRecord( recid, editor, callback ) {
343         return loadRecord( recid, editor, function ( record ) {
344             setSource( recid.split('/') );
345
346             if (callback) callback( record );
347         } );
348     }
349
350     // Search functions
351     function showAdvancedSearch() {
352         $('#advanced-search-servers').empty();
353         $.each( z3950Servers, function( server_id, server ) {
354             $('#advanced-search-servers').append( '<li data-server-id="' + server_id + '"><input class="search-toggle-server" type="checkbox"' + ( server.checked ? ' checked="checked">' : '>' ) + server.name + '</li>' );
355         } );
356         $('#advanced-search-ui').modal('show');
357     }
358
359     function startAdvancedSearch() {
360         var terms = [];
361
362         $('#advanced-search-ui .search-box').each( function() {
363             if ( !this.value ) return;
364
365             terms.push( [ $(this).data('qualifier'), this.value ] );
366         } );
367
368         if ( !terms.length ) return;
369
370         if ( Search.Run( z3950Servers, Search.JoinTerms(terms) ) ) {
371             $('#advanced-search-ui').modal('hide');
372             $("#search-overlay").show();
373             showResultsBox();
374         }
375     }
376
377     function showResultsBox(data) {
378         $('#search-top-pages, #search-bottom-pages').find('.pagination').empty();
379         $('#searchresults thead tr').empty();
380         $('#searchresults tbody').empty();
381         $('#search-serversinfo').empty().append('<li>' + _("Loading...") + '</li>');
382         $('#search-results-ui').modal('show');
383     }
384
385     function showSearchSorting( sort_key, sort_direction ) {
386         var $th = $('#searchresults thead tr th[data-sort-label="' + sort_key + '"]');
387         $th.parent().find( 'th[data-sort-label]' ).attr( 'class', 'sorting' );
388
389         if ( sort_direction == 'asc' ) {
390             direction = 'asc';
391             $th.attr( 'class', 'sorting_asc' );
392         } else {
393             direction = 'desc';
394             $th.attr( 'class', 'sorting_desc' );
395         }
396     }
397
398     function showSearchResults( editor, data ) {
399         backends.search.records = {};
400
401         $('#searchresults thead tr').empty();
402         $('#searchresults tbody').empty();
403         $('#search-serversinfo').empty();
404
405         $.each( z3950Servers, function( server_id, server ) {
406             var num_fetched = data.num_fetched[server_id];
407
408             if ( data.errors[server_id] ) {
409                 num_fetched = data.errors[server_id];
410             } else if ( num_fetched == null ) {
411                 num_fetched = '-';
412             } else if ( num_fetched < data.num_hits[server_id] ) {
413                 num_fetched += '+';
414             }
415
416             $('#search-serversinfo').append( '<li data-server-id="' + server_id + '"><input class="search-toggle-server" type="checkbox"' + ( server.checked ? ' checked="checked">' : '>' ) + server.name + ' (' + num_fetched + ')' + '</li>' );
417         } );
418
419         var seenColumns = {};
420
421         $.each( data.hits, function( undef, hit ) {
422             $.each( hit.metadata, function(key) {
423                 seenColumns[key] = true;
424             } );
425         } );
426
427         $('#searchresults thead tr').append('<th>' + _("Source") + '</th>');
428
429         $.each( z3950Labels, function( undef, label ) {
430             if ( seenColumns[ label[0] ] ) {
431                 $('#searchresults thead tr').append( '<th class="sorting" data-sort-label="' + label[0] + '">' + label[1] + '</th>' );
432             }
433         } );
434
435         showSearchSorting( data.sort_key, data.sort_direction );
436
437         $('#searchresults thead tr').append('<th>' + _("Tools") + '</th>');
438
439         $.each( data.hits, function( undef, hit ) {
440             backends.search.records[ hit.server + ':' + hit.index ] = hit.record;
441             hit.id = 'search/' + hit.server + ':' + hit.index;
442
443             var result = '<tr>';
444             result += '<td class="sourcecol">' + z3950Servers[ hit.server ].name + '</td>';
445
446             $.each( z3950Labels, function( undef, label ) {
447                 if ( !seenColumns[ label[0] ] ) return;
448
449                 if ( hit.metadata[ label[0] ] ) {
450                     result += '<td class="infocol">' + hit.metadata[ label[0] ] + '</td>';
451                 } else {
452                     result += '<td class="infocol">&nbsp;</td>';
453                 }
454             } );
455
456             result += '<td class="toolscol"><ul><li><a href="#" class="marc-link">' + _("View MARC") + '</a></li>';
457             result += '<li><a href="#" class="open-link">' + _("Import") + '</a></li>';
458             if ( state.canSave ) result += '<li><a href="#" class="substitute-link" title="' + _("Replace the current record's contents") + '">' + _("Substitute") + '</a></li>';
459             result += '</ul></td></tr>';
460
461             var $tr = $( result );
462             $tr.find( '.marc-link' ).click( function() {
463                 var $info_columns = $tr.find( '.infocol' );
464                 var $marc_column = $tr.find( '.marccol' );
465
466                 if ( !$marc_column.length ) {
467                     $marc_column = $( '<td class="marccol" colspan="' + $info_columns.length + '"></td>' ).insertAfter( $info_columns.eq(-1) ).hide();
468                     CodeMirror.runMode( TextMARC.RecordToText( hit.record ), 'marc', $marc_column[0] );
469                 }
470
471                 if ( $marc_column.is(':visible') ) {
472                     $tr.find('.marc-link').text( _("View MARC") );
473                     $info_columns.show();
474                     $marc_column.hide();
475                 } else {
476                     $tr.find('.marc-link').text( _("Hide MARC") );
477                     $marc_column.show();
478                     $info_columns.hide();
479                 }
480
481                 return false;
482             } );
483             $tr.find( '.open-link' ).click( function() {
484                 $( '#search-results-ui' ).modal('hide');
485                 openRecord( hit.id, editor );
486
487                 return false;
488             } );
489             $tr.find( '.substitute-link' ).click( function() {
490                 $( '#search-results-ui' ).modal('hide');
491                 loadRecord( hit.id, editor );
492
493                 return false;
494             } );
495             $('#searchresults tbody').append( $tr );
496         } );
497
498         var pages = [];
499         var cur_page = data.offset / data.page_size;
500         var max_page = Math.ceil( data.total_fetched / data.page_size ) - 1;
501
502         if ( cur_page != 0 ) {
503             pages.push( '<li><a class="search-nav" href="#" data-offset="' + (data.offset - data.page_size) + '">&laquo; ' + _("Previous") + '</a></li>' );
504         }
505
506         for ( var page = Math.max( 0, cur_page - 9 ); page <= Math.min( max_page, cur_page + 9 ); page++ ) {
507             if ( page == cur_page ) {
508                 pages.push( ' <li class="active"><a href="#">' + ( page + 1 ) + '</a></li>' );
509             } else {
510                 pages.push( ' <li><a class="search-nav" href="#" data-offset="' + ( page * data.page_size ) + '">' + ( page + 1 ) + '</a></li>' );
511             }
512         }
513
514         if ( cur_page < max_page ) {
515             pages.push( ' <li><a class="search-nav" href="#" data-offset="' + (data.offset + data.page_size) + '">' + _("Next") + ' &raquo;</a></li>' );
516         }
517
518         if ( pages.length > 1 ) $( '#search-top-pages, #search-bottom-pages' ).find( '.pagination' ).html( '<ul>' + pages.join( '' ) + '</ul>');
519
520         var $overlay = $('#search-overlay');
521         $overlay.find('span').text(_("Loading"));
522         $overlay.find('.bar').css( { display: 'block', width: 100 * ( 1 - data.activeclients / Search.includedServers.length ) + '%' } );
523
524         if ( data.activeclients ) {
525             $overlay.find('.bar').css( { display: 'block', width: 100 * ( 1 - data.activeclients / Search.includedServers.length ) + '%' } );
526             $overlay.show();
527         } else {
528             $overlay.find('.bar').css( { display: 'block', width: '100%' } );
529             $overlay.fadeOut();
530         }
531     }
532
533     function invalidateSearchResults() {
534         var $overlay = $('#search-overlay');
535         $overlay.find('span').text(_("Search expired, please try again"));
536         $overlay.find('.bar').css( { display: 'none' } );
537         $overlay.show();
538     }
539
540     function handleSearchError(error) {
541         if (error.code == 1) {
542             invalidateSearchResults();
543             Search.Reconnect();
544         } else {
545             humanMsg.displayMsg( _("<h3>Internal search error</h3>") + '<p>' + error + '</p>' + _("<p>Please <b>refresh</b> the page and try again."), { className: 'humanError' } );
546         }
547     }
548
549     function handleSearchInitError(error) {
550         $('#quicksearch-overlay').fadeIn().find('p').text(error);
551     }
552
553     // Preference functions
554     function showPreference( pref ) {
555         var value = Preferences.user[pref];
556
557         switch (pref) {
558             case 'fieldWidgets':
559                 $( '#set-field-widgets' ).text( value ? _("Show fields verbatim") : _("Show helpers for fixed and coded fields") );
560                 break;
561             case 'font':
562                 $( '#editor .CodeMirror' ).css( { fontFamily: value } );
563                 editor.refresh();
564                 break;
565             case 'fontSize':
566                 $( '#editor .CodeMirror' ).css( { fontSize: value } );
567                 editor.refresh();
568                 break;
569             case 'macros':
570                 showSavedMacros();
571                 break;
572             case 'selected_search_targets':
573                 $.each( z3950Servers, function( server_id, server ) {
574                     var saved_val = Preferences.user.selected_search_targets[server_id];
575
576                     if ( saved_val != null ) server.checked = saved_val;
577                 } );
578                 break;
579         }
580     }
581
582     function bindPreference( editor, pref ) {
583         function _addHandler( sel, event, handler ) {
584             $( sel ).on( event, function (e) {
585                 e.preventDefault();
586                 handler( e, Preferences.user[pref] );
587                 Preferences.Save( [% USER_INFO.0.borrowernumber %] );
588                 showPreference(pref);
589             } );
590         }
591
592         switch (pref) {
593             case 'fieldWidgets':
594                 _addHandler( '#set-field-widgets', 'click', function( e, oldValue ) {
595                     editor.setUseWidgets( Preferences.user.fieldWidgets = !Preferences.user.fieldWidgets );
596                 } );
597                 break;
598             case 'font':
599                 _addHandler( '#prefs-menu .set-font', 'click', function( e, oldValue ) {
600                     Preferences.user.font = $( e.target ).css( 'font-family' );
601                 } );
602                 break;
603             case 'fontSize':
604                 _addHandler( '#prefs-menu .set-fontSize', 'click', function( e, oldValue ) {
605                     Preferences.user.fontSize = $( e.target ).css( 'font-size' );
606                 } );
607                 break;
608             case 'selected_search_targets':
609                 $( document ).on( 'change', 'input.search-toggle-server', function() {
610                     var server_id = $( this ).parent().data('server-id');
611                     Preferences.user.selected_search_targets[server_id] = this.checked;
612                     Preferences.Save( [% USER_INFO.0.borrowernumber %] );
613                 } );
614                 break;
615         }
616     }
617
618     function displayPreferences( editor ) {
619         $.each( Preferences.user, function( pref, value ) {
620             showPreference( pref );
621             bindPreference( editor, pref );
622         } );
623     }
624
625     //> Macro functions
626     function loadMacro( name ) {
627         $( '#macro-list li' ).removeClass( 'active' );
628         macroEditor.activeMacro = name;
629
630         if ( !name ) {
631             macroEditor.setValue( '' );
632             return;
633         }
634
635         $( '#macro-list li[data-name="' + name + '"]' ).addClass( 'active' );
636         var macro = Preferences.user.macros[name];
637         macroEditor.setValue( macro.contents );
638         $( '#macro-format' ).val( macro.format || 'its' );
639         if ( macro.history ) macroEditor.setHistory( macro.history );
640     }
641
642     function storeMacro( name, macro ) {
643         if ( macro ) {
644             Preferences.user.macros[name] = macro;
645         } else {
646             delete Preferences.user.macros[name];
647         }
648
649         Preferences.Save( [% USER_INFO.0.borrowernumber %] );
650     }
651
652     function showSavedMacros( macros ) {
653         var scrollTop = $('#macro-list').scrollTop();
654         $( '#macro-list' ).empty();
655         var macro_list = $.map( Preferences.user.macros, function( macro, name ) {
656             return $.extend( { name: name }, macro );
657         } );
658         macro_list.sort( function( a, b ) {
659             return a.name.localeCompare(b.name);
660         } );
661         $.each( macro_list, function( undef, macro ) {
662             var $li = $( '<li data-name="' + macro.name + '"><a href="#">' + macro.name + '</a><ol class="macro-info"></ol></li>' );
663             $li.click( function() {
664                 loadMacro(macro.name);
665                 return false;
666             } );
667             if ( macro.name == macroEditor.activeMacro ) $li.addClass( 'active' );
668             var modified = macro.modified && new Date(macro.modified);
669             $li.find( '.macro-info' ).append(
670                 '<li><span class="label">' + _("Last changed:") + '</span>' +
671                 ( modified ? modified.toLocaleFormat() : _("never") ) + '</li>'
672             );
673             $('#macro-list').append($li);
674         } );
675         var $new_li = $( '<li class="new-macro"><a href="#">' + _("New macro...") + '</a></li>' );
676         $new_li.click( function() {
677             // TODO: make this a bit less retro
678             var name = prompt(_("Please enter the name for the new macro:"));
679             if (!name) return;
680
681             if ( !Preferences.user.macros[name] ) storeMacro( name, { format: "rancor", contents: "" } );
682             showSavedMacros();
683             loadMacro( name );
684         } );
685         $('#macro-list').append($new_li);
686         $('#macro-list').scrollTop(scrollTop);
687     }
688
689     function saveMacro() {
690         var name = macroEditor.activeMacro;
691
692         if ( !name || macroEditor.savedGeneration == macroEditor.changeGeneration() ) return;
693
694         macroEditor.savedGeneration = macroEditor.changeGeneration();
695         storeMacro( name, { contents: macroEditor.getValue(), modified: (new Date()).valueOf(), history: macroEditor.getHistory(), format: $('#macro-format').val() } );
696         $('#macro-save-message').text(_("Saved"));
697         showSavedMacros();
698     }
699
700     $(document).ready( function() {
701         // Editor setup
702         editor = new MARCEditor( {
703             onCursorActivity: function() {
704                 $('#status-tag-info').empty();
705                 $('#status-subfield-info').empty();
706
707                 var field = editor.getCurrentField();
708                 var cur = editor.getCursor();
709
710                 if ( !field ) return;
711
712                 var taginfo = KohaBackend.GetTagInfo( '', field.tag );
713                 $('#status-tag-info').html( '<strong>' + field.tag + ':</strong> ' );
714
715                 if ( taginfo ) {
716                     $('#status-tag-info').append( '<a href="' + getFieldHelpURL( field.tag ) + '" target="_blank" class="show-field-help" title="' + _("Show help for this tag") + '">[?]</a> '  + taginfo.lib );
717
718                     var subfield = field.getSubfieldAt( cur.ch );
719                     if ( !subfield ) return;
720
721                     var subfieldinfo = taginfo.subfields[ subfield.code ];
722                     $('#status-subfield-info').html( '<strong>$' + subfield.code + ':</strong> ' );
723
724                     if ( subfieldinfo ) {
725                         $('#status-subfield-info').append( subfieldinfo.lib );
726                     } else {
727                         $('#status-subfield-info').append( '<em>' + _("Unknown subfield") + '</em>' );
728                     }
729                 } else {
730                     $('#status-tag-info').append( '<em>' + _("Unknown tag") + '</em>' );
731                 }
732             },
733             position: function (elt) { $(elt).insertAfter('#toolbar') },
734         } );
735
736         macroEditor = CodeMirror(
737             $('#macro-editor')[0],
738             {
739                 mode: 'null',
740                 lineNumbers: true,
741             }
742         );
743
744         // Automatically detect resizes and change the height of the editor and position of modals.
745         var resizeTimer = null;
746         $( window ).resize( function() {
747             if ( resizeTimer == null ) resizeTimer = setTimeout( function() {
748                 resizeTimer = null;
749
750                 var pos = $('#editor .CodeMirror').position();
751                 $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 - $('#changelanguage').height() ); // 24 is hardcoded value but works well
752
753                 $('.modal-body').each( function() {
754                     $(this).height( $(window).height() * .8 - $(this).prevAll('.modal-header').height() );
755                 } );
756             }, 100);
757
758             $("#advanced-search-ui, #search-results-ui, #macro-ui").css( {
759                 marginLeft: function() {
760                     return -($(this).width() / 2);
761                 }
762             } );
763
764         } ).resize();
765
766         var saveableBackends = [];
767         $.each( backends, function( id, backend ) {
768             if ( backend.save ) saveableBackends.push( [ backend.saveLabel, id ] );
769         } );
770         saveableBackends.sort();
771         $.each( saveableBackends, function( undef, backend ) {
772             $( '#save-dropdown' ).append( '<li><a href="#" data-backend="' + backend[1] + '">' + backend[0] + '</a></li>' );
773         } );
774
775         var macro_format_list = $.map( Macros.formats, function( format, name ) {
776             return $.extend( { name: name }, format );
777         } );
778         macro_format_list.sort( function( a, b ) {
779             return a.description.localeCompare(b.description);
780         } );
781         $.each( macro_format_list, function() {
782             $('#macro-format').append( '<option value="' + this.name + '">' + this.description + '</option>' );
783         } );
784
785         // Click bindings
786         $( '#save-record, #save-dropdown a' ).click( function() {
787             $( '#save-record' ).find('i').attr( 'class', 'icon-loading' ).siblings( 'span' ).text( _("Saving...") );
788
789             function finishCb(result) {
790                 if ( result.error == 'syntax' ) {
791                     humanMsg.displayAlert( _("Incorrect syntax, cannot save"), { className: 'humanError' } );
792                 } else if ( result.error == 'invalid' ) {
793                     humanMsg.displayAlert( _("Record structure invalid, cannot save"), { className: 'humanError' } );
794                 } else if ( !result.error ) {
795                     humanMsg.displayAlert( _("Record saved "), { className: 'humanSuccess' } );
796                 }
797
798                 $.each( result.errors || [], function( undef, error ) {
799                     switch ( error.type ) {
800                         case 'noTag':
801                             editor.addError( error.line, _("Invalid tag number") );
802                             break;
803                         case 'noIndicators':
804                             editor.addError( error.line, _("Invalid indicators") );
805                             break;
806                         case 'missingTag':
807                             editor.addError( null, _("Missing mandatory tag: ") + error.tag );
808                             break;
809                         case 'missingSubfield':
810                             if ( error.subfield == '@' ) {
811                                 editor.addError( error.line, _("Missing control field contents") );
812                             } else {
813                                 editor.addError( error.line, _("Missing mandatory subfield: $") + error.subfield );
814                             }
815                             break;
816                         case 'unrepeatableTag':
817                             editor.addError( error.line, _("Tag ") + error.tag + _(" cannot be repeated") );
818                             break;
819                         case 'unrepeatableSubfield':
820                             editor.addError( error.line, _("Subfield $") + error.subfield + _(" cannot be repeated") );
821                             break;
822                     }
823                 } );
824
825                 $( '#save-record' ).find('i').attr( 'class', 'icon-hdd' );
826
827                 if ( result.error ) {
828                     // Reset backend info
829                     setSource( [ state.backend, state.recordID ] );
830                 }
831             }
832
833             var backend = $( this ).data( 'backend' ) || ( state.saveBackend );
834             if ( state.backend == backend ) {
835                 saveRecord( backend + '/' + state.recordID, editor, finishCb );
836             } else {
837                 saveRecord( backend + '/', editor, finishCb );
838             }
839
840             return false;
841         } );
842
843         $('#import-records').click( function() {
844             $('#import-records-input')
845                 .off('change')
846                 .change( function() {
847                     if ( !this.files || !this.files.length ) return;
848
849                     var file = this.files[0];
850                     var reader = new FileReader();
851
852                     reader.onload = function() {
853                         var record = new MARC.Record();
854
855                         if ( /\.mrc$/.test( file.name ) ) {
856                             record.loadISO2709( reader.result );
857                         } else if ( /\.xml$/.test( file.name ) ) {
858                             record.loadMARCXML( reader.result );
859                         } else {
860                             humanMsg.displayAlert( _("Unknown record type, cannot import"), { className: 'humanError' } );
861                             return;
862                         }
863
864                         editor.displayRecord( record );
865                     };
866
867                     reader.readAsText( file );
868                 } )
869                 .click();
870
871             return false;
872         } );
873
874         $('#open-macros').click( function() {
875             $('#macro-ui').modal('show');
876
877             return false;
878         } );
879
880         $('#run-macro').click( function() {
881             var result = Macros.Run( editor, $('#macro-format').val(), macroEditor.getValue() );
882
883             if ( !result.errors.length ) {
884                 $('#macro-ui').modal('hide');
885                 return false;
886             }
887
888             var errors = [];
889             $.each( result.errors, function() {
890                 var error = '<b>' + _("Line ") + (this.line + 1) + ':</b> ';
891
892                 switch ( this.error ) {
893                     case 'failed': error += _("failed to run"); break;
894                     case 'unrecognized': error += _("unrecognized command"); break;
895                 }
896
897                 errors.push(error);
898             } );
899
900             humanMsg.displayMsg( _("<h3>Failed to run macro:</h3>") + '<ul><li>' + errors.join('</li><li>') + '</li></ul>', { className: 'humanError' } );
901
902             return false;
903         } );
904
905         $('#delete-macro').click( function() {
906             if ( !macroEditor.activeMacro || !confirm( _("Are you sure you want to delete this macro?") ) ) return;
907
908             storeMacro( macroEditor.activeMacro, undefined );
909             showSavedMacros();
910             loadMacro( undefined );
911
912             return false;
913         } );
914
915         var saveTimeout;
916         macroEditor.on( 'change', function( cm, change ) {
917             $('#macro-save-message').empty();
918             if ( change.origin == 'setValue' ) return;
919
920             if ( saveTimeout ) clearTimeout( saveTimeout );
921             saveTimeout = setTimeout( function() {
922                 saveMacro();
923
924                 saveTimeout = null;
925             }, 500 );
926         } );
927
928         $( '#switch-editor' ).click( function() {
929             if ( !confirm( _("Any changes will not be saved. Continue?") ) ) return;
930
931             $.cookie( 'catalogue_editor_[% USER_INFO.0.borrowernumber %]', 'basic', { expires: 365, path: '/' } );
932
933             if ( state.backend == 'catalog' ) {
934                 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=' + state.recordID;
935             } else if ( state.backend == 'new' ) {
936                 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl';
937             } else {
938                 humanMsg.displayAlert( _("Cannot open this record in the basic editor"), { className: 'humanError' } );
939             }
940         } );
941
942         $( '#show-advanced-search' ).click( function() {
943             showAdvancedSearch();
944
945             return false;
946         } );
947
948         $('#advanced-search').submit( function() {
949             startAdvancedSearch();
950
951             return false;
952         } );
953
954         $( document ).on( 'click', 'a.search-nav', function() {
955             $("#search-overlay").show();
956             Search.Fetch( { offset: $( this ).data( 'offset' ) } );
957             return false;
958         });
959
960         $( document ).on( 'click', 'th[data-sort-label]', function() {
961             $("#search-overlay").show();
962             var direction;
963
964             if ( $( this ).hasClass( 'sorting_asc' ) ) {
965                 direction = 'desc';
966             } else {
967                 direction = 'asc';
968             }
969
970             showSearchSorting( $( this ).data( 'sort-label' ), direction );
971
972             Search.Fetch( { sort_key: $( this ).data( 'sort-label' ), sort_direction: direction } );
973             return false;
974         });
975
976         $( document ).on( 'change', 'input.search-toggle-server', function() {
977             var server = z3950Servers[ $( this ).parent().data('server-id') ];
978             server.checked = this.checked;
979
980             if ( $('#search-results-ui').is( ':visible' ) ) {
981                 $("#search-overlay").show();
982                 Search.Fetch();
983             }
984         } );
985
986         // Key bindings
987         bindGlobalKeys();
988
989         // Setup UI
990         $("#advanced-search-ui, #search-results-ui, #macro-ui").each( function() {
991             $(this).modal({ show: false });
992         } );
993
994         var $quicksearch = $('#quicksearch fieldset');
995         $('<div id="quicksearch-overlay"><h3>' + _("Search unavailable") + '</h3> <p></p></div>').css({
996             position: 'absolute',
997             top: $quicksearch.offset().top,
998             left: $quicksearch.offset().left,
999             height: $quicksearch.outerHeight(),
1000             width: $quicksearch.outerWidth(),
1001         }).appendTo(document.body).hide();
1002
1003         var prevAlerts = [];
1004         humanMsg.logMsg = function(msg, options) {
1005             $('#show-alerts').popover('hide');
1006             prevAlerts.unshift('<li>' + msg + '</li>');
1007             prevAlerts.splice(5, 999); // Truncate old messages
1008         };
1009
1010         $('#show-alerts').popover({
1011             html: true,
1012             placement: 'bottom',
1013             content: function() {
1014                 return '<div id="alerts-container"><ul>' + prevAlerts.join('') + '</ul></div>';
1015             },
1016         });
1017         $('#new-record' ).click( function() {
1018             openRecord( 'new/', editor );
1019             return false;
1020         } );
1021
1022         // Start editor
1023         Preferences.Load( [% USER_INFO.0.borrowernumber || 0 %] );
1024         displayPreferences(editor);
1025         makeAuthorisedValueWidgets( '' );
1026         Search.Init( {
1027             page_size: 20,
1028             onresults: function(data) { showSearchResults( editor, data ) },
1029             onerror: handleSearchError,
1030         } );
1031
1032         function finishCb( data ) {
1033             if ( data.error ) openRecord( 'new/', editor, finishCb );
1034
1035             Resources.GetAll().done( function() {
1036                 $("#loading").hide();
1037                 editor.focus();
1038             } );
1039         }
1040
1041         if ( "[% auth_forwarded_hash %]" ) {
1042             document.location.hash = "[% auth_forwarded_hash %]";
1043         }
1044
1045         if ( !document.location.hash || !openRecord( document.location.hash.slice(1), editor, finishCb ) ) {
1046             openRecord( 'new/', editor, finishCb );
1047         }
1048     } );
1049 } )();
1050
1051 </script>