Bug 11559: (QA followup) fix several small issues
[koha-equinox.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         macroEditor.setOption( 'readOnly', false );
656         $( '#macro-format' ).val( macro.format || 'its' );
657         if ( macro.history ) macroEditor.setHistory( macro.history );
658     }
659
660     function storeMacro( name, macro ) {
661         if ( macro ) {
662             Preferences.user.macros[name] = macro;
663         } else {
664             delete Preferences.user.macros[name];
665         }
666
667         Preferences.Save( [% USER_INFO.0.borrowernumber %] );
668     }
669
670     function showSavedMacros( macros ) {
671         var scrollTop = $('#macro-list').scrollTop();
672         $( '#macro-list' ).empty();
673         var macro_list = $.map( Preferences.user.macros, function( macro, name ) {
674             return $.extend( { name: name }, macro );
675         } );
676         macro_list.sort( function( a, b ) {
677             return a.name.localeCompare(b.name);
678         } );
679         $.each( macro_list, function( undef, macro ) {
680             var $li = $( '<li data-name="' + macro.name + '"><a href="#">' + macro.name + '</a><ol class="macro-info"></ol></li>' );
681             $li.click( function() {
682                 loadMacro(macro.name);
683                 return false;
684             } );
685             if ( macro.name == macroEditor.activeMacro ) $li.addClass( 'active' );
686             var modified = macro.modified && new Date(macro.modified);
687             $li.find( '.macro-info' ).append(
688                 '<li><span class="label">' + _("Last changed:") + '</span>' +
689                 ( modified ? ( modified.toLocaleDateString() + ', ' + modified.toLocaleTimeString() ) : _("never") ) + '</li>'
690             );
691             $('#macro-list').append($li);
692         } );
693         var $new_li = $( '<li class="new-macro"><a href="#">' + _("New macro...") + '</a></li>' );
694         $new_li.click( function() {
695             // TODO: make this a bit less retro
696             var name = prompt(_("Please enter the name for the new macro:"));
697             if (!name) return;
698
699             if ( !Preferences.user.macros[name] ) storeMacro( name, { format: "rancor", contents: "" } );
700             showSavedMacros();
701             loadMacro( name );
702         } );
703         $('#macro-list').append($new_li);
704         $('#macro-list').scrollTop(scrollTop);
705     }
706
707     function saveMacro() {
708         var name = macroEditor.activeMacro;
709
710         if ( !name || macroEditor.savedGeneration == macroEditor.changeGeneration() ) return;
711
712         macroEditor.savedGeneration = macroEditor.changeGeneration();
713         storeMacro( name, { contents: macroEditor.getValue(), modified: (new Date()).valueOf(), history: macroEditor.getHistory(), format: $('#macro-format').val() } );
714         $('#macro-save-message').text(_("Saved"));
715         showSavedMacros();
716     }
717
718     $(document).ready( function() {
719         // Editor setup
720         editor = new MARCEditor( {
721             onCursorActivity: function() {
722                 $('#status-tag-info').empty();
723                 $('#status-subfield-info').empty();
724
725                 var field = editor.getCurrentField();
726                 var cur = editor.getCursor();
727
728                 if ( !field ) return;
729
730                 var taginfo = KohaBackend.GetTagInfo( '', field.tag );
731                 $('#status-tag-info').html( '<strong>' + field.tag + ':</strong> ' );
732
733                 if ( taginfo ) {
734                     $('#status-tag-info').append( '<a href="' + getFieldHelpURL( field.tag ) + '" target="_blank" class="show-field-help" title="' + _("Show help for this tag") + '">[?]</a> '  + taginfo.lib );
735
736                     var subfield = field.getSubfieldAt( cur.ch );
737                     if ( !subfield ) return;
738
739                     var subfieldinfo = taginfo.subfields[ subfield.code ];
740                     $('#status-subfield-info').html( '<strong>‡' + subfield.code + ':</strong> ' );
741
742                     if ( subfieldinfo ) {
743                         $('#status-subfield-info').append( subfieldinfo.lib );
744                     } else {
745                         $('#status-subfield-info').append( '<em>' + _("Unknown subfield") + '</em>' );
746                     }
747                 } else {
748                     $('#status-tag-info').append( '<em>' + _("Unknown tag") + '</em>' );
749                 }
750             },
751             position: function (elt) { $(elt).insertAfter('#toolbar') },
752         } );
753
754         // Automatically detect resizes and change the height of the editor and position of modals.
755         var resizeTimer = null;
756         function onResize() {
757             if ( resizeTimer == null ) resizeTimer = setTimeout( function() {
758                 resizeTimer = null;
759
760                 var pos = $('#editor .CodeMirror').position();
761                 $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 - $('#changelanguage').height() ); // 24 is hardcoded value but works well
762
763                 $('.modal-body').each( function() {
764                     $(this).height( $(window).height() * .8 - $(this).prevAll('.modal-header').height() );
765                 } );
766             }, 100);
767
768             $("#advanced-search-ui, #search-results-ui, #macro-ui").css( {
769                 marginLeft: function() {
770                     return -($(this).width() / 2);
771                 }
772             } );
773         }
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                     readOnly: true,
791                 }
792             );
793             var saveTimeout;
794             macroEditor.on( 'change', function( cm, change ) {
795                 $('#macro-save-message').empty();
796                 if ( change.origin == 'setValue' ) return;
797
798                 if ( saveTimeout ) clearTimeout( saveTimeout );
799                 saveTimeout = setTimeout( function() {
800                     saveMacro();
801
802                     saveTimeout = null;
803                 }, 500 );
804             } );
805
806             showSavedMacros();
807         } );
808
809         var saveableBackends = [];
810         $.each( backends, function( id, backend ) {
811             if ( backend.save ) saveableBackends.push( [ backend.saveLabel, id ] );
812         } );
813         saveableBackends.sort();
814         $.each( saveableBackends, function( undef, backend ) {
815             $( '#save-dropdown' ).append( '<li><a href="#" data-backend="' + backend[1] + '">' + backend[0] + '</a></li>' );
816         } );
817
818         var macro_format_list = $.map( Macros.formats, function( format, name ) {
819             return $.extend( { name: name }, format );
820         } );
821         macro_format_list.sort( function( a, b ) {
822             return a.description.localeCompare(b.description);
823         } );
824         $.each( macro_format_list, function() {
825             $('#macro-format').append( '<option value="' + this.name + '">' + this.description + '</option>' );
826         } );
827
828         // Click bindings
829         $( '#save-record, #save-dropdown a' ).click( function() {
830             $( '#save-record' ).find('i').attr( 'class', 'icon-loading' ).siblings( 'span' ).text( _("Saving...") );
831
832             function finishCb(result) {
833                 if ( result.error == 'syntax' ) {
834                     humanMsg.displayAlert( _("Incorrect syntax, cannot save"), { className: 'humanError' } );
835                 } else if ( result.error == 'invalid' ) {
836                     humanMsg.displayAlert( _("Record structure invalid, cannot save"), { className: 'humanError' } );
837                 } else if ( !result.error ) {
838                     humanMsg.displayAlert( _("Record saved "), { className: 'humanSuccess' } );
839                 }
840
841                 $.each( result.errors || [], function( undef, error ) {
842                     switch ( error.type ) {
843                         case 'noTag':
844                             editor.addError( error.line, _("Invalid tag number") );
845                             break;
846                         case 'noIndicators':
847                             editor.addError( error.line, _("Invalid indicators") );
848                             break;
849                         case 'noSubfields':
850                             editor.addError( error.line, _("Tag has no subfields") );
851                             break;
852                         case 'missingTag':
853                             editor.addError( null, _("Missing mandatory tag: ") + error.tag );
854                             break;
855                         case 'missingSubfield':
856                             if ( error.subfield == '@' ) {
857                                 editor.addError( error.line, _("Missing control field contents") );
858                             } else {
859                                 editor.addError( error.line, _("Missing mandatory subfield: â€¡") + error.subfield );
860                             }
861                             break;
862                         case 'unrepeatableTag':
863                             editor.addError( error.line, _("Tag ") + error.tag + _(" cannot be repeated") );
864                             break;
865                         case 'unrepeatableSubfield':
866                             editor.addError( error.line, _("Subfield â€¡") + error.subfield + _(" cannot be repeated") );
867                             break;
868                         case 'itemTagUnsupported':
869                             editor.addError( error.line, _("Item tags cannot currently be saved") );
870                             break;
871                     }
872                 } );
873
874                 $( '#save-record' ).find('i').attr( 'class', 'icon-hdd' );
875
876                 if ( result.error ) {
877                     // Reset backend info
878                     setSource( [ state.backend, state.recordID ] );
879                 }
880             }
881
882             var backend = $( this ).data( 'backend' ) || ( state.saveBackend );
883             if ( state.backend == backend ) {
884                 saveRecord( backend + '/' + state.recordID, editor, finishCb );
885             } else {
886                 saveRecord( backend + '/', editor, finishCb );
887             }
888
889             return false;
890         } );
891
892         $('#import-records').click( function() {
893             $('#import-records-input')
894                 .off('change')
895                 .change( function() {
896                     if ( !this.files || !this.files.length ) return;
897
898                     var file = this.files[0];
899                     var reader = new FileReader();
900
901                     reader.onload = function() {
902                         var record = new MARC.Record();
903
904                         if ( /\.(mrc|marc|iso|iso2709|marcstd)$/.test( file.name ) ) {
905                             record.loadISO2709( reader.result );
906                         } else if ( /\.(xml|marcxml)$/.test( file.name ) ) {
907                             record.loadMARCXML( reader.result );
908                         } else {
909                             humanMsg.displayAlert( _("Unknown record type, cannot import"), { className: 'humanError' } );
910                             return;
911                         }
912
913                         if (record.marc8_corrupted) humanMsg.displayMsg( '<h3>' + _("Possible record corruption") + '</h3><p>' + _("Record not marked as UTF-8, may be corrupted") + '</p>', { className: 'humanError' } );
914
915                         editor.displayRecord( record );
916                     };
917
918                     reader.readAsText( file );
919                 } )
920                 .click();
921
922             return false;
923         } );
924
925         $('#open-macros').click( function() {
926             $('#macro-ui').modal('show');
927
928             return false;
929         } );
930
931         $('#run-macro').click( function() {
932             var result = Macros.Run( editor, $('#macro-format').val(), macroEditor.getValue() );
933
934             if ( !result.errors.length ) {
935                 $('#macro-ui').modal('hide');
936                 return false;
937             }
938
939             var errors = [];
940             $.each( result.errors, function() {
941                 var error = '<b>' + _("Line ") + (this.line + 1) + ':</b> ';
942
943                 switch ( this.error ) {
944                     case 'failed': error += _("failed to run"); break;
945                     case 'unrecognized': error += _("unrecognized command"); break;
946                 }
947
948                 errors.push(error);
949             } );
950
951             humanMsg.displayMsg( _("<h3>Failed to run macro:</h3>") + '<ul><li>' + errors.join('</li><li>') + '</li></ul>', { className: 'humanError' } );
952
953             return false;
954         } );
955
956         $('#delete-macro').click( function() {
957             if ( !macroEditor.activeMacro || !confirm( _("Are you sure you want to delete this macro?") ) ) return;
958
959             storeMacro( macroEditor.activeMacro, undefined );
960             showSavedMacros();
961             loadMacro( undefined );
962
963             return false;
964         } );
965
966         $( '#switch-editor' ).click( function() {
967             if ( !confirm( _("Any changes will not be saved. Continue?") ) ) return;
968
969             $.cookie( 'catalogue_editor_[% USER_INFO.0.borrowernumber %]', 'basic', { expires: 365, path: '/' } );
970
971             if ( state.backend == 'catalog' ) {
972                 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=' + state.recordID;
973             } else if ( state.backend == 'new' ) {
974                 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl';
975             } else {
976                 humanMsg.displayAlert( _("Cannot open this record in the basic editor"), { className: 'humanError' } );
977             }
978         } );
979
980         $( '#show-advanced-search' ).click( function() {
981             showAdvancedSearch();
982
983             return false;
984         } );
985
986         $('#advanced-search').submit( function() {
987             startAdvancedSearch();
988
989             return false;
990         } );
991
992         $( document ).on( 'click', 'a.search-nav', function() {
993             if ( Search.Fetch( { offset: $( this ).data( 'offset' ) } ) ) {
994                 $("#search-overlay").show();
995             }
996
997             return false;
998         });
999
1000         $( document ).on( 'click', 'th[data-sort-label]', function() {
1001             var direction;
1002
1003             if ( $( this ).hasClass( 'sorting_asc' ) ) {
1004                 direction = 'desc';
1005             } else {
1006                 direction = 'asc';
1007             }
1008
1009             if ( Search.Fetch( { sort_key: $( this ).data( 'sort-label' ), sort_direction: direction } ) ) {
1010                 showSearchSorting( $( this ).data( 'sort-label' ), direction );
1011
1012                 $("#search-overlay").show();
1013             }
1014
1015             return false;
1016         });
1017
1018         $( document ).on( 'change', 'input.search-toggle-server', function() {
1019             var server = z3950Servers[ $( this ).closest('li').data('server-id') ];
1020             server.checked = this.checked;
1021
1022             if ( $('#search-results-ui').is( ':visible' ) && Search.Fetch() ) {
1023                 $("#search-overlay").show();
1024             }
1025         } );
1026
1027         // Key bindings
1028         bindGlobalKeys();
1029
1030         // Setup UI
1031         $("#advanced-search-ui, #search-results-ui, #macro-ui").each( function() {
1032             $(this).modal({ show: false });
1033         } );
1034
1035         var $quicksearch = $('#quicksearch fieldset');
1036         $('<div id="quicksearch-overlay"><h3>' + _("Search unavailable") + '</h3> <p></p></div>').css({
1037             position: 'absolute',
1038             top: $quicksearch.offset().top,
1039             left: $quicksearch.offset().left,
1040             height: $quicksearch.outerHeight(),
1041             width: $quicksearch.outerWidth(),
1042         }).appendTo(document.body).hide();
1043
1044         var prevAlerts = [];
1045         humanMsg.logMsg = function(msg, options) {
1046             $('#show-alerts').popover('hide');
1047             prevAlerts.unshift('<li>' + msg + '</li>');
1048             prevAlerts.splice(5, 999); // Truncate old messages
1049         };
1050
1051         $('#show-alerts').popover({
1052             html: true,
1053             placement: 'bottom',
1054             content: function() {
1055                 return '<div id="alerts-container"><ul>' + prevAlerts.join('') + '</ul></div>';
1056             },
1057         });
1058
1059         $('#show-shortcuts').popover({
1060             html: true,
1061             placement: 'bottom',
1062             content: function() {
1063                 return '<div id="shortcuts-container">' + $('#shortcuts-contents').html() + '</div>';
1064             },
1065         });
1066
1067         $('#new-record' ).click( function() {
1068             if ( editor.modified && !confirm( _("Are you sure you want to erase your changes?") ) ) return;
1069
1070             openRecord( 'new/', editor );
1071             return false;
1072         } );
1073
1074         // Start editor
1075         Preferences.Load( [% USER_INFO.0.borrowernumber || 0 %] );
1076         displayPreferences(editor);
1077         makeAuthorisedValueWidgets( '' );
1078         Search.Init( {
1079             page_size: 20,
1080             onresults: function(data) { showSearchResults( editor, data ) },
1081             onerror: handleSearchError,
1082         } );
1083
1084         function finishCb( data ) {
1085             if ( data.error ) openRecord( 'new/', editor, finishCb );
1086
1087             Resources.GetAll().done( function() {
1088                 $("#loading").hide();
1089                 $( window ).resize( onResize ).resize();
1090                 editor.focus();
1091             } );
1092         }
1093
1094         if ( "[% auth_forwarded_hash %]" ) {
1095             document.location.hash = "[% auth_forwarded_hash %]";
1096         }
1097
1098         if ( !document.location.hash || !openRecord( document.location.hash.slice(1), editor, finishCb ) ) {
1099             openRecord( 'new/', editor, finishCb );
1100         }
1101     } );
1102 } )();
1103
1104 </script>