Bug 11559: (QA followup) switch to new delimiter, fix minor issues
[koha-equinox.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         var bibnumMap = KohaBackend.GetSubfieldForKohaField('biblio.biblionumber');
441         $.each( data.hits, function( undef, hit ) {
442             backends.search.records[ hit.server + ':' + hit.index ] = hit.record;
443
444             switch ( hit.server ) {
445                 case 'koha:biblioserver':
446                     var bibnumField = hit.record.field( bibnumMap[0] );
447
448                     if ( bibnumField && bibnumField.hasSubfield( bibnumMap[1] ) ) {
449                         hit.id = 'catalog/' + bibnumField.subfield( bibnumMap[1] );
450                         break;
451                     }
452
453                     // Otherwise, fallthrough
454
455                 default:
456                     hit.id = 'search/' + hit.server + ':' + hit.index;
457             }
458
459             var result = '<tr>';
460             result += '<td class="sourcecol">' + z3950Servers[ hit.server ].name + '</td>';
461
462             $.each( z3950Labels, function( undef, label ) {
463                 if ( !seenColumns[ label[0] ] ) return;
464
465                 if ( hit.metadata[ label[0] ] ) {
466                     result += '<td class="infocol">' + hit.metadata[ label[0] ] + '</td>';
467                 } else {
468                     result += '<td class="infocol">&nbsp;</td>';
469                 }
470             } );
471
472             result += '<td class="toolscol"><ul><li><a href="#" class="marc-link">' + _("View MARC") + '</a></li>';
473             result += '<li><a href="#" class="open-link">' + ( hit.server == 'koha:biblioserver' ? _("Edit") : _("Import") ) + '</a></li>';
474             if ( state.canSave ) result += '<li><a href="#" class="substitute-link" title="' + _("Replace the current record's contents") + '">' + _("Substitute") + '</a></li>';
475             result += '</ul></td></tr>';
476
477             var $tr = $( result );
478             $tr.find( '.marc-link' ).click( function() {
479                 var $info_columns = $tr.find( '.infocol' );
480                 var $marc_column = $tr.find( '.marccol' );
481
482                 if ( !$marc_column.length ) {
483                     $marc_column = $( '<td class="marccol" colspan="' + $info_columns.length + '"></td>' ).insertAfter( $info_columns.eq(-1) ).hide();
484                     CodeMirror.runMode( TextMARC.RecordToText( hit.record ), 'marc', $marc_column[0] );
485                 }
486
487                 if ( $marc_column.is(':visible') ) {
488                     $tr.find('.marc-link').text( _("View MARC") );
489                     $info_columns.show();
490                     $marc_column.hide();
491                 } else {
492                     $tr.find('.marc-link').text( _("Hide MARC") );
493                     $marc_column.show();
494                     $info_columns.hide();
495                 }
496
497                 return false;
498             } );
499             $tr.find( '.open-link' ).click( function() {
500                 $( '#search-results-ui' ).modal('hide');
501                 openRecord( hit.id, editor );
502
503                 return false;
504             } );
505             $tr.find( '.substitute-link' ).click( function() {
506                 $( '#search-results-ui' ).modal('hide');
507                 loadRecord( hit.id, editor );
508
509                 return false;
510             } );
511             $('#searchresults tbody').append( $tr );
512         } );
513
514         var pages = [];
515         var cur_page = data.offset / data.page_size;
516         var max_page = Math.ceil( data.total_fetched / data.page_size ) - 1;
517
518         if ( cur_page != 0 ) {
519             pages.push( '<li><a class="search-nav" href="#" data-offset="' + (data.offset - data.page_size) + '">&laquo; ' + _("Previous") + '</a></li>' );
520         }
521
522         for ( var page = Math.max( 0, cur_page - 9 ); page <= Math.min( max_page, cur_page + 9 ); page++ ) {
523             if ( page == cur_page ) {
524                 pages.push( ' <li class="active"><a href="#">' + ( page + 1 ) + '</a></li>' );
525             } else {
526                 pages.push( ' <li><a class="search-nav" href="#" data-offset="' + ( page * data.page_size ) + '">' + ( page + 1 ) + '</a></li>' );
527             }
528         }
529
530         if ( cur_page < max_page ) {
531             pages.push( ' <li><a class="search-nav" href="#" data-offset="' + (data.offset + data.page_size) + '">' + _("Next") + ' &raquo;</a></li>' );
532         }
533
534         if ( pages.length > 1 ) $( '#search-top-pages, #search-bottom-pages' ).find( '.pagination' ).html( '<ul>' + pages.join( '' ) + '</ul>');
535
536         var $overlay = $('#search-overlay');
537         $overlay.find('span').text(_("Loading"));
538         $overlay.find('.bar').css( { display: 'block', width: 100 * ( 1 - data.activeclients / Search.includedServers.length ) + '%' } );
539
540         if ( data.activeclients ) {
541             $overlay.find('.bar').css( { display: 'block', width: 100 * ( 1 - data.activeclients / Search.includedServers.length ) + '%' } );
542             $overlay.show();
543         } else {
544             $overlay.find('.bar').css( { display: 'block', width: '100%' } );
545             $overlay.fadeOut();
546             $('#searchresults')[0].focus();
547         }
548     }
549
550     function invalidateSearchResults() {
551         var $overlay = $('#search-overlay');
552         $overlay.find('span').text(_("Search expired, please try again"));
553         $overlay.find('.bar').css( { display: 'none' } );
554         $overlay.show();
555     }
556
557     function handleSearchError(error) {
558         if (error.code == 1) {
559             invalidateSearchResults();
560             Search.Reconnect();
561         } else {
562             humanMsg.displayMsg( _("<h3>Internal search error</h3>") + '<p>' + error + '</p>' + _("<p>Please <b>refresh</b> the page and try again."), { className: 'humanError' } );
563         }
564     }
565
566     function handleSearchInitError(error) {
567         $('#quicksearch-overlay').fadeIn().find('p').text(error);
568     }
569
570     // Preference functions
571     function showPreference( pref ) {
572         var value = Preferences.user[pref];
573
574         switch (pref) {
575             case 'fieldWidgets':
576                 $( '#set-field-widgets' ).text( value ? _("Show fields verbatim") : _("Show helpers for fixed and coded fields") );
577                 break;
578             case 'font':
579                 $( '#editor .CodeMirror' ).css( { fontFamily: value } );
580                 editor.refresh();
581                 break;
582             case 'fontSize':
583                 $( '#editor .CodeMirror' ).css( { fontSize: value } );
584                 editor.refresh();
585                 break;
586             case 'macros':
587                 // Macros loaded on first show of modal
588                 break;
589             case 'selected_search_targets':
590                 $.each( z3950Servers, function( server_id, server ) {
591                     var saved_val = Preferences.user.selected_search_targets[server_id];
592
593                     if ( saved_val != null ) server.checked = saved_val;
594                 } );
595                 break;
596         }
597     }
598
599     function bindPreference( editor, pref ) {
600         function _addHandler( sel, event, handler ) {
601             $( sel ).on( event, function (e) {
602                 e.preventDefault();
603                 handler( e, Preferences.user[pref] );
604                 Preferences.Save( [% USER_INFO.0.borrowernumber %] );
605                 showPreference(pref);
606             } );
607         }
608
609         switch (pref) {
610             case 'fieldWidgets':
611                 _addHandler( '#set-field-widgets', 'click', function( e, oldValue ) {
612                     editor.setUseWidgets( Preferences.user.fieldWidgets = !Preferences.user.fieldWidgets );
613                 } );
614                 break;
615             case 'font':
616                 _addHandler( '#prefs-menu .set-font', 'click', function( e, oldValue ) {
617                     Preferences.user.font = $( e.target ).css( 'font-family' );
618                 } );
619                 break;
620             case 'fontSize':
621                 _addHandler( '#prefs-menu .set-fontSize', 'click', function( e, oldValue ) {
622                     Preferences.user.fontSize = $( e.target ).css( 'font-size' );
623                 } );
624                 break;
625             case 'selected_search_targets':
626                 $( document ).on( 'change', 'input.search-toggle-server', function() {
627                     var server_id = $( this ).closest('li').data('server-id');
628                     Preferences.user.selected_search_targets[server_id] = this.checked;
629                     Preferences.Save( [% USER_INFO.0.borrowernumber %] );
630                 } );
631                 break;
632         }
633     }
634
635     function displayPreferences( editor ) {
636         $.each( Preferences.user, function( pref, value ) {
637             showPreference( pref );
638             bindPreference( editor, pref );
639         } );
640     }
641
642     //> Macro functions
643     function loadMacro( name ) {
644         $( '#macro-list li' ).removeClass( 'active' );
645         macroEditor.activeMacro = name;
646
647         if ( !name ) {
648             macroEditor.setValue( '' );
649             return;
650         }
651
652         $( '#macro-list li[data-name="' + name + '"]' ).addClass( 'active' );
653         var macro = Preferences.user.macros[name];
654         macroEditor.setValue( macro.contents );
655         $( '#macro-format' ).val( macro.format || 'its' );
656         if ( macro.history ) macroEditor.setHistory( macro.history );
657     }
658
659     function storeMacro( name, macro ) {
660         if ( macro ) {
661             Preferences.user.macros[name] = macro;
662         } else {
663             delete Preferences.user.macros[name];
664         }
665
666         Preferences.Save( [% USER_INFO.0.borrowernumber %] );
667     }
668
669     function showSavedMacros( macros ) {
670         var scrollTop = $('#macro-list').scrollTop();
671         $( '#macro-list' ).empty();
672         var macro_list = $.map( Preferences.user.macros, function( macro, name ) {
673             return $.extend( { name: name }, macro );
674         } );
675         macro_list.sort( function( a, b ) {
676             return a.name.localeCompare(b.name);
677         } );
678         $.each( macro_list, function( undef, macro ) {
679             var $li = $( '<li data-name="' + macro.name + '"><a href="#">' + macro.name + '</a><ol class="macro-info"></ol></li>' );
680             $li.click( function() {
681                 loadMacro(macro.name);
682                 return false;
683             } );
684             if ( macro.name == macroEditor.activeMacro ) $li.addClass( 'active' );
685             var modified = macro.modified && new Date(macro.modified);
686             $li.find( '.macro-info' ).append(
687                 '<li><span class="label">' + _("Last changed:") + '</span>' +
688                 ( modified ? ( modified.toLocaleDateString() + ', ' + modified.toLocaleTimeString() ) : _("never") ) + '</li>'
689             );
690             $('#macro-list').append($li);
691         } );
692         var $new_li = $( '<li class="new-macro"><a href="#">' + _("New macro...") + '</a></li>' );
693         $new_li.click( function() {
694             // TODO: make this a bit less retro
695             var name = prompt(_("Please enter the name for the new macro:"));
696             if (!name) return;
697
698             if ( !Preferences.user.macros[name] ) storeMacro( name, { format: "rancor", contents: "" } );
699             showSavedMacros();
700             loadMacro( name );
701         } );
702         $('#macro-list').append($new_li);
703         $('#macro-list').scrollTop(scrollTop);
704     }
705
706     function saveMacro() {
707         var name = macroEditor.activeMacro;
708
709         if ( !name || macroEditor.savedGeneration == macroEditor.changeGeneration() ) return;
710
711         macroEditor.savedGeneration = macroEditor.changeGeneration();
712         storeMacro( name, { contents: macroEditor.getValue(), modified: (new Date()).valueOf(), history: macroEditor.getHistory(), format: $('#macro-format').val() } );
713         $('#macro-save-message').text(_("Saved"));
714         showSavedMacros();
715     }
716
717     $(document).ready( function() {
718         // Editor setup
719         editor = new MARCEditor( {
720             onCursorActivity: function() {
721                 $('#status-tag-info').empty();
722                 $('#status-subfield-info').empty();
723
724                 var field = editor.getCurrentField();
725                 var cur = editor.getCursor();
726
727                 if ( !field ) return;
728
729                 var taginfo = KohaBackend.GetTagInfo( '', field.tag );
730                 $('#status-tag-info').html( '<strong>' + field.tag + ':</strong> ' );
731
732                 if ( taginfo ) {
733                     $('#status-tag-info').append( '<a href="' + getFieldHelpURL( field.tag ) + '" target="_blank" class="show-field-help" title="' + _("Show help for this tag") + '">[?]</a> '  + taginfo.lib );
734
735                     var subfield = field.getSubfieldAt( cur.ch );
736                     if ( !subfield ) return;
737
738                     var subfieldinfo = taginfo.subfields[ subfield.code ];
739                     $('#status-subfield-info').html( '<strong>‡' + subfield.code + ':</strong> ' );
740
741                     if ( subfieldinfo ) {
742                         $('#status-subfield-info').append( subfieldinfo.lib );
743                     } else {
744                         $('#status-subfield-info').append( '<em>' + _("Unknown subfield") + '</em>' );
745                     }
746                 } else {
747                     $('#status-tag-info').append( '<em>' + _("Unknown tag") + '</em>' );
748                 }
749             },
750             position: function (elt) { $(elt).insertAfter('#toolbar') },
751         } );
752
753         // Automatically detect resizes and change the height of the editor and position of modals.
754         var resizeTimer = null;
755         $( window ).resize( function() {
756             if ( resizeTimer == null ) resizeTimer = setTimeout( function() {
757                 resizeTimer = null;
758
759                 var pos = $('#editor .CodeMirror').position();
760                 $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 - $('#changelanguage').height() ); // 24 is hardcoded value but works well
761
762                 $('.modal-body').each( function() {
763                     $(this).height( $(window).height() * .8 - $(this).prevAll('.modal-header').height() );
764                 } );
765             }, 100);
766
767             $("#advanced-search-ui, #search-results-ui, #macro-ui").css( {
768                 marginLeft: function() {
769                     return -($(this).width() / 2);
770                 }
771             } );
772
773         } ).resize();
774
775         $( '#macro-ui' ).on( 'shown', function() {
776             if ( macroEditor ) return;
777
778             macroEditor = CodeMirror(
779                 $('#macro-editor')[0],
780                 {
781                     mode: 'null',
782                     lineNumbers: true,
783                 }
784             );
785             var saveTimeout;
786             macroEditor.on( 'change', function( cm, change ) {
787                 $('#macro-save-message').empty();
788                 if ( change.origin == 'setValue' ) return;
789
790                 if ( saveTimeout ) clearTimeout( saveTimeout );
791                 saveTimeout = setTimeout( function() {
792                     saveMacro();
793
794                     saveTimeout = null;
795                 }, 500 );
796             } );
797
798             showSavedMacros();
799         } );
800
801         var saveableBackends = [];
802         $.each( backends, function( id, backend ) {
803             if ( backend.save ) saveableBackends.push( [ backend.saveLabel, id ] );
804         } );
805         saveableBackends.sort();
806         $.each( saveableBackends, function( undef, backend ) {
807             $( '#save-dropdown' ).append( '<li><a href="#" data-backend="' + backend[1] + '">' + backend[0] + '</a></li>' );
808         } );
809
810         var macro_format_list = $.map( Macros.formats, function( format, name ) {
811             return $.extend( { name: name }, format );
812         } );
813         macro_format_list.sort( function( a, b ) {
814             return a.description.localeCompare(b.description);
815         } );
816         $.each( macro_format_list, function() {
817             $('#macro-format').append( '<option value="' + this.name + '">' + this.description + '</option>' );
818         } );
819
820         // Click bindings
821         $( '#save-record, #save-dropdown a' ).click( function() {
822             $( '#save-record' ).find('i').attr( 'class', 'icon-loading' ).siblings( 'span' ).text( _("Saving...") );
823
824             function finishCb(result) {
825                 if ( result.error == 'syntax' ) {
826                     humanMsg.displayAlert( _("Incorrect syntax, cannot save"), { className: 'humanError' } );
827                 } else if ( result.error == 'invalid' ) {
828                     humanMsg.displayAlert( _("Record structure invalid, cannot save"), { className: 'humanError' } );
829                 } else if ( !result.error ) {
830                     humanMsg.displayAlert( _("Record saved "), { className: 'humanSuccess' } );
831                 }
832
833                 $.each( result.errors || [], function( undef, error ) {
834                     switch ( error.type ) {
835                         case 'noTag':
836                             editor.addError( error.line, _("Invalid tag number") );
837                             break;
838                         case 'noIndicators':
839                             editor.addError( error.line, _("Invalid indicators") );
840                             break;
841                         case 'noSubfields':
842                             editor.addError( error.line, _("Tag has no subfields") );
843                             break;
844                         case 'missingTag':
845                             editor.addError( null, _("Missing mandatory tag: ") + error.tag );
846                             break;
847                         case 'missingSubfield':
848                             if ( error.subfield == '@' ) {
849                                 editor.addError( error.line, _("Missing control field contents") );
850                             } else {
851                                 editor.addError( error.line, _("Missing mandatory subfield: â€¡") + error.subfield );
852                             }
853                             break;
854                         case 'unrepeatableTag':
855                             editor.addError( error.line, _("Tag ") + error.tag + _(" cannot be repeated") );
856                             break;
857                         case 'unrepeatableSubfield':
858                             editor.addError( error.line, _("Subfield â€¡") + error.subfield + _(" cannot be repeated") );
859                             break;
860                         case 'itemTagUnsupported':
861                             editor.addError( error.line, _("Item tags cannot currently be saved") );
862                             break;
863                     }
864                 } );
865
866                 $( '#save-record' ).find('i').attr( 'class', 'icon-hdd' );
867
868                 if ( result.error ) {
869                     // Reset backend info
870                     setSource( [ state.backend, state.recordID ] );
871                 }
872             }
873
874             var backend = $( this ).data( 'backend' ) || ( state.saveBackend );
875             if ( state.backend == backend ) {
876                 saveRecord( backend + '/' + state.recordID, editor, finishCb );
877             } else {
878                 saveRecord( backend + '/', editor, finishCb );
879             }
880
881             return false;
882         } );
883
884         $('#import-records').click( function() {
885             $('#import-records-input')
886                 .off('change')
887                 .change( function() {
888                     if ( !this.files || !this.files.length ) return;
889
890                     var file = this.files[0];
891                     var reader = new FileReader();
892
893                     reader.onload = function() {
894                         var record = new MARC.Record();
895
896                         if ( /\.(mrc|marc|iso|iso2709|marcstd)$/.test( file.name ) ) {
897                             record.loadISO2709( reader.result );
898                         } else if ( /\.(xml|marcxml)$/.test( file.name ) ) {
899                             record.loadMARCXML( reader.result );
900                         } else {
901                             humanMsg.displayAlert( _("Unknown record type, cannot import"), { className: 'humanError' } );
902                             return;
903                         }
904
905                         editor.displayRecord( record );
906                     };
907
908                     reader.readAsText( file );
909                 } )
910                 .click();
911
912             return false;
913         } );
914
915         $('#open-macros').click( function() {
916             $('#macro-ui').modal('show');
917
918             return false;
919         } );
920
921         $('#run-macro').click( function() {
922             var result = Macros.Run( editor, $('#macro-format').val(), macroEditor.getValue() );
923
924             if ( !result.errors.length ) {
925                 $('#macro-ui').modal('hide');
926                 return false;
927             }
928
929             var errors = [];
930             $.each( result.errors, function() {
931                 var error = '<b>' + _("Line ") + (this.line + 1) + ':</b> ';
932
933                 switch ( this.error ) {
934                     case 'failed': error += _("failed to run"); break;
935                     case 'unrecognized': error += _("unrecognized command"); break;
936                 }
937
938                 errors.push(error);
939             } );
940
941             humanMsg.displayMsg( _("<h3>Failed to run macro:</h3>") + '<ul><li>' + errors.join('</li><li>') + '</li></ul>', { className: 'humanError' } );
942
943             return false;
944         } );
945
946         $('#delete-macro').click( function() {
947             if ( !macroEditor.activeMacro || !confirm( _("Are you sure you want to delete this macro?") ) ) return;
948
949             storeMacro( macroEditor.activeMacro, undefined );
950             showSavedMacros();
951             loadMacro( undefined );
952
953             return false;
954         } );
955
956         $( '#switch-editor' ).click( function() {
957             if ( !confirm( _("Any changes will not be saved. Continue?") ) ) return;
958
959             $.cookie( 'catalogue_editor_[% USER_INFO.0.borrowernumber %]', 'basic', { expires: 365, path: '/' } );
960
961             if ( state.backend == 'catalog' ) {
962                 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=' + state.recordID;
963             } else if ( state.backend == 'new' ) {
964                 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl';
965             } else {
966                 humanMsg.displayAlert( _("Cannot open this record in the basic editor"), { className: 'humanError' } );
967             }
968         } );
969
970         $( '#show-advanced-search' ).click( function() {
971             showAdvancedSearch();
972
973             return false;
974         } );
975
976         $('#advanced-search').submit( function() {
977             startAdvancedSearch();
978
979             return false;
980         } );
981
982         $( document ).on( 'click', 'a.search-nav', function() {
983             if ( Search.Fetch( { offset: $( this ).data( 'offset' ) } ) ) {
984                 $("#search-overlay").show();
985             }
986
987             return false;
988         });
989
990         $( document ).on( 'click', 'th[data-sort-label]', function() {
991             var direction;
992
993             if ( $( this ).hasClass( 'sorting_asc' ) ) {
994                 direction = 'desc';
995             } else {
996                 direction = 'asc';
997             }
998
999             if ( Search.Fetch( { sort_key: $( this ).data( 'sort-label' ), sort_direction: direction } ) ) {
1000                 showSearchSorting( $( this ).data( 'sort-label' ), direction );
1001
1002                 $("#search-overlay").show();
1003             }
1004
1005             return false;
1006         });
1007
1008         $( document ).on( 'change', 'input.search-toggle-server', function() {
1009             var server = z3950Servers[ $( this ).closest('li').data('server-id') ];
1010             server.checked = this.checked;
1011
1012             if ( $('#search-results-ui').is( ':visible' ) && Search.Fetch() ) {
1013                 $("#search-overlay").show();
1014             }
1015         } );
1016
1017         // Key bindings
1018         bindGlobalKeys();
1019
1020         // Setup UI
1021         $("#advanced-search-ui, #search-results-ui, #macro-ui").each( function() {
1022             $(this).modal({ show: false });
1023         } );
1024
1025         var $quicksearch = $('#quicksearch fieldset');
1026         $('<div id="quicksearch-overlay"><h3>' + _("Search unavailable") + '</h3> <p></p></div>').css({
1027             position: 'absolute',
1028             top: $quicksearch.offset().top,
1029             left: $quicksearch.offset().left,
1030             height: $quicksearch.outerHeight(),
1031             width: $quicksearch.outerWidth(),
1032         }).appendTo(document.body).hide();
1033
1034         var prevAlerts = [];
1035         humanMsg.logMsg = function(msg, options) {
1036             $('#show-alerts').popover('hide');
1037             prevAlerts.unshift('<li>' + msg + '</li>');
1038             prevAlerts.splice(5, 999); // Truncate old messages
1039         };
1040
1041         $('#show-alerts').popover({
1042             html: true,
1043             placement: 'bottom',
1044             content: function() {
1045                 return '<div id="alerts-container"><ul>' + prevAlerts.join('') + '</ul></div>';
1046             },
1047         });
1048
1049         $('#show-shortcuts').popover({
1050             html: true,
1051             placement: 'bottom',
1052             content: function() {
1053                 return '<div id="shortcuts-container">' + $('#shortcuts-contents').html() + '</div>';
1054             },
1055         });
1056
1057         $('#new-record' ).click( function() {
1058             if ( editor.modified && !confirm( _("Are you sure you want to erase your changes?") ) ) return;
1059
1060             openRecord( 'new/', editor );
1061             return false;
1062         } );
1063
1064         // Start editor
1065         Preferences.Load( [% USER_INFO.0.borrowernumber || 0 %] );
1066         displayPreferences(editor);
1067         makeAuthorisedValueWidgets( '' );
1068         Search.Init( {
1069             page_size: 20,
1070             onresults: function(data) { showSearchResults( editor, data ) },
1071             onerror: handleSearchError,
1072         } );
1073
1074         function finishCb( data ) {
1075             if ( data.error ) openRecord( 'new/', editor, finishCb );
1076
1077             Resources.GetAll().done( function() {
1078                 $("#loading").hide();
1079                 editor.focus();
1080             } );
1081         }
1082
1083         if ( "[% auth_forwarded_hash %]" ) {
1084             document.location.hash = "[% auth_forwarded_hash %]";
1085         }
1086
1087         if ( !document.location.hash || !openRecord( document.location.hash.slice(1), editor, finishCb ) ) {
1088             openRecord( 'new/', editor, finishCb );
1089         }
1090     } );
1091 } )();
1092
1093 </script>