Bug 17268: (follow-up) Fix translatability
[koha.git] / koha-tmpl / intranet-tmpl / prog / en / includes / cateditor-ui.inc
blob08ad36a44e087deac587b55fc89200b99e50316d
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>
36 [% IF marcflavour == 'MARC21' %]
37 [% PROCESS 'cateditor-widgets-marc21.inc' %]
38 [% ELSE %]
39 <script>var editorWidgets = {};</script>
40 [% END %]
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     };
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     ];
75     var state = {
76         backend: '',
77         saveBackend: 'catalog',
78         recordID: undefined
79     };
81     var editor;
82     var macroEditor;
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;
91                 var defaultvalue = subfield.defaultvalue || authvals[0].value;
93                 Widget.Register( tag + subfield, {
94                     init: function() {
95                         var $result = $( '<span class="subfield-widget"></span>' );
97                         return $result[0];
98                     },
99                     postCreate: function() {
100                         var value = defaultvalue;
101                         var widget = this;
103                         $.each( authvals, function() {
104                             if ( this.value == widget.text ) {
105                                 value = this.value;
106                             }
107                         } );
109                         this.setText( value );
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 );
118                         $node.change( $.proxy( function() {
119                             this.setText( $node.val() );
120                         }, this ) );
121                     },
122                     makeTemplate: function() {
123                         return defaultvalue;
124                     },
125                 } );
126             } );
127         } );
128     }
130     function bindGlobalKeys() {
131         shortcut.add( 'ctrl+s', function(event) {
132             $( '#save-record' ).click();
134             event.preventDefault();
135         } );
137         shortcut.add( 'alt+ctrl+k', function(event) {
138             $( '#search-by-keywords' ).focus();
140             return false;
141         } );
143         shortcut.add( 'alt+ctrl+a', function(event) {
144             $( '#search-by-author' ).focus();
146             return false;
147         } );
149         shortcut.add( 'alt+ctrl+i', function(event) {
150             $( '#search-by-isbn' ).focus();
152             return false;
153         } );
155         shortcut.add( 'alt+ctrl+t', function(event) {
156             $( '#search-by-title' ).focus();
158             return false;
159         } );
161         shortcut.add( 'ctrl+h', function() {
162             var field = editor.getCurrentField();
164             if ( !field ) return;
166             window.open( getFieldHelpURL( field.tag ) );
167         } );
169         $('#quicksearch .search-box').each( function() {
170             shortcut.add( 'enter', $.proxy( function() {
171                 var terms = [];
173                 $('#quicksearch .search-box').each( function() {
174                     if ( !this.value ) return;
176                     terms.push( [ $(this).data('qualifier'), this.value ] );
177                 } );
179                 if ( !terms.length ) return;
181                 if ( Search.Run( z3950Servers, Search.JoinTerms(terms) ) ) {
182                     $("#search-overlay").show();
183                     showResultsBox();
184                 }
186                 return false;
187             }, this), { target: this, type: 'keypress' } );
188         } );
189     }
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;
222                 return url;
223             }
224         [% END %]
225     }
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 );
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 );
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;
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                 }
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;
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                 }
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' );
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' );
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                 }
312                 callback( backends.search.records[ id ] );
313             },
314             records: {},
315         },
316     };
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';
324         var backend = backends[state.backend];
326         document.location.hash = '#' + parts[0] + '/' + parts[1];
328         $('#title').text( backend.titleForRecord.replace( '{ID}', parts[1] ) );
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     }
337     function saveRecord( recid, editor, callback ) {
338         var parts = recid.split('/');
339         if ( parts.length != 2 ) return false;
341         if ( !backends[ parts[0] ] || !backends[ parts[0] ].save ) return false;
343         editor.removeErrors();
344         var record = editor.getRecord();
346         if ( record.errors ) {
347             state.saving = false;
348             callback( { error: 'syntax', errors: record.errors } );
349             return;
350         }
352         var errors = KohaBackend.ValidateRecord( '', record );
353         if ( errors.length ) {
354             state.saving = false;
355             callback( { error: 'invalid', errors: errors } );
356             return;
357         }
359         backends[ parts[0] ].save( parts[1], record, function(data) {
360             state.saving = false;
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             }
369             if (data.newId) {
370                 setSource(data.newId);
371             } else {
372                 setSource( [ state.backend, state.recordID ] );
373             }
375             if (callback) callback( data );
376         } );
377     }
379     function loadRecord( recid, editor, callback ) {
380         var parts = recid.split('/');
381         if ( parts.length != 2 ) return false;
383         if ( !backends[ parts[0] ] || !backends[ parts[0] ].get ) return false;
385         backends[ parts[0] ].get( parts[1], function( record ) {
386             if ( !record.error ) {
387                 editor.displayRecord( record );
388                 editor.focus();
389             }
391             if (callback) callback(record);
392         } );
394         return true;
395     }
397     function openRecord( recid, editor, callback ) {
398         return loadRecord( recid, editor, function ( record ) {
399             setSource( recid.split('/') );
401             if (callback) callback( record );
402         } );
403     }
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     }
414     function startAdvancedSearch() {
415         var terms = [];
417         $('#advanced-search-ui .search-box').each( function() {
418             if ( !this.value ) return;
420             terms.push( [ $(this).data('qualifier'), this.value ] );
421         } );
423         if ( !terms.length ) return;
425         if ( Search.Run( z3950Servers, Search.JoinTerms(terms) ) ) {
426             $('#advanced-search-ui').modal('hide');
427             $("#search-overlay").show();
428             showResultsBox();
429         }
430     }
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     }
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' );
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     }
453     function showSearchResults( editor, data ) {
454         backends.search.records = {};
456         $('#searchresults thead tr').empty();
457         $('#searchresults tbody').empty();
458         $('#search-serversinfo').empty();
460         $.each( z3950Servers, function( server_id, server ) {
461             var num_fetched = data.num_fetched[server_id];
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             }
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         } );
474         var seenColumns = {};
476         $.each( data.hits, function( undef, hit ) {
477             $.each( hit.metadata, function(key) {
478                 seenColumns[key] = true;
479             } );
480         } );
482         $('#searchresults thead tr').append('<th>' + _("Source") + '</th>');
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         } );
490         showSearchSorting( data.sort_key, data.sort_direction );
492         $('#searchresults thead tr').append('<th>' + _("Tools") + '</th>');
494         var bibnumMap = KohaBackend.GetSubfieldForKohaField('biblio.biblionumber');
495         $.each( data.hits, function( undef, hit ) {
496             backends.search.records[ hit.server + ':' + hit.index ] = hit.record;
498             switch ( hit.server ) {
499                 case 'koha:biblioserver':
500                     var bibnumField = hit.record.field( bibnumMap[0] );
502                     if ( bibnumField && bibnumField.hasSubfield( bibnumMap[1] ) ) {
503                         hit.id = 'catalog/' + bibnumField.subfield( bibnumMap[1] );
504                         break;
505                     }
507                     // Otherwise, fallthrough
509                 default:
510                     hit.id = 'search/' + hit.server + ':' + hit.index;
511             }
513             var result = '<tr>';
514             result += '<td class="sourcecol">' + z3950Servers[ hit.server ].name + '</td>';
516             $.each( z3950Labels, function( undef, label ) {
517                 if ( !seenColumns[ label[0] ] ) return;
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             } );
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>';
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' );
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                 }
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                 }
551                 return false;
552             } );
553             $tr.find( '.open-link' ).click( function() {
554                 $( '#search-results-ui' ).modal('hide');
555                 openRecord( hit.id, editor );
557                 return false;
558             } );
559             $tr.find( '.substitute-link' ).click( function() {
560                 $( '#search-results-ui' ).modal('hide');
561                 loadRecord( hit.id, editor );
563                 return false;
564             } );
565             $('#searchresults tbody').append( $tr );
566         } );
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;
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         }
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         }
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         }
588         $( '#search-top-pages, #search-bottom-pages' ).find( 'nav' ).html( pages.length > 1 ? ( '<ul class="pagination pagination-sm">' + pages.join( '' ) + '</ul>' ) : '' );
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 ) + '%' } );
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     }
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     }
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     }
620     function handleSearchInitError(error) {
621         $('#quicksearch-overlay').fadeIn().find('p').text(error);
622     }
624     // Preference functions
625     function showPreference( pref ) {
626         var value = Preferences.user[pref];
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];
647                     if ( saved_val != null ) server.checked = saved_val;
648                 } );
649                 break;
650         }
651     }
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         }
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     }
689     function displayPreferences( editor ) {
690         $.each( Preferences.user, function( pref, value ) {
691             showPreference( pref );
692             bindPreference( editor, pref );
693         } );
694     }
696     //> Macro functions
697     var canCreatePublic = "[% CAN_user_editcatalogue_create_shared_macros | html %]";
698     var canDeletePublic = "[% CAN_user_editcatalogue_delete_shared_macros | html %]";
700     function deleteMacro( id ){
701         $( '#macro-list' ).empty();
702         var shared = macroEditor.activeMacroShared;
703         var id = macroEditor.activeMacroId;
704         macroEditor.activeMacroId = null;
705         api_url = "/api/v1/advancededitormacros/";
706         if( shared ) { api_url += "shared/" }
707         let options = {
708             url: api_url + id,
709             method: "DELETE",
710             contentType: "application/json",
711         };
712         $.ajax(options)
713             .then(function(result) {
714                 humanMsg.displayAlert( _("Macro successfully deleted") );
715                 showSavedMacros();
716             })
717             .fail(function(err) {
718                 var err_message;
719                 if( err.status == "404" ){
720                     err_message = "Macro not found";
721                 } else if ( err.status == "403" ){
722                     err_message = _("You do not have permission to delete this macro");
723                 } else {
724                     err_message = _("There was a problem, please check the logs");
725                 }
726                 humanMsg.displayAlert( _("Failed to delete macro: " + err_message), { className: 'humanError' } );
727             });
728     }
731     function loadMacro( name, id, shared ) {
732         $( '#macro-list li' ).removeClass( 'active' );
733         $(".macro_shared").prop("checked",false).hide();
734         $("#delete-macro").prop("disabled",true);
735         macroEditor.setOption( 'readOnly', false );
736         macroEditor.activeMacro = name;
737         macroEditor.activeMacroId = id;
739         if ( !name ) {
740             macroEditor.setValue( '' );
741             return;
742         }
743         $( '#macro-list li[data-name="' + name + '"][data-id="' + id + '"]' ).addClass( 'active' );
744         api_url = "/api/v1/advancededitormacros/";
745         if( shared ) { api_url += "shared/" }
746         let options = {
747             url: api_url + id,
748             method: "GET",
749             contentType: "application/json",
750         };
751         $.ajax(options)
752             .then(function(result) {
753                 macroEditor.setValue( result.macro_text );
754                 $(".macro_shared").show();
755                 if( result.shared ){
756                     $(".macro_shared").prop("checked",true);
757                     if( canCreatePublic ){
758                         macroEditor.setOption( 'readOnly', false );
759                     } else {
760                         macroEditor.setOption( 'readOnly', true );
761                     }
762                     if( canDeletePublic ){
763                         $("#delete-macro").prop("disabled",false);
764                     }
765                 } else {
766                     macroEditor.setOption( 'readOnly', false );
767                     $("#delete-macro").prop("disabled",false);
768                 }
769                 macroEditor.activeMacroShared = result.shared;
770             })
771             .fail(function(err) {
772                 var err_message;
773                 if( err.status == "404" ){
774                     err_message = "Macro not found";
775                 } else if ( err.status == "403" ){
776                     err_message = _("You do not have permission to access this macro");
777                 } else {
778                     err_message = _("There was a problem, please check the logs");
779                 }
780                 humanMsg.displayAlert( _("Failed to load macros: ") + err_message, { className: 'humanError' } );
781             });
783     }
785     function convertOldMacros(){
786         $("#convert-macros").remove();
787         var macro_list = $.map( Preferences.user.macros, function( macro, name ) {
788             return $.extend( { name: name }, macro );
789         } );
790         macro_list.sort( function( a, b ) {
791             return a.name.localeCompare(b.name);
792         } );
793         $.each( macro_list, function( index, macro ) {
794             let options = {
795                 url: "/api/v1/advancededitormacros/",
796                 method: "POST",
797                 contentType: "application/json",
798                 data: JSON.stringify({
799                     name: macro.name,
800                     patron_id: [% logged_in_user.borrowernumber | html %],
801                     macro_text: macro.contents,
802                     shared: false
803                 })
804             };
805             $.ajax(options)
806                 .then(function(undef, result) {
807                     delete Preferences.user.macros[macro.name];
808                     Preferences.Save( [% logged_in_user.borrowernumber | html %] );
809                     if( index == macro_list.length -1 ){
810                         showSavedMacros();
811                     }
813                 })
814                 .fail(function(err) {
815                     var err_message;
816                     if( err.status == "403" ){
817                         err_message = _("You do not have permission to create this macro");
818                     } else {
819                         err_message = _("There was a problem, please check the logs");
820                     }
821                     humanMsg.displayAlert( _("Failed to create macro: ") + err_message, { className: 'humanError' } );
822                 });
823         } );
824     }
826     function showSavedMacros( macros ) {
827         var scrollTop = $('#macro-list').scrollTop();
828         $( '#macro-list' ).empty();
829         $("#convert-macros").remove();
830         if( Object.keys(Preferences.user.macros).length ){
831             $convert = $( '<button class="btn btn-default" id="convert-macros" title="Convert browser storage macros"><i class="fa fa-adjust"></i> Convert old macros</button>' );
832             $convert.click( function(){
833                 if( !confirm( _("This will retrieve macros stored in the brower, save them in the database, and delete them from the browser. Proceed?") ) ){
834                     return;
835                 }
836                 convertOldMacros();
837             });
838             $("#macro-toolbar").prepend($convert);
839         }
840         let options = {
841             url: "/api/v1/advancededitormacros/",
842             method: "GET",
843             contentType: "application/json",
844         };
845         $.ajax(options)
846             .then(function(result) {
847                     $.each(result,function( undef, macro ){
848                         var $li = $( '<li data-name="' + macro.name + '" data-id="' + macro.macro_id + '"><a href="#">'+ macro.macro_id + ' - ' + macro.name + '</a><ol class="macro-info"></ol></li>' );
849                         if ( macro.macro_id == macroEditor.activeMacroId ) $li.addClass( 'active' );
850                         $li.click( function() {
851                             loadMacro(macro.name, macro.macro_id, macro.shared);
852                             return false;
853                         } );
854                         $('#macro-list').append($li);
855                     });
856             })
857             .fail(function(err) {
858                 var err_message = _("There was a problem, please check the logs");
859                 humanMsg.displayAlert( _("Failed to load macros: ") + err_message, { className: 'humanError' } );
860             });
861         var $new_li = $( '<li class="new-macro"><a href="#">' + _("New macro...") + '</a></li>' );
862         $new_li.click( function() {
863             // TODO: make this a bit less retro
864             var name = prompt(_("Please enter the name for the new macro:"));
865             if (!name) return;
867 //            if ( !Preferences.user.macros[name] ) storeMacro( name, { format: "rancor", contents: "" } );
868             let options = {
869                 url: "/api/v1/advancededitormacros/",
870                 method: "POST",
871                 contentType: "application/json",
872                 data: JSON.stringify({
873                     name: name,
874                     patron_id: [% logged_in_user.borrowernumber | html %],
875                     macro_text: "",
876                     shared: false
877                 })
878             };
879             $.ajax(options)
880                 .then(function(undef, result) {
881                     showSavedMacros();
882                     loadMacro( result.name, result.macro_id );
883                 })
884                 .fail(function(err) {
885                     var err_message;
886                     if( err.status == "403" ){
887                         err_message = _("You do not have permission to access this macro");
888                     } else {
889                         err_message = _("There was a problem, please check the logs");
890                     }
891                     humanMsg.displayAlert( _("Failed to create macro: ") + err_message, { className: 'humanError' } );
892                 });
893         } );
894         $('#macro-list').append($new_li);
895         $('#macro-list').scrollTop(scrollTop);
896     }
898     function saveMacro(shared) {
899         var name = macroEditor.activeMacro;
900         var macro_id = macroEditor.activeMacroId;
901         var was_shared = macroEditor.activeMacroShared;
903         if ( !name || macroEditor.savedGeneration == macroEditor.changeGeneration() && was_shared == shared ) return;
905         macroEditor.savedGeneration = macroEditor.changeGeneration();
906         api_url = "/api/v1/advancededitormacros/";
907         if( shared || was_shared ) { api_url += "shared/" }
909         let options = {
910             url: api_url + macro_id,
911             method: "PUT",
912             contentType: "application/json",
913             data: JSON.stringify({
914                 name: name,
915                 patron_id: [% logged_in_user.borrowernumber | html %],
916                 macro_text: macroEditor.getValue(),
917                 shared: shared
918             })
919         };
920         $.ajax(options)
921             .then(function(result) {
922                 $('#macro-save-message').text(_("Saved"));
923                 macroEditor.activeMacroShared = shared;
924                 showSavedMacros();
925             })
926             .fail(function(err) {
927                 var err_message;
928                 if( err.status == "404" ){
929                     err_message = _("Macro not found");
930                 } else if ( err.status ="403" ){
931                     err_message = _("You do not have permission to access this macro");
932                 } else {
933                     err_message = _("There was a problem, please check the logs");
934                 }
935                 humanMsg.displayAlert( _("Failed to save macro: ") + err_message, { className: 'humanError' } );
936             });
937     }
939     $(".macro_shared").change(function(){
940         if(this.checked){
941             saveMacro(true);
942         } else {
943             saveMacro(false);
944         }
945     });
947     // END Macro functions
949     $(document).ready( function() {
950         // Editor setup
951         editor = new MARCEditor( {
952             onCursorActivity: function() {
953                 $('#status-tag-info').empty();
954                 $('#status-subfield-info').empty();
956                 var field = editor.getCurrentField();
957                 var cur = editor.getCursor();
959                 if ( !field ) return;
961                 var taginfo = KohaBackend.GetTagInfo( '', field.tag );
962                 $('#status-tag-info').html( '<strong>' + field.tag + ':</strong> ' );
964                 if ( taginfo ) {
965                     $('#status-tag-info').append( '<a href="' + getFieldHelpURL( field.tag ) + '" target="_blank" class="show-field-help" title="' + _("Show help for this tag") + '">[?]</a> '  + taginfo.lib );
967                     var subfield = field.getSubfieldAt( cur.ch );
968                     if ( !subfield ) return;
970                     var subfieldinfo = taginfo.subfields[ subfield.code ];
971                     $('#status-subfield-info').html( '<strong>‡' + subfield.code + ':</strong> ' );
973                     if ( subfieldinfo ) {
974                         $('#status-subfield-info').append( subfieldinfo.lib );
975                     } else {
976                         $('#status-subfield-info').append( '<em>' + _("Unknown subfield") + '</em>' );
977                     }
978                 } else {
979                     $('#status-tag-info').append( '<em>' + _("Unknown tag") + '</em>' );
980                 }
981             },
982             position: function (elt) { $(elt).insertAfter('#toolbar') },
983         } );
985         // Automatically detect resizes and change the height of the editor and position of modals.
986         var resizeTimer = null;
987         function onResize() {
988             if ( resizeTimer == null ) resizeTimer = setTimeout( function() {
989                 resizeTimer = null;
991                 var pos = $('#editor .CodeMirror').offset();
992                 $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 - $('#changelanguage').height() ); // 24 is hardcoded value but works well
994                 $('.modal-body').each( function() {
995                     $(this).height( $(window).height() * .8 - $(this).prevAll('.modal-header').height() );
996                 } );
997             }, 100);
998         }
1000         $( '#macro-ui' ).on( 'shown.bs.modal', function() {
1001             if ( macroEditor ) return;
1003             macroEditor = CodeMirror(
1004                 $('#macro-editor')[0],
1005                 {
1006                     extraKeys: {
1007                         'Ctrl-D': function( cm ) {
1008                             var cur = cm.getCursor();
1010                             cm.replaceRange( "‡", cur, null );
1011                         },
1012                     },
1013                     mode: 'null',
1014                     lineNumbers: true,
1015                     readOnly: true,
1016                 }
1017             );
1018             var saveTimeout;
1019             macroEditor.on( 'change', function( cm, change ) {
1020                 $('#macro-save-message').empty();
1021                 if ( change.origin == 'setValue' ) return;
1023                 if ( saveTimeout ) clearTimeout( saveTimeout );
1024                 saveTimeout = setTimeout( function() {
1025                     saveMacro(macroEditor.activeMacroShared);
1027                     saveTimeout = null;
1028                 }, 500 );
1029             } );
1031             showSavedMacros();
1032         } );
1034         var saveableBackends = [];
1035         $.each( backends, function( id, backend ) {
1036             if ( backend.save ) saveableBackends.push( [ backend.saveLabel, id ] );
1037         } );
1038         saveableBackends.sort();
1039         $.each( saveableBackends, function( undef, backend ) {
1040             $( '#save-dropdown' ).append( '<li><a href="#" data-backend="' + backend[1] + '">' + backend[0] + '</a></li>' );
1041         } );
1043         // Click bindings
1044         $( '#save-record, #save-dropdown a' ).click( function() {
1045              $( '#save-record' ).find('i').attr( 'class', 'fa fa-spinner fa-spin' ).siblings( 'span' ).text( _("Saving...") );
1047             function finishCb(result) {
1048                 if ( result.error == 'syntax' ) {
1049                     humanMsg.displayAlert( _("Incorrect syntax, cannot save"), { className: 'humanError' } );
1050                 } else if ( result.error == 'invalid' ) {
1051                     humanMsg.displayAlert( _("Record structure invalid, cannot save"), { className: 'humanError' } );
1052                 } else if ( result.error ) {
1053                     humanMsg.displayAlert( _("Something went wrong, cannot save"), { className: 'humanError' } );
1054                 } else if ( !result.error ) {
1055                     humanMsg.displayAlert( _("Record saved "), { className: 'humanSuccess' } );
1056                 }
1058                 $.each( result.errors || [], function( undef, error ) {
1059                     switch ( error.type ) {
1060                         case 'noTag':
1061                             editor.addError( error.line, _("Invalid tag number") );
1062                             break;
1063                         case 'noIndicators':
1064                             editor.addError( error.line, _("Invalid indicators") );
1065                             break;
1066                         case 'noSubfields':
1067                             editor.addError( error.line, _("Tag has no subfields") );
1068                             break;
1069                         case 'missingTag':
1070                             editor.addError( null, _("Missing mandatory tag: ") + error.tag );
1071                             break;
1072                         case 'missingSubfield':
1073                             if ( error.subfield == '@' ) {
1074                                 editor.addError( error.line, _("Missing control field contents") );
1075                             } else {
1076                                 editor.addError( error.line, _("Missing mandatory subfield: â€¡") + error.subfield );
1077                             }
1078                             break;
1079                         case 'unrepeatableTag':
1080                             editor.addError( error.line, _("Tag ") + error.tag + _(" cannot be repeated") );
1081                             break;
1082                         case 'unrepeatableSubfield':
1083                             editor.addError( error.line, _("Subfield â€¡") + error.subfield + _(" cannot be repeated") );
1084                             break;
1085                         case 'itemTagUnsupported':
1086                             editor.addError( error.line, _("Item tags cannot currently be saved") );
1087                             break;
1088                     }
1089                 } );
1091                 $( '#save-record' ).find('i').attr( 'class', 'fa fa-hdd-o' );
1093                 if ( result.error ) {
1094                     // Reset backend info
1095                     setSource( [ state.backend, state.recordID ] );
1096                 }
1097             }
1099             var backend = $( this ).data( 'backend' ) || ( state.saveBackend );
1100             if ( state.backend == backend ) {
1101                 saveRecord( backend + '/' + state.recordID, editor, finishCb );
1102             } else {
1103                 saveRecord( backend + '/', editor, finishCb );
1104             }
1106             return false;
1107         } );
1109         $('#import-records').click( function() {
1110             $('#import-records-input')
1111                 .off('change')
1112                 .change( function() {
1113                     if ( !this.files || !this.files.length ) return;
1115                     var file = this.files[0];
1116                     var reader = new FileReader();
1118                     reader.onload = function() {
1119                         var record = new MARC.Record();
1121                         if ( /\.(mrc|marc|iso|iso2709|marcstd)$/.test( file.name ) ) {
1122                             record.loadISO2709( reader.result );
1123                         } else if ( /\.(xml|marcxml)$/.test( file.name ) ) {
1124                             record.loadMARCXML( reader.result );
1125                         } else {
1126                             humanMsg.displayAlert( _("Unknown record type, cannot import"), { className: 'humanError' } );
1127                             return;
1128                         }
1130                         if (record.marc8_corrupted) humanMsg.displayMsg( '<h3>' + _("Possible record corruption") + '</h3><p>' + _("Record not marked as UTF-8, may be corrupted") + '</p>', { className: 'humanError' } );
1132                         editor.displayRecord( record );
1133                     };
1135                     reader.readAsText( file );
1136                 } )
1137                 .click();
1139             return false;
1140         } );
1142         $('#open-macros').click( function() {
1143             $('#macro-ui').modal('show');
1145             return false;
1146         } );
1148         $('#run-macro').click( function() {
1149             var result = Macros.Run( editor, 'rancor', macroEditor.getValue() );
1151             if ( !result.errors.length ) {
1152                 $('#macro-ui').modal('hide');
1153                 editor.focus(); //Return cursor to editor after macro run
1154                 return false;
1155             }
1157             var errors = [];
1158             $.each( result.errors, function() {
1159                 var error = '<b>' + _("Line ") + (this.line + 1) + ':</b> ';
1161                 switch ( this.error ) {
1162                     case 'failed': error += _("failed to run"); break;
1163                     case 'unrecognized': error += _("unrecognized command"); break;
1164                 }
1166                 errors.push(error);
1167             } );
1169             humanMsg.displayMsg( '<h3>' + _("Failed to run macro:") + '</h3><ul><li>' + errors.join('</li><li>') + '</li></ul>', { className: 'humanError' } );
1171             return false;
1172         } );
1174         $('#delete-macro').click( function() {
1175             if ( !macroEditor.activeMacro || !confirm( _("Are you sure you want to delete this macro?") ) ) return;
1176             deleteMacro();
1177             loadMacro( undefined );
1179             return false;
1180         } );
1182         $( '#switch-editor' ).click( function() {
1183             if ( !confirm( _("Any changes will not be saved. Continue?") ) ) return;
1185             $.cookie( 'catalogue_editor_[% logged_in_user.borrowernumber | html %]', 'basic', { expires: 365, path: '/' } );
1187             if ( state.backend == 'catalog' ) {
1188                 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=' + state.recordID;
1189             } else if ( state.backend == 'new' ) {
1190                 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl';
1191             } else {
1192                 humanMsg.displayAlert( _("Cannot open this record in the basic editor"), { className: 'humanError' } );
1193             }
1194         } );
1196         $( '#show-advanced-search' ).click( function() {
1197             showAdvancedSearch();
1199             return false;
1200         } );
1202         $('#advanced-search').submit( function() {
1203             startAdvancedSearch();
1205             return false;
1206         } );
1208         $( document ).on( 'click', 'a.search-nav', function() {
1209             if ( Search.Fetch( { offset: $( this ).data( 'offset' ) } ) ) {
1210                 $("#search-overlay").show();
1211             }
1213             return false;
1214         });
1216         $( document ).on( 'click', 'th[data-sort-label]', function() {
1217             var direction;
1219             if ( $( this ).hasClass( 'sorting_asc' ) ) {
1220                 direction = 'desc';
1221             } else {
1222                 direction = 'asc';
1223             }
1225             if ( Search.Fetch( { sort_key: $( this ).data( 'sort-label' ), sort_direction: direction } ) ) {
1226                 showSearchSorting( $( this ).data( 'sort-label' ), direction );
1228                 $("#search-overlay").show();
1229             }
1231             return false;
1232         });
1234         $( document ).on( 'change', 'input.search-toggle-server', function() {
1235             var server = z3950Servers[ $( this ).closest('li').data('server-id') ];
1236             server.checked = this.checked;
1238             if ( $('#search-results-ui').is( ':visible' ) && Search.Fetch() ) {
1239                 $("#search-overlay").show();
1240             }
1241         } );
1243         // Key bindings
1244         bindGlobalKeys();
1246         // Setup UI
1247         $("#advanced-search-ui, #search-results-ui, #macro-ui").each( function() {
1248             $(this).modal({ show: false });
1249         } );
1251         var $quicksearch = $('#quicksearch fieldset');
1252         $('<div id="quicksearch-overlay"><h3>' + _("Search unavailable") + '</h3> <p></p></div>').css({
1253             position: 'absolute',
1254             top: $quicksearch.offset().top,
1255             left: $quicksearch.offset().left,
1256             height: $quicksearch.outerHeight(),
1257             width: $quicksearch.outerWidth(),
1258         }).appendTo(document.body).hide();
1260         var prevAlerts = [];
1261         humanMsg.logMsg = function(msg, options) {
1262             $('#show-alerts').popover('hide');
1263             prevAlerts.unshift('<li>' + msg + '</li>');
1264             prevAlerts.splice(5, 999); // Truncate old messages
1265         };
1267         $('#show-alerts').popover({
1268             html: true,
1269             placement: 'bottom',
1270             content: function() {
1271                 return '<div id="alerts-container"><ul>' + prevAlerts.join('') + '</ul></div>';
1272             },
1273         });
1275         $('#show-shortcuts').popover({
1276             html: true,
1277             placement: 'bottom',
1278             content: function() {
1279                 return '<div id="shortcuts-container">' + $('#shortcuts-contents').html() + '</div>';
1280             },
1281         });
1283         $('#new-record' ).click( function() {
1284             if ( editor.modified && !confirm( _("Are you sure you want to erase your changes?") ) ) return;
1286             openRecord( 'new/', editor );
1287             return false;
1288         } );
1290         window.onbeforeunload = function() {
1291             if(editor.modified )
1292                 { return 1; }
1293             else
1294                 { return undefined; }
1295         };
1297         $('a.change-framework').click( function() {
1298             $("#loading").show();
1299             editor.setFrameworkCode(
1300                 $(this).data( 'frameworkcode' ),
1301                 true,
1302                 function ( error ) {
1303                     if ( typeof error !== 'undefined' ) {
1304                         humanMsg.displayAlert( _("Failed to change framework"), { className: 'humanError' } );
1305                     }
1306                     $('#loading').hide();
1307                 }
1308             );
1309         } );
1311         // Start editor
1312         Preferences.Load( [% logged_in_user.borrowernumber || 0 | html %] );
1313         displayPreferences(editor);
1314         makeAuthorisedValueWidgets( '' );
1315         Search.Init( {
1316             page_size: 20,
1317             onresults: function(data) { showSearchResults( editor, data ) },
1318             onerror: handleSearchError,
1319         } );
1321         function finishCb( data ) {
1322             if ( data.error ) {
1323                 humanMsg.displayAlert( data.error );
1324                 openRecord( 'new/', editor, finishCb );
1325             }
1327             Resources.GetAll().done( function() {
1328                 $("#loading").hide();
1329                 $( window ).resize( onResize ).resize();
1330                 editor.focus();
1331             } );
1332         }
1334         if ( "[% auth_forwarded_hash | html %]" ) {
1335             document.location.hash = "[% auth_forwarded_hash | html %]";
1336         }
1338         if ( !document.location.hash || !openRecord( document.location.hash.slice(1), editor, finishCb ) ) {
1339             openRecord( 'new/', editor, finishCb );
1340         }
1341     } );
1342 } )();
1344 </script>
1345 <!-- / cateditor-ui.inc -->