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