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