Bug 25002: JS Includes should be wrapped with template comments
[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     function loadMacro( name ) {
698         $( '#macro-list li' ).removeClass( 'active' );
699         macroEditor.activeMacro = name;
700
701         if ( !name ) {
702             macroEditor.setValue( '' );
703             return;
704         }
705
706         $( '#macro-list li[data-name="' + name + '"]' ).addClass( 'active' );
707         var macro = Preferences.user.macros[name];
708         macroEditor.setValue( macro.contents );
709         macroEditor.setOption( 'readOnly', false );
710         if ( macro.history ) macroEditor.setHistory( macro.history );
711     }
712
713     function storeMacro( name, macro ) {
714         if ( macro ) {
715             Preferences.user.macros[name] = macro;
716         } else {
717             delete Preferences.user.macros[name];
718         }
719
720         Preferences.Save( [% logged_in_user.borrowernumber | html %] );
721     }
722
723     function showSavedMacros( macros ) {
724         var scrollTop = $('#macro-list').scrollTop();
725         $( '#macro-list' ).empty();
726         var macro_list = $.map( Preferences.user.macros, function( macro, name ) {
727             return $.extend( { name: name }, macro );
728         } );
729         macro_list.sort( function( a, b ) {
730             return a.name.localeCompare(b.name);
731         } );
732         $.each( macro_list, function( undef, macro ) {
733             var $li = $( '<li data-name="' + macro.name + '"><a href="#">' + macro.name + '</a><ol class="macro-info"></ol></li>' );
734             $li.click( function() {
735                 loadMacro(macro.name);
736                 return false;
737             } );
738             if ( macro.name == macroEditor.activeMacro ) $li.addClass( 'active' );
739             var modified = macro.modified && new Date(macro.modified);
740             $li.find( '.macro-info' ).append(
741                 '<li><span class="label">' + _("Last changed:") + '</span>' +
742                 ( modified ? ( modified.toLocaleDateString() + ', ' + modified.toLocaleTimeString() ) : _("never") ) + '</li>'
743             );
744             $('#macro-list').append($li);
745         } );
746         var $new_li = $( '<li class="new-macro"><a href="#">' + _("New macro...") + '</a></li>' );
747         $new_li.click( function() {
748             // TODO: make this a bit less retro
749             var name = prompt(_("Please enter the name for the new macro:"));
750             if (!name) return;
751
752             if ( !Preferences.user.macros[name] ) storeMacro( name, { format: "rancor", contents: "" } );
753             showSavedMacros();
754             loadMacro( name );
755         } );
756         $('#macro-list').append($new_li);
757         $('#macro-list').scrollTop(scrollTop);
758     }
759
760     function saveMacro() {
761         var name = macroEditor.activeMacro;
762
763         if ( !name || macroEditor.savedGeneration == macroEditor.changeGeneration() ) return;
764
765         macroEditor.savedGeneration = macroEditor.changeGeneration();
766         storeMacro( name, { contents: macroEditor.getValue(), modified: (new Date()).valueOf(), history: macroEditor.getHistory() } );
767         $('#macro-save-message').text(_("Saved"));
768         showSavedMacros();
769     }
770
771     $(document).ready( function() {
772         // Editor setup
773         editor = new MARCEditor( {
774             onCursorActivity: function() {
775                 $('#status-tag-info').empty();
776                 $('#status-subfield-info').empty();
777
778                 var field = editor.getCurrentField();
779                 var cur = editor.getCursor();
780
781                 if ( !field ) return;
782
783                 var taginfo = KohaBackend.GetTagInfo( '', field.tag );
784                 $('#status-tag-info').html( '<strong>' + field.tag + ':</strong> ' );
785
786                 if ( taginfo ) {
787                     $('#status-tag-info').append( '<a href="' + getFieldHelpURL( field.tag ) + '" target="_blank" class="show-field-help" title="' + _("Show help for this tag") + '">[?]</a> '  + taginfo.lib );
788
789                     var subfield = field.getSubfieldAt( cur.ch );
790                     if ( !subfield ) return;
791
792                     var subfieldinfo = taginfo.subfields[ subfield.code ];
793                     $('#status-subfield-info').html( '<strong>‡' + subfield.code + ':</strong> ' );
794
795                     if ( subfieldinfo ) {
796                         $('#status-subfield-info').append( subfieldinfo.lib );
797                     } else {
798                         $('#status-subfield-info').append( '<em>' + _("Unknown subfield") + '</em>' );
799                     }
800                 } else {
801                     $('#status-tag-info').append( '<em>' + _("Unknown tag") + '</em>' );
802                 }
803             },
804             position: function (elt) { $(elt).insertAfter('#toolbar') },
805         } );
806
807         // Automatically detect resizes and change the height of the editor and position of modals.
808         var resizeTimer = null;
809         function onResize() {
810             if ( resizeTimer == null ) resizeTimer = setTimeout( function() {
811                 resizeTimer = null;
812
813                 var pos = $('#editor .CodeMirror').offset();
814                 $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 - $('#changelanguage').height() ); // 24 is hardcoded value but works well
815
816                 $('.modal-body').each( function() {
817                     $(this).height( $(window).height() * .8 - $(this).prevAll('.modal-header').height() );
818                 } );
819             }, 100);
820         }
821
822         $( '#macro-ui' ).on( 'shown.bs.modal', function() {
823             if ( macroEditor ) return;
824
825             macroEditor = CodeMirror(
826                 $('#macro-editor')[0],
827                 {
828                     extraKeys: {
829                         'Ctrl-D': function( cm ) {
830                             var cur = cm.getCursor();
831
832                             cm.replaceRange( "‡", cur, null );
833                         },
834                     },
835                     mode: 'null',
836                     lineNumbers: true,
837                     readOnly: true,
838                 }
839             );
840             var saveTimeout;
841             macroEditor.on( 'change', function( cm, change ) {
842                 $('#macro-save-message').empty();
843                 if ( change.origin == 'setValue' ) return;
844
845                 if ( saveTimeout ) clearTimeout( saveTimeout );
846                 saveTimeout = setTimeout( function() {
847                     saveMacro();
848
849                     saveTimeout = null;
850                 }, 500 );
851             } );
852
853             showSavedMacros();
854         } );
855
856         var saveableBackends = [];
857         $.each( backends, function( id, backend ) {
858             if ( backend.save ) saveableBackends.push( [ backend.saveLabel, id ] );
859         } );
860         saveableBackends.sort();
861         $.each( saveableBackends, function( undef, backend ) {
862             $( '#save-dropdown' ).append( '<li><a href="#" data-backend="' + backend[1] + '">' + backend[0] + '</a></li>' );
863         } );
864
865         // Click bindings
866         $( '#save-record, #save-dropdown a' ).click( function() {
867              $( '#save-record' ).find('i').attr( 'class', 'fa fa-spinner fa-spin' ).siblings( 'span' ).text( _("Saving...") );
868
869             function finishCb(result) {
870                 if ( result.error == 'syntax' ) {
871                     humanMsg.displayAlert( _("Incorrect syntax, cannot save"), { className: 'humanError' } );
872                 } else if ( result.error == 'invalid' ) {
873                     humanMsg.displayAlert( _("Record structure invalid, cannot save"), { className: 'humanError' } );
874                 } else if ( result.error ) {
875                     humanMsg.displayAlert( _("Something went wrong, cannot save"), { className: 'humanError' } );
876                 } else if ( !result.error ) {
877                     humanMsg.displayAlert( _("Record saved "), { className: 'humanSuccess' } );
878                 }
879
880                 $.each( result.errors || [], function( undef, error ) {
881                     switch ( error.type ) {
882                         case 'noTag':
883                             editor.addError( error.line, _("Invalid tag number") );
884                             break;
885                         case 'noIndicators':
886                             editor.addError( error.line, _("Invalid indicators") );
887                             break;
888                         case 'noSubfields':
889                             editor.addError( error.line, _("Tag has no subfields") );
890                             break;
891                         case 'missingTag':
892                             editor.addError( null, _("Missing mandatory tag: ") + error.tag );
893                             break;
894                         case 'missingSubfield':
895                             if ( error.subfield == '@' ) {
896                                 editor.addError( error.line, _("Missing control field contents") );
897                             } else {
898                                 editor.addError( error.line, _("Missing mandatory subfield: â€¡") + error.subfield );
899                             }
900                             break;
901                         case 'unrepeatableTag':
902                             editor.addError( error.line, _("Tag ") + error.tag + _(" cannot be repeated") );
903                             break;
904                         case 'unrepeatableSubfield':
905                             editor.addError( error.line, _("Subfield â€¡") + error.subfield + _(" cannot be repeated") );
906                             break;
907                         case 'itemTagUnsupported':
908                             editor.addError( error.line, _("Item tags cannot currently be saved") );
909                             break;
910                     }
911                 } );
912
913                 $( '#save-record' ).find('i').attr( 'class', 'fa fa-hdd-o' );
914
915                 if ( result.error ) {
916                     // Reset backend info
917                     setSource( [ state.backend, state.recordID ] );
918                 }
919             }
920
921             var backend = $( this ).data( 'backend' ) || ( state.saveBackend );
922             if ( state.backend == backend ) {
923                 saveRecord( backend + '/' + state.recordID, editor, finishCb );
924             } else {
925                 saveRecord( backend + '/', editor, finishCb );
926             }
927
928             return false;
929         } );
930
931         $('#import-records').click( function() {
932             $('#import-records-input')
933                 .off('change')
934                 .change( function() {
935                     if ( !this.files || !this.files.length ) return;
936
937                     var file = this.files[0];
938                     var reader = new FileReader();
939
940                     reader.onload = function() {
941                         var record = new MARC.Record();
942
943                         if ( /\.(mrc|marc|iso|iso2709|marcstd)$/.test( file.name ) ) {
944                             record.loadISO2709( reader.result );
945                         } else if ( /\.(xml|marcxml)$/.test( file.name ) ) {
946                             record.loadMARCXML( reader.result );
947                         } else {
948                             humanMsg.displayAlert( _("Unknown record type, cannot import"), { className: 'humanError' } );
949                             return;
950                         }
951
952                         if (record.marc8_corrupted) humanMsg.displayMsg( '<h3>' + _("Possible record corruption") + '</h3><p>' + _("Record not marked as UTF-8, may be corrupted") + '</p>', { className: 'humanError' } );
953
954                         editor.displayRecord( record );
955                     };
956
957                     reader.readAsText( file );
958                 } )
959                 .click();
960
961             return false;
962         } );
963
964         $('#open-macros').click( function() {
965             $('#macro-ui').modal('show');
966
967             return false;
968         } );
969
970         $('#run-macro').click( function() {
971             var result = Macros.Run( editor, 'rancor', macroEditor.getValue() );
972
973             if ( !result.errors.length ) {
974                 $('#macro-ui').modal('hide');
975                 editor.focus(); //Return cursor to editor after macro run
976                 return false;
977             }
978
979             var errors = [];
980             $.each( result.errors, function() {
981                 var error = '<b>' + _("Line ") + (this.line + 1) + ':</b> ';
982
983                 switch ( this.error ) {
984                     case 'failed': error += _("failed to run"); break;
985                     case 'unrecognized': error += _("unrecognized command"); break;
986                 }
987
988                 errors.push(error);
989             } );
990
991             humanMsg.displayMsg( '<h3>' + _("Failed to run macro:") + '</h3><ul><li>' + errors.join('</li><li>') + '</li></ul>', { className: 'humanError' } );
992
993             return false;
994         } );
995
996         $('#delete-macro').click( function() {
997             if ( !macroEditor.activeMacro || !confirm( _("Are you sure you want to delete this macro?") ) ) return;
998
999             storeMacro( macroEditor.activeMacro, undefined );
1000             showSavedMacros();
1001             loadMacro( undefined );
1002
1003             return false;
1004         } );
1005
1006         $( '#switch-editor' ).click( function() {
1007             if ( !confirm( _("Any changes will not be saved. Continue?") ) ) return;
1008
1009             $.cookie( 'catalogue_editor_[% logged_in_user.borrowernumber | html %]', 'basic', { expires: 365, path: '/' } );
1010
1011             if ( state.backend == 'catalog' ) {
1012                 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=' + state.recordID;
1013             } else if ( state.backend == 'new' ) {
1014                 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl';
1015             } else {
1016                 humanMsg.displayAlert( _("Cannot open this record in the basic editor"), { className: 'humanError' } );
1017             }
1018         } );
1019
1020         $( '#show-advanced-search' ).click( function() {
1021             showAdvancedSearch();
1022
1023             return false;
1024         } );
1025
1026         $('#advanced-search').submit( function() {
1027             startAdvancedSearch();
1028
1029             return false;
1030         } );
1031
1032         $( document ).on( 'click', 'a.search-nav', function() {
1033             if ( Search.Fetch( { offset: $( this ).data( 'offset' ) } ) ) {
1034                 $("#search-overlay").show();
1035             }
1036
1037             return false;
1038         });
1039
1040         $( document ).on( 'click', 'th[data-sort-label]', function() {
1041             var direction;
1042
1043             if ( $( this ).hasClass( 'sorting_asc' ) ) {
1044                 direction = 'desc';
1045             } else {
1046                 direction = 'asc';
1047             }
1048
1049             if ( Search.Fetch( { sort_key: $( this ).data( 'sort-label' ), sort_direction: direction } ) ) {
1050                 showSearchSorting( $( this ).data( 'sort-label' ), direction );
1051
1052                 $("#search-overlay").show();
1053             }
1054
1055             return false;
1056         });
1057
1058         $( document ).on( 'change', 'input.search-toggle-server', function() {
1059             var server = z3950Servers[ $( this ).closest('li').data('server-id') ];
1060             server.checked = this.checked;
1061
1062             if ( $('#search-results-ui').is( ':visible' ) && Search.Fetch() ) {
1063                 $("#search-overlay").show();
1064             }
1065         } );
1066
1067         // Key bindings
1068         bindGlobalKeys();
1069
1070         // Setup UI
1071         $("#advanced-search-ui, #search-results-ui, #macro-ui").each( function() {
1072             $(this).modal({ show: false });
1073         } );
1074
1075         var $quicksearch = $('#quicksearch fieldset');
1076         $('<div id="quicksearch-overlay"><h3>' + _("Search unavailable") + '</h3> <p></p></div>').css({
1077             position: 'absolute',
1078             top: $quicksearch.offset().top,
1079             left: $quicksearch.offset().left,
1080             height: $quicksearch.outerHeight(),
1081             width: $quicksearch.outerWidth(),
1082         }).appendTo(document.body).hide();
1083
1084         var prevAlerts = [];
1085         humanMsg.logMsg = function(msg, options) {
1086             $('#show-alerts').popover('hide');
1087             prevAlerts.unshift('<li>' + msg + '</li>');
1088             prevAlerts.splice(5, 999); // Truncate old messages
1089         };
1090
1091         $('#show-alerts').popover({
1092             html: true,
1093             placement: 'bottom',
1094             content: function() {
1095                 return '<div id="alerts-container"><ul>' + prevAlerts.join('') + '</ul></div>';
1096             },
1097         });
1098
1099         $('#show-shortcuts').popover({
1100             html: true,
1101             placement: 'bottom',
1102             content: function() {
1103                 return '<div id="shortcuts-container">' + $('#shortcuts-contents').html() + '</div>';
1104             },
1105         });
1106
1107         $('#new-record' ).click( function() {
1108             if ( editor.modified && !confirm( _("Are you sure you want to erase your changes?") ) ) return;
1109
1110             openRecord( 'new/', editor );
1111             return false;
1112         } );
1113
1114         window.onbeforeunload = function() {
1115             if(editor.modified )
1116                 { return 1; }
1117             else
1118                 { return undefined; }
1119         };
1120
1121         $('a.change-framework').click( function() {
1122             $("#loading").show();
1123             editor.setFrameworkCode(
1124                 $(this).data( 'frameworkcode' ),
1125                 true,
1126                 function ( error ) {
1127                     if ( typeof error !== 'undefined' ) {
1128                         humanMsg.displayAlert( _("Failed to change framework"), { className: 'humanError' } );
1129                     }
1130                     $('#loading').hide();
1131                 }
1132             );
1133         } );
1134
1135         // Start editor
1136         Preferences.Load( [% logged_in_user.borrowernumber || 0 | html %] );
1137         displayPreferences(editor);
1138         makeAuthorisedValueWidgets( '' );
1139         Search.Init( {
1140             page_size: 20,
1141             onresults: function(data) { showSearchResults( editor, data ) },
1142             onerror: handleSearchError,
1143         } );
1144
1145         function finishCb( data ) {
1146             if ( data.error ) {
1147                 humanMsg.displayAlert( data.error );
1148                 openRecord( 'new/', editor, finishCb );
1149             }
1150
1151             Resources.GetAll().done( function() {
1152                 $("#loading").hide();
1153                 $( window ).resize( onResize ).resize();
1154                 editor.focus();
1155             } );
1156         }
1157
1158         if ( "[% auth_forwarded_hash | html %]" ) {
1159             document.location.hash = "[% auth_forwarded_hash | html %]";
1160         }
1161
1162         if ( !document.location.hash || !openRecord( document.location.hash.slice(1), editor, finishCb ) ) {
1163             openRecord( 'new/', editor, finishCb );
1164         }
1165     } );
1166 } )();
1167
1168 </script>
1169 <!-- / cateditor-ui.inc -->