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