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