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