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