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