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