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