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