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