Bug 11559: (QA followup) fix QA issues
[koha.git] / koha-tmpl / intranet-tmpl / prog / en / includes / cateditor-ui.inc
1 <script src="[% interface %]/lib/codemirror/codemirror-compressed.js"></script>
2 <script src="[% interface %]/lib/filesaver.js"></script>
3 <script src="[% interface %]/lib/koha/cateditor/marc-mode.js"></script>
4 <script src="[% interface %]/lib/require.js"></script>
5 <script>
6 require.config( {
7     baseUrl: '[% interface %]/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         $( '#search-top-pages, #search-bottom-pages' ).find( '.pagination' ).html( pages.length > 1 ? ( '<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                     extraKeys: {
782                         'Ctrl-D': function( cm ) {
783                             var cur = cm.getCursor();
784
785                             cm.replaceRange( "‡", cur, null );
786                         },
787                     },
788                     mode: 'null',
789                     lineNumbers: true,
790                 }
791             );
792             var saveTimeout;
793             macroEditor.on( 'change', function( cm, change ) {
794                 $('#macro-save-message').empty();
795                 if ( change.origin == 'setValue' ) return;
796
797                 if ( saveTimeout ) clearTimeout( saveTimeout );
798                 saveTimeout = setTimeout( function() {
799                     saveMacro();
800
801                     saveTimeout = null;
802                 }, 500 );
803             } );
804
805             showSavedMacros();
806         } );
807
808         var saveableBackends = [];
809         $.each( backends, function( id, backend ) {
810             if ( backend.save ) saveableBackends.push( [ backend.saveLabel, id ] );
811         } );
812         saveableBackends.sort();
813         $.each( saveableBackends, function( undef, backend ) {
814             $( '#save-dropdown' ).append( '<li><a href="#" data-backend="' + backend[1] + '">' + backend[0] + '</a></li>' );
815         } );
816
817         var macro_format_list = $.map( Macros.formats, function( format, name ) {
818             return $.extend( { name: name }, format );
819         } );
820         macro_format_list.sort( function( a, b ) {
821             return a.description.localeCompare(b.description);
822         } );
823         $.each( macro_format_list, function() {
824             $('#macro-format').append( '<option value="' + this.name + '">' + this.description + '</option>' );
825         } );
826
827         // Click bindings
828         $( '#save-record, #save-dropdown a' ).click( function() {
829             $( '#save-record' ).find('i').attr( 'class', 'icon-loading' ).siblings( 'span' ).text( _("Saving...") );
830
831             function finishCb(result) {
832                 if ( result.error == 'syntax' ) {
833                     humanMsg.displayAlert( _("Incorrect syntax, cannot save"), { className: 'humanError' } );
834                 } else if ( result.error == 'invalid' ) {
835                     humanMsg.displayAlert( _("Record structure invalid, cannot save"), { className: 'humanError' } );
836                 } else if ( !result.error ) {
837                     humanMsg.displayAlert( _("Record saved "), { className: 'humanSuccess' } );
838                 }
839
840                 $.each( result.errors || [], function( undef, error ) {
841                     switch ( error.type ) {
842                         case 'noTag':
843                             editor.addError( error.line, _("Invalid tag number") );
844                             break;
845                         case 'noIndicators':
846                             editor.addError( error.line, _("Invalid indicators") );
847                             break;
848                         case 'noSubfields':
849                             editor.addError( error.line, _("Tag has no subfields") );
850                             break;
851                         case 'missingTag':
852                             editor.addError( null, _("Missing mandatory tag: ") + error.tag );
853                             break;
854                         case 'missingSubfield':
855                             if ( error.subfield == '@' ) {
856                                 editor.addError( error.line, _("Missing control field contents") );
857                             } else {
858                                 editor.addError( error.line, _("Missing mandatory subfield: â€¡") + error.subfield );
859                             }
860                             break;
861                         case 'unrepeatableTag':
862                             editor.addError( error.line, _("Tag ") + error.tag + _(" cannot be repeated") );
863                             break;
864                         case 'unrepeatableSubfield':
865                             editor.addError( error.line, _("Subfield â€¡") + error.subfield + _(" cannot be repeated") );
866                             break;
867                         case 'itemTagUnsupported':
868                             editor.addError( error.line, _("Item tags cannot currently be saved") );
869                             break;
870                     }
871                 } );
872
873                 $( '#save-record' ).find('i').attr( 'class', 'icon-hdd' );
874
875                 if ( result.error ) {
876                     // Reset backend info
877                     setSource( [ state.backend, state.recordID ] );
878                 }
879             }
880
881             var backend = $( this ).data( 'backend' ) || ( state.saveBackend );
882             if ( state.backend == backend ) {
883                 saveRecord( backend + '/' + state.recordID, editor, finishCb );
884             } else {
885                 saveRecord( backend + '/', editor, finishCb );
886             }
887
888             return false;
889         } );
890
891         $('#import-records').click( function() {
892             $('#import-records-input')
893                 .off('change')
894                 .change( function() {
895                     if ( !this.files || !this.files.length ) return;
896
897                     var file = this.files[0];
898                     var reader = new FileReader();
899
900                     reader.onload = function() {
901                         var record = new MARC.Record();
902
903                         if ( /\.(mrc|marc|iso|iso2709|marcstd)$/.test( file.name ) ) {
904                             record.loadISO2709( reader.result );
905                         } else if ( /\.(xml|marcxml)$/.test( file.name ) ) {
906                             record.loadMARCXML( reader.result );
907                         } else {
908                             humanMsg.displayAlert( _("Unknown record type, cannot import"), { className: 'humanError' } );
909                             return;
910                         }
911
912                         if (record.marc8_corrupted) humanMsg.displayMsg( '<h3>' + _("Possible record corruption") + '</h3><p>' + _("Record not marked as UTF-8, may be corrupted") + '</p>', { className: 'humanError' } );
913
914                         editor.displayRecord( record );
915                     };
916
917                     reader.readAsText( file );
918                 } )
919                 .click();
920
921             return false;
922         } );
923
924         $('#open-macros').click( function() {
925             $('#macro-ui').modal('show');
926
927             return false;
928         } );
929
930         $('#run-macro').click( function() {
931             var result = Macros.Run( editor, $('#macro-format').val(), macroEditor.getValue() );
932
933             if ( !result.errors.length ) {
934                 $('#macro-ui').modal('hide');
935                 return false;
936             }
937
938             var errors = [];
939             $.each( result.errors, function() {
940                 var error = '<b>' + _("Line ") + (this.line + 1) + ':</b> ';
941
942                 switch ( this.error ) {
943                     case 'failed': error += _("failed to run"); break;
944                     case 'unrecognized': error += _("unrecognized command"); break;
945                 }
946
947                 errors.push(error);
948             } );
949
950             humanMsg.displayMsg( _("<h3>Failed to run macro:</h3>") + '<ul><li>' + errors.join('</li><li>') + '</li></ul>', { className: 'humanError' } );
951
952             return false;
953         } );
954
955         $('#delete-macro').click( function() {
956             if ( !macroEditor.activeMacro || !confirm( _("Are you sure you want to delete this macro?") ) ) return;
957
958             storeMacro( macroEditor.activeMacro, undefined );
959             showSavedMacros();
960             loadMacro( undefined );
961
962             return false;
963         } );
964
965         $( '#switch-editor' ).click( function() {
966             if ( !confirm( _("Any changes will not be saved. Continue?") ) ) return;
967
968             $.cookie( 'catalogue_editor_[% USER_INFO.0.borrowernumber %]', 'basic', { expires: 365, path: '/' } );
969
970             if ( state.backend == 'catalog' ) {
971                 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=' + state.recordID;
972             } else if ( state.backend == 'new' ) {
973                 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl';
974             } else {
975                 humanMsg.displayAlert( _("Cannot open this record in the basic editor"), { className: 'humanError' } );
976             }
977         } );
978
979         $( '#show-advanced-search' ).click( function() {
980             showAdvancedSearch();
981
982             return false;
983         } );
984
985         $('#advanced-search').submit( function() {
986             startAdvancedSearch();
987
988             return false;
989         } );
990
991         $( document ).on( 'click', 'a.search-nav', function() {
992             if ( Search.Fetch( { offset: $( this ).data( 'offset' ) } ) ) {
993                 $("#search-overlay").show();
994             }
995
996             return false;
997         });
998
999         $( document ).on( 'click', 'th[data-sort-label]', function() {
1000             var direction;
1001
1002             if ( $( this ).hasClass( 'sorting_asc' ) ) {
1003                 direction = 'desc';
1004             } else {
1005                 direction = 'asc';
1006             }
1007
1008             if ( Search.Fetch( { sort_key: $( this ).data( 'sort-label' ), sort_direction: direction } ) ) {
1009                 showSearchSorting( $( this ).data( 'sort-label' ), direction );
1010
1011                 $("#search-overlay").show();
1012             }
1013
1014             return false;
1015         });
1016
1017         $( document ).on( 'change', 'input.search-toggle-server', function() {
1018             var server = z3950Servers[ $( this ).closest('li').data('server-id') ];
1019             server.checked = this.checked;
1020
1021             if ( $('#search-results-ui').is( ':visible' ) && Search.Fetch() ) {
1022                 $("#search-overlay").show();
1023             }
1024         } );
1025
1026         // Key bindings
1027         bindGlobalKeys();
1028
1029         // Setup UI
1030         $("#advanced-search-ui, #search-results-ui, #macro-ui").each( function() {
1031             $(this).modal({ show: false });
1032         } );
1033
1034         var $quicksearch = $('#quicksearch fieldset');
1035         $('<div id="quicksearch-overlay"><h3>' + _("Search unavailable") + '</h3> <p></p></div>').css({
1036             position: 'absolute',
1037             top: $quicksearch.offset().top,
1038             left: $quicksearch.offset().left,
1039             height: $quicksearch.outerHeight(),
1040             width: $quicksearch.outerWidth(),
1041         }).appendTo(document.body).hide();
1042
1043         var prevAlerts = [];
1044         humanMsg.logMsg = function(msg, options) {
1045             $('#show-alerts').popover('hide');
1046             prevAlerts.unshift('<li>' + msg + '</li>');
1047             prevAlerts.splice(5, 999); // Truncate old messages
1048         };
1049
1050         $('#show-alerts').popover({
1051             html: true,
1052             placement: 'bottom',
1053             content: function() {
1054                 return '<div id="alerts-container"><ul>' + prevAlerts.join('') + '</ul></div>';
1055             },
1056         });
1057
1058         $('#show-shortcuts').popover({
1059             html: true,
1060             placement: 'bottom',
1061             content: function() {
1062                 return '<div id="shortcuts-container">' + $('#shortcuts-contents').html() + '</div>';
1063             },
1064         });
1065
1066         $('#new-record' ).click( function() {
1067             if ( editor.modified && !confirm( _("Are you sure you want to erase your changes?") ) ) return;
1068
1069             openRecord( 'new/', editor );
1070             return false;
1071         } );
1072
1073         // Start editor
1074         Preferences.Load( [% USER_INFO.0.borrowernumber || 0 %] );
1075         displayPreferences(editor);
1076         makeAuthorisedValueWidgets( '' );
1077         Search.Init( {
1078             page_size: 20,
1079             onresults: function(data) { showSearchResults( editor, data ) },
1080             onerror: handleSearchError,
1081         } );
1082
1083         function finishCb( data ) {
1084             if ( data.error ) openRecord( 'new/', editor, finishCb );
1085
1086             Resources.GetAll().done( function() {
1087                 $("#loading").hide();
1088                 editor.focus();
1089             } );
1090         }
1091
1092         if ( "[% auth_forwarded_hash %]" ) {
1093             document.location.hash = "[% auth_forwarded_hash %]";
1094         }
1095
1096         if ( !document.location.hash || !openRecord( document.location.hash.slice(1), editor, finishCb ) ) {
1097             openRecord( 'new/', editor, finishCb );
1098         }
1099     } );
1100 } )();
1101
1102 </script>