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