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