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 -->
13 [% FOREACH shortcut IN shortcuts -%]
14 var [% shortcut.shortcut_name | html %] = "[% shortcut.shortcut_keys | html %]";
17 [%- FOREACH authtag = authtags -%]
18 [% authtag.tagfield | html %]: {
19 subfield: '[% authtag.tagsubfield | html %]',
20 authtypecode: '[% authtag.authtypecode | html %]',
25 baseUrl: '[% interface | html %]/lib/koha/cateditor/',
28 marcflavour: '[% marcflavour | html %]',
29 themelang: '[% themelang | html %]',
36 [% IF marcflavour == 'MARC21' %]
37 [% PROCESS 'cateditor-widgets-marc21.inc' %]
39 <script>var editorWidgets = {};</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 ) {
46 server_id: 'koha:biblioserver',
47 name: _("Local catalog"),
51 [%- FOREACH server = z3950_servers -%]
53 server_id: [% server.id | html %],
54 name: '[% server.servername | html_entity %]',
55 recordtype: '[% server.recordtype | html %]',
56 checked: [% server.checked ? 'true' : 'false' | html %],
61 // The columns that should show up in a search, in order, and keyed by the corresponding <metadata> tag in the XSL and Pazpar2 config
63 [ "local_number", _("Local number") ],
64 [ "title", _("Title") ],
65 [ "subtitle",_("Subtitle") ],
66 [ "series", _("Series title") ],
67 [ "author", _("Author") ],
68 [ "lccn", _("LCCN") ],
69 [ "isbn", _("ISBN") ],
70 [ "issn", _("ISSN") ],
71 [ "medium", _("Medium") ],
72 [ "edition", _("Edition") ],
73 [ "date", _("Published") ],
74 [ "notes", _("Notes") ],
79 saveBackend: 'catalog',
86 function makeAuthorisedValueWidgets( frameworkCode ) {
87 $.each( KohaBackend.GetAllTagsInfo( frameworkCode ), function( tag, tagInfo ) {
88 $.each( tagInfo.subfields, function( subfield, subfieldInfo ) {
89 if ( !subfieldInfo.authorised_value ) return;
90 var authvals = KohaBackend.GetAuthorisedValues( subfieldInfo.authorised_value );
91 if ( !authvals ) return;
93 var defaultvalue = subfield.defaultvalue || authvals[0].value;
95 Widget.Register( tag + subfield, {
97 var $result = $( '<span class="subfield-widget"></span>' );
101 postCreate: function() {
103 var value = widget.text || defaultvalue;
105 $.each( authvals, function() {
106 if ( this.value == widget.text ) {
111 this.setText( value );
113 $( '<select></select>' ).appendTo( this.node );
114 var $node = $( this.node ).find( 'select' );
116 $.each( authvals, function( undef, authval ) {
118 if (authval.value == value){
120 selected = ' selected="selected" ';
122 $node.append( '<option value="' + authval.value + '"' + selected + '>' + authval.lib + '</option>' );
125 $node.append( "<option value='" + value + "' selected='selected'>" + value + " " + _("(Not an authorised value)") + " " + "</option>" );
127 $node.val( this.text );
129 $node.change( $.proxy( function() {
130 this.setText( $node.val() );
133 makeTemplate: function() {
141 function bindGlobalKeys() {
142 shortcut.add( 'ctrl+s', function(event) {
143 $( '#save-record' ).click();
145 event.preventDefault();
148 shortcut.add( 'alt+ctrl+k', function(event) {
149 $( '#search-by-keywords' ).focus();
154 shortcut.add( 'alt+ctrl+a', function(event) {
155 $( '#search-by-author' ).focus();
160 shortcut.add( 'alt+ctrl+i', function(event) {
161 $( '#search-by-isbn' ).focus();
166 shortcut.add( 'alt+ctrl+t', function(event) {
167 $( '#search-by-title' ).focus();
172 shortcut.add( 'ctrl+h', function() {
173 var field = editor.getCurrentField();
175 if ( !field ) return;
177 window.open( getFieldHelpURL( field.tag ) );
180 $('#quicksearch .search-box').each( function() {
181 shortcut.add( 'enter', $.proxy( function() {
184 $('#quicksearch .search-box').each( function() {
185 if ( !this.value ) return;
187 terms.push( [ $(this).data('qualifier'), this.value ] );
190 if ( !terms.length ) return;
192 if ( Search.Run( z3950Servers, Search.JoinTerms(terms) ) ) {
193 $("#search-overlay").show();
198 }, this), { target: this, type: 'keypress' } );
202 function getFieldHelpURL( tag ) {
203 [% IF Koha.Preference('marcfielddocurl') %]
204 var docurl = "[% Koha.Preference('marcfielddocurl').replace('"','"') | html %]";
205 docurl = docurl.replace("{MARC}", "[% marcflavour | html %]");
206 docurl = docurl.replace("{FIELD}", ""+tag);
207 docurl = docurl.replace("{LANG}", "[% lang | html %]");
209 [% ELSIF ( marcflavour == 'MARC21' ) %]
210 if ( tag == '000' ) {
211 return "http://www.loc.gov/marc/bibliographic/bdleader.html";
212 } else if ( tag >= '090' && tag < '100' ) {
213 return "http://www.loc.gov/marc/bibliographic/bd09x.html";
214 } else if ( tag < '900' ) {
215 return "http://www.loc.gov/marc/bibliographic/bd" + tag + ".html";
217 return "http://www.loc.gov/marc/bibliographic/bd9xx.html";
219 [% ELSIF ( marcflavour == 'UNIMARC' ) %]
220 /* http://archive.ifla.org/VI/3/p1996-1/ is an outdated version of UNIMARC, but
221 seems to be the only version available that can be linked to per tag. More recent
222 versions of the UNIMARC standard are available on the IFLA website only as
225 if ( tag == '000' ) {
226 return "http://archive.ifla.org/VI/3/p1996-1/uni.htm";
229 var url = "http://archive.ifla.org/VI/3/p1996-1/uni" + first + ".htm#";
230 if ( first == '0' ) url += "b";
231 if ( first != '9' ) url += tag;
241 titleForRecord: _("Editing new record"),
242 get: function( id, callback ) {
243 record = new MARC.Record();
244 KohaBackend.FillRecord( '', record );
250 titleForRecord: _("Editing new full record"),
251 get: function( id, callback ) {
252 record = new MARC.Record();
253 KohaBackend.FillRecord( '', record, true );
259 titleForRecord: _("Editing duplicate record of #{ID}"),
260 saveLabel: _("Duplicate"),
261 get: function( id, callback ) {
262 if ( !id ) return false;
263 var remove_control_num = [% IF Koha.Preference('autoControlNumber') == 'OFF' %] false [% ELSE %] true [% END %];
265 KohaBackend.GetRecord( id, remove_control_num, callback );
267 save: function( id, record, done ) {
268 function finishCb( data ) {
269 done( { error: data.error, newRecord: data.marcxml && data.marcxml[0], newId: data.biblionumber && [ 'catalog', data.biblionumber ] } );
271 [% IF Koha.Preference('autoControlNumber') != 'OFF' %]
272 record.removeField("001");
275 KohaBackend.CreateRecord( record, finishCb );
279 titleForRecord: _("Editing catalog record #{ID}"),
281 { title: _("view"), href: "/cgi-bin/koha/catalogue/detail.pl?biblionumber={ID}" },
282 { title: _("edit items"), href: "/cgi-bin/koha/cataloguing/additem.pl?biblionumber={ID}" },
284 saveLabel: _("Save to catalog"),
285 get: function( id, callback ) {
286 if ( !id ) return false;
288 KohaBackend.GetRecord( id, false, callback );
290 save: function( id, record, done ) {
291 function finishCb( data ) {
292 done( { error: data.error, newRecord: data.marcxml && data.marcxml[0], newId: data.biblionumber && [ 'catalog', data.biblionumber ] } );
296 KohaBackend.SaveRecord( id, record, finishCb );
298 KohaBackend.CreateRecord( record, finishCb );
303 saveLabel: _("Save as MARC (.mrc) file"),
304 save: function( id, record, done ) {
305 var recname = 'record.mrc';
307 recname = 'bib-'+state.recordID+'.mrc';
310 [% IF (Koha.Preference('DefaultSaveRecordFileID') == 'controlnumber') %]
311 var controlnumfield = record.field('001');
312 if(controlnumfield) {
313 recname = 'record-'+controlnumfield.subfield('@')+'.mrc';
316 saveAs( new Blob( [record.toISO2709()], { 'type': 'application/octet-stream;charset=utf-8' } ), recname );
322 saveLabel: _("Save as MARCXML (.xml) file"),
323 save: function( id, record, done ) {
324 var recname = 'record.xml';
326 recname = 'bib-'+state.recordID+'.xml';
329 [% IF (Koha.Preference('DefaultSaveRecordFileID') == 'controlnumber') %]
330 var controlnumfield = record.field('001');
331 if(controlnumfield) {
332 recname = 'record-'+controlnumfield.subfield('@')+'.xml';
335 saveAs( new Blob( [record.toXML()], { 'type': 'application/octet-stream;charset=utf-8' } ), recname );
341 titleForRecord: _("Editing search result"),
342 get: function( id, callback ) {
343 if ( !id ) return false;
344 if ( !backends.search.records[ id ] ) {
345 callback( { error: _( "Invalid record" ) } );
349 callback( backends.search.records[ id ] );
355 function setSource(parts) {
356 state.backend = parts[0];
357 state.recordID = parts[1];
358 state.canSave = backends[ state.backend ].save != null;
359 state.saveBackend = state.canSave ? state.backend : 'catalog';
361 var backend = backends[state.backend];
363 document.location.hash = '#' + parts[0] + '/' + parts[1];
365 $('#title').text( backend.titleForRecord.replace( '{ID}', parts[1] ) );
367 $.each( backend.links || [], function( i, link ) {
368 $('#title').append(' <a target="_blank" href="' + link.href.replace( '{ID}', parts[1] ) + '">(' + link.title + ')</a>' );
370 $( 'title', document.head ).html( _("Koha › Cataloging › ") + backend.titleForRecord.replace( '{ID}', parts[1] ) );
371 $('#save-record span').text( backends[ state.saveBackend ].saveLabel );
374 function saveRecord( recid, editor, callback ) {
375 var parts = recid.split('/');
376 if ( parts.length != 2 ) return false;
378 if ( !backends[ parts[0] ] || !backends[ parts[0] ].save ) return false;
380 editor.removeErrors();
381 var record = editor.getRecord();
383 if ( record.errors ) {
384 state.saving = false;
385 callback( { error: 'syntax', errors: record.errors } );
389 var errors = KohaBackend.ValidateRecord( '', record );
390 if ( errors.length ) {
391 state.saving = false;
392 callback( { error: 'invalid', errors: errors } );
396 backends[ parts[0] ].save( parts[1], record, function(data) {
397 state.saving = false;
399 if (data.newRecord) {
400 var record = new MARC.Record();
401 record.loadMARCXML(data.newRecord);
402 record.frameworkcode = data.newRecord.frameworkcode;
403 editor.displayRecord( record );
407 setSource(data.newId);
409 setSource( [ state.backend, state.recordID ] );
412 if (callback) callback( data );
416 function loadRecord( recid, editor, callback ) {
417 var parts = recid.split('/');
418 if ( parts.length != 2 ) return false;
420 if ( !backends[ parts[0] ] || !backends[ parts[0] ].get ) return false;
422 backends[ parts[0] ].get( parts[1], function( record ) {
423 if ( !record.error ) {
424 var remove_control_num = [% IF Koha.Preference('autoControlNumber') == 'OFF' %] false [% ELSE %] true [% END %];
425 if( remove_control_num ){ record.removeField("001"); }
426 editor.displayRecord( record );
430 if (callback) callback(record);
436 function openRecord( recid, editor, callback ) {
437 return loadRecord( recid, editor, function ( record ) {
438 setSource( recid.split('/') );
440 if (callback) callback( record );
445 function showAdvancedSearch() {
446 $('#advanced-search-servers').empty();
447 $.each( z3950Servers, function( index, server ) {
448 $('#advanced-search-servers').append( '<li data-server-id="' + index + '"><label><input class="search-toggle-server" type="checkbox"' + ( server.checked ? ' checked="checked">' : '>' ) + server.name + '</label></li>' );
450 $('#advanced-search-ui').modal('show');
453 function startAdvancedSearch() {
456 $('#advanced-search-ui .search-box').each( function() {
457 if ( !this.value ) return;
459 terms.push( [ $(this).data('qualifier'), this.value ] );
462 if ( !terms.length ) return;
464 if ( Search.Run( z3950Servers, Search.JoinTerms(terms) ) ) {
465 $('#advanced-search-ui').modal('hide');
466 $("#search-overlay").show();
471 function showResultsBox(data) {
472 $('#search-top-pages, #search-bottom-pages').find('nav').empty();
473 $('#searchresults thead tr').empty();
474 $('#searchresults tbody').empty();
475 $('#search-serversinfo').empty().append('<li>' + _("Loading...") + '</li>');
476 $('#search-results-ui').modal('show');
479 function showSearchSorting( sort_key, sort_direction ) {
480 var $th = $('#searchresults thead tr th[data-sort-label="' + sort_key + '"]');
481 $th.parent().find( 'th[data-sort-label]' ).attr( 'class', 'sorting' );
483 if ( sort_direction == 'asc' ) {
485 $th.attr( 'class', 'sorting_asc' );
488 $th.attr( 'class', 'sorting_desc' );
492 function showSearchResults( editor, data ) {
493 backends.search.records = {};
495 $('#searchresults thead tr').empty();
496 $('#searchresults tbody').empty();
497 $('#search-serversinfo').empty();
499 $.each( z3950Servers, function( index, server ) {
500 var num_fetched = data.num_fetched[server.server_id];
502 if ( data.errors[server.server_id] ) {
503 num_fetched = data.errors[server.server_id];
504 } else if ( num_fetched == null ) {
506 } else if ( num_fetched < data.num_hits[server.server_id] ) {
510 $('#search-serversinfo').append( '<li data-server-id="' + index + '"><label><input class="search-toggle-server" type="checkbox"' + ( server.checked ? ' checked="checked">' : '>' ) + server.name + ' (' + num_fetched + ')' + '</label></li>' );
513 var seenColumns = {};
515 $.each( data.hits, function( undef, hit ) {
516 $.each( hit.metadata, function(key) {
517 seenColumns[key] = true;
521 $('#searchresults thead tr').append('<th>' + _("Source") + '</th>');
523 $.each( z3950Labels, function( undef, label ) {
524 if ( seenColumns[ label[0] ] ) {
525 $('#searchresults thead tr').append( '<th class="sorting" data-sort-label="' + label[0] + '">' + label[1] + '</th>' );
529 showSearchSorting( data.sort_key, data.sort_direction );
531 $('#searchresults thead tr').append('<th>' + _("Tools") + '</th>');
533 var bibnumMap = KohaBackend.GetSubfieldForKohaField('biblio.biblionumber');
534 $.each( data.hits, function( undef, hit ) {
535 backends.search.records[ hit.server + ':' + hit.index ] = hit.record;
537 switch ( hit.server ) {
538 case 'koha:biblioserver':
539 var bibnumField = hit.record.field( bibnumMap[0] );
541 if ( bibnumField && bibnumField.hasSubfield( bibnumMap[1] ) ) {
542 hit.id = 'catalog/' + bibnumField.subfield( bibnumMap[1] );
546 // Otherwise, fallthrough
549 hit.id = 'search/' + hit.server + ':' + hit.index;
553 var server_name = hit.servername == 'koha:biblioserver' ? _("Local catalog") : hit.servername;
554 result += '<td class="sourcecol">' + server_name + '</td>';
556 $.each( z3950Labels, function( undef, label ) {
557 if ( !seenColumns[ label[0] ] ) return;
559 if ( hit.metadata[ label[0] ] ) {
560 result += '<td class="infocol">' + hit.metadata[ label[0] ] + '</td>';
562 result += '<td class="infocol"> </td>';
566 result += '<td class="toolscol"><ul><li><a href="#" class="marc-link">' + _("View MARC") + '</a></li>';
567 result += '<li><a href="#" class="open-link">' + ( hit.server == 'koha:biblioserver' ? _("Edit") : _("Import") ) + '</a></li>';
568 if ( state.canSave ) result += '<li><a href="#" class="substitute-link" title="' + _("Replace the current record's contents") + '">' + _("Substitute") + '</a></li>';
569 result += '</ul></td></tr>';
571 var $tr = $( result );
572 $tr.find( '.marc-link' ).click( function() {
573 var $info_columns = $tr.find( '.infocol' );
574 var $marc_column = $tr.find( '.marccol' );
576 if ( !$marc_column.length ) {
577 $marc_column = $( '<td class="marccol" colspan="' + $info_columns.length + '"></td>' ).insertAfter( $info_columns.eq(-1) ).hide();
578 CodeMirror.runMode( TextMARC.RecordToText( hit.record ), 'marc', $marc_column[0] );
581 if ( $marc_column.is(':visible') ) {
582 $tr.find('.marc-link').text( _("View MARC") );
583 $info_columns.show();
586 $tr.find('.marc-link').text( _("Hide MARC") );
588 $info_columns.hide();
593 $tr.find( '.open-link' ).click( function() {
594 $( '#search-results-ui' ).modal('hide');
595 openRecord( hit.id, editor );
599 $tr.find( '.substitute-link' ).click( function() {
600 $( '#search-results-ui' ).modal('hide');
601 loadRecord( hit.id, editor );
605 $('#searchresults tbody').append( $tr );
609 var cur_page = data.offset / data.page_size;
610 var max_page = Math.ceil( data.total_fetched / data.page_size ) - 1;
612 if ( cur_page != 0 ) {
613 pages.push( '<li><a class="search-nav" href="#" data-offset="' + (data.offset - data.page_size) + '"><span aria-hidden="true">«</span> ' + _("Previous") + '</a></li>' );
616 for ( var page = Math.max( 0, cur_page - 9 ); page <= Math.min( max_page, cur_page + 9 ); page++ ) {
617 if ( page == cur_page ) {
618 pages.push( ' <li class="active"><a href="#">' + ( page + 1 ) + '</a></li>' );
620 pages.push( ' <li><a class="search-nav" href="#" data-offset="' + ( page * data.page_size ) + '">' + ( page + 1 ) + '</a></li>' );
624 if ( cur_page < max_page ) {
625 pages.push( ' <li><a class="search-nav" href="#" data-offset="' + (data.offset + data.page_size) + '">' + _("Next") + ' <span aria-hidden="true">»</span></a></li>' );
628 $( '#search-top-pages, #search-bottom-pages' ).find( 'nav' ).html( pages.length > 1 ? ( '<ul class="pagination pagination-sm">' + pages.join( '' ) + '</ul>' ) : '' );
630 var $overlay = $('#search-overlay');
631 $overlay.find('span').text(_("Loading"));
632 $overlay.find('.bar').css( { display: 'block', width: 100 * ( 1 - data.activeclients / Search.includedServers.length ) + '%' } );
634 if ( data.activeclients ) {
635 $overlay.find('.bar').css( { display: 'block', width: 100 * ( 1 - data.activeclients / Search.includedServers.length ) + '%' } );
638 $overlay.find('.bar').css( { display: 'block', width: '100%' } );
640 $('#searchresults')[0].focus();
644 function invalidateSearchResults() {
645 var $overlay = $('#search-overlay');
646 $overlay.find('span').text(_("Search expired, please try again"));
647 $overlay.find('.bar').css( { display: 'none' } );
651 function handleSearchError(error) {
652 if (error.code == 1) {
653 invalidateSearchResults();
656 humanMsg.displayMsg( '<h3>' + _("Internal search error") + '</h3><p>' + error.responseText + '</p><p>' + _("Please refresh the page and try again.") + '</p>', { className: 'humanError' } );
660 function handleSearchInitError(error) {
661 $('#quicksearch-overlay').fadeIn().find('p').text(error);
664 // Preference functions
665 function showPreference( pref ) {
666 var value = Preferences.user[pref];
670 $( '#set-field-widgets' ).text( value ? _("Show fields verbatim") : _("Show helpers for fixed and coded fields") );
673 $( '#editor .CodeMirror' ).css( { fontFamily: value } );
677 $( '#editor .CodeMirror' ).css( { fontSize: value } );
681 // Macros loaded on first show of modal
683 case 'selected_search_targets':
684 $.each( z3950Servers, function( index, server ) {
685 var saved_val = Preferences.user.selected_search_targets[server.server_id];
687 if ( saved_val != null ) server.checked = saved_val;
693 function bindPreference( editor, pref ) {
694 function _addHandler( sel, event, handler ) {
695 $( sel ).on( event, function (e) {
697 handler( e, Preferences.user[pref] );
698 Preferences.Save( [% logged_in_user.borrowernumber | html %] );
699 showPreference(pref);
705 _addHandler( '#set-field-widgets', 'click', function( e, oldValue ) {
706 editor.setUseWidgets( Preferences.user.fieldWidgets = !Preferences.user.fieldWidgets );
710 _addHandler( '#prefs-menu .set-font', 'click', function( e, oldValue ) {
711 Preferences.user.font = $( e.target ).css( 'font-family' );
715 _addHandler( '#prefs-menu .set-fontSize', 'click', function( e, oldValue ) {
716 Preferences.user.fontSize = $( e.target ).css( 'font-size' );
719 case 'selected_search_targets':
720 $( document ).on( 'change', 'input.search-toggle-server', function() {
721 var server_id = $( this ).closest('li').data('server-id');
722 Preferences.user.selected_search_targets[server_id] = this.checked;
723 Preferences.Save( [% logged_in_user.borrowernumber | html %] );
729 function displayPreferences( editor ) {
730 $.each( Preferences.user, function( pref, value ) {
731 showPreference( pref );
732 bindPreference( editor, pref );
737 var canCreatePublic = "[% CAN_user_editcatalogue_create_shared_macros | html %]";
738 var canDeletePublic = "[% CAN_user_editcatalogue_delete_shared_macros | html %]";
740 function deleteMacro( id ){
741 $( '#macro-list' ).empty();
742 var shared = macroEditor.activeMacroShared;
743 var id = macroEditor.activeMacroId;
744 macroEditor.activeMacroId = null;
745 api_url = "/api/v1/advanced_editor/macros/";
746 if( shared ) { api_url += "shared/" }
750 contentType: "application/json",
753 .then(function(result) {
754 humanMsg.displayAlert( _("Macro successfully deleted") );
757 .fail(function(err) {
759 if( err.status == "404" ){
760 err_message = "Macro not found";
761 } else if ( err.status == "403" ){
762 err_message = _("You do not have permission to delete this macro");
764 err_message = _("There was a problem, please check the logs");
766 humanMsg.displayAlert( _("Failed to delete macro: " + err_message), { className: 'humanError' } );
771 function loadMacro( name, id, shared ) {
772 $( '#macro-list li' ).removeClass( 'active' );
773 $(".macro_shared").prop("checked",false).hide();
774 $("#delete-macro").prop("disabled",true);
775 macroEditor.setOption( 'readOnly', false );
776 macroEditor.activeMacro = name;
777 macroEditor.activeMacroId = id;
780 macroEditor.setValue( '' );
783 $( '#macro-list li[data-name="' + name + '"][data-id="' + id + '"]' ).addClass( 'active' );
784 api_url = "/api/v1/advanced_editor/macros/";
785 if( shared ) { api_url += "shared/" }
789 contentType: "application/json",
792 .then(function(result) {
793 macroEditor.setValue( result.macro_text );
794 $(".macro_shared").show();
796 $(".macro_shared").prop("checked",true);
797 if( canCreatePublic ){
798 macroEditor.setOption( 'readOnly', false );
800 macroEditor.setOption( 'readOnly', true );
802 if( canDeletePublic ){
803 $("#delete-macro").prop("disabled",false);
806 macroEditor.setOption( 'readOnly', false );
807 $("#delete-macro").prop("disabled",false);
809 macroEditor.activeMacroShared = result.shared;
811 .fail(function(err) {
813 if( err.status == "404" ){
814 err_message = "Macro not found";
815 } else if ( err.status == "403" ){
816 err_message = _("You do not have permission to access this macro");
818 err_message = _("There was a problem, please check the logs");
820 humanMsg.displayAlert( _("Failed to load macros: ") + err_message, { className: 'humanError' } );
825 function convertOldMacros(){
826 $("#convert-macros").remove();
827 var macro_list = $.map( Preferences.user.macros, function( macro, name ) {
828 return $.extend( { name: name }, macro );
830 macro_list.sort( function( a, b ) {
831 return a.name.localeCompare(b.name);
833 $.each( macro_list, function( index, macro ) {
835 url: "/api/v1/advanced_editor/macros/",
837 contentType: "application/json",
838 data: JSON.stringify({
840 patron_id: [% logged_in_user.borrowernumber | html %],
841 macro_text: macro.contents,
846 .then(function(undef, result) {
847 delete Preferences.user.macros[macro.name];
848 Preferences.Save( [% logged_in_user.borrowernumber | html %] );
849 if( index == macro_list.length -1 ){
854 .fail(function(err) {
856 if( err.status == "403" ){
857 err_message = _("You do not have permission to create this macro");
859 err_message = _("There was a problem, please check the logs");
861 humanMsg.displayAlert( _("Failed to create macro: ") + err_message, { className: 'humanError' } );
866 function showSavedMacros( macros ) {
867 var scrollTop = $('#macro-list').scrollTop();
868 $( '#macro-list' ).empty();
869 $("#convert-macros").remove();
870 if( Object.keys(Preferences.user.macros).length ){
871 $convert = $( '<button class="btn btn-default" id="convert-macros" title="'+_("Convert browser storage macros")+'><i class="fa fa-adjust" aria-hidden="true"></i> Convert old macros</button>' );
872 $convert.click( function(){
873 if( !confirm( _("This will retrieve macros stored in the brower, save them in the database, and delete them from the browser. Proceed?") ) ){
878 $("#macro-toolbar").prepend($convert);
881 url: "/api/v1/advanced_editor/macros/",
883 contentType: "application/json",
886 .then(function(result) {
887 $.each(result,function( undef, macro ){
888 var $li = $( '<li data-name="' + macro.name + '" data-id="' + macro.macro_id + '"><a href="#">' + macro.name + '</a><ol class="macro-info"></ol></li>' );
889 if ( macro.macro_id == macroEditor.activeMacroId ) $li.addClass( 'active' );
890 $li.click( function() {
891 loadMacro(macro.name, macro.macro_id, macro.shared);
894 $('#macro-list').append($li);
897 .fail(function(err) {
898 var err_message = _("There was a problem, please check the logs");
899 humanMsg.displayAlert( _("Failed to load macros: ") + err_message, { className: 'humanError' } );
901 var $new_li = $( '<li class="new-macro"><a href="#">' + _("New macro...") + '</a></li>' );
902 $new_li.click( function() {
903 // TODO: make this a bit less retro
904 var name = prompt(_("Please enter the name for the new macro:"));
907 // if ( !Preferences.user.macros[name] ) storeMacro( name, { format: "rancor", contents: "" } );
909 url: "/api/v1/advanced_editor/macros/",
911 contentType: "application/json",
912 data: JSON.stringify({
914 patron_id: [% logged_in_user.borrowernumber | html %],
920 .then(function(result) {
922 loadMacro( result.name, result.macro_id );
924 .fail(function(err) {
926 if( err.status == "403" ){
927 err_message = _("You do not have permission to access this macro");
929 err_message = _("There was a problem, please check the logs");
931 humanMsg.displayAlert( _("Failed to create macro: ") + err_message, { className: 'humanError' } );
934 $('#macro-list').append($new_li);
935 $('#macro-list').scrollTop(scrollTop);
938 function saveMacro(shared) {
939 var name = macroEditor.activeMacro;
940 var macro_id = macroEditor.activeMacroId;
941 var was_shared = macroEditor.activeMacroShared;
943 if ( !name || macroEditor.savedGeneration == macroEditor.changeGeneration() && was_shared == shared ) return;
945 macroEditor.savedGeneration = macroEditor.changeGeneration();
946 api_url = "/api/v1/advanced_editor/macros/";
947 if( shared || was_shared ) { api_url += "shared/" }
950 url: api_url + macro_id,
952 contentType: "application/json",
953 data: JSON.stringify({
955 patron_id: [% logged_in_user.borrowernumber | html %],
956 macro_text: macroEditor.getValue(),
961 .then(function(result) {
962 $('#macro-save-message').text(_("Saved"));
963 macroEditor.activeMacroShared = shared;
966 .fail(function(err) {
968 if( err.status == "404" ){
969 err_message = _("Macro not found");
970 } else if ( err.status ="403" ){
971 err_message = _("You do not have permission to access this macro");
973 err_message = _("There was a problem, please check the logs");
975 humanMsg.displayAlert( _("Failed to save macro: ") + err_message, { className: 'humanError' } );
979 $(".macro_shared").change(function(){
987 // END Macro functions
989 $(document).ready( function() {
991 editor = new MARCEditor( {
992 onCursorActivity: function() {
993 $('#status-tag-info').empty();
994 $('#status-subfield-info').empty();
996 var field = editor.getCurrentField();
997 var cur = editor.getCursor();
999 if ( !field ) return;
1001 var taginfo = KohaBackend.GetTagInfo( '', field.tag );
1002 $('#status-tag-info').html( '<strong>' + field.tag + ':</strong> ' );
1005 $('#status-tag-info').append( '<a href="' + getFieldHelpURL( field.tag ) + '" target="_blank" class="show-field-help" title="' + _("Show help for this tag") + '">[?]</a> ' + taginfo.lib );
1007 var subfield = field.getSubfieldAt( cur.ch );
1008 if ( !subfield ) return;
1010 var subfieldinfo = taginfo.subfields[ subfield.code ];
1011 $('#status-subfield-info').html( '<strong>‡' + subfield.code + ':</strong> ' );
1013 if ( subfieldinfo ) {
1014 $('#status-subfield-info').append( subfieldinfo.lib );
1016 $('#status-subfield-info').append( '<em>' + _("Unknown subfield") + '</em>' );
1019 $('#status-tag-info').append( '<em>' + _("Unknown tag") + '</em>' );
1022 position: function (elt) { $(elt).insertAfter('#toolbar') },
1025 // Automatically detect resizes and change the height of the editor and position of modals.
1026 var resizeTimer = null;
1027 function onResize() {
1028 if ( resizeTimer == null ) resizeTimer = setTimeout( function() {
1031 var pos = $('#editor .CodeMirror').offset();
1032 if ( $('#changelanguage').length ) {
1033 $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 - $('#changelanguage').height() ); // 24 is hardcoded value but works well
1035 $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 );
1037 $('.modal-body').each( function() {
1038 $(this).height( $(window).height() * .8 - $(this).prevAll('.modal-header').height() );
1044 $( '#macro-ui' ).on( 'shown.bs.modal', function() {
1045 if ( macroEditor ) return;
1047 macroEditor = CodeMirror(
1048 $('#macro-editor')[0],
1051 'Ctrl-D': function( cm ) {
1052 var cur = cm.getCursor();
1054 cm.replaceRange( "‡", cur, null );
1063 macroEditor.on( 'change', function( cm, change ) {
1064 $('#macro-save-message').empty();
1065 if ( change.origin == 'setValue' ) return;
1067 if ( saveTimeout ) clearTimeout( saveTimeout );
1068 saveTimeout = setTimeout( function() {
1069 saveMacro(macroEditor.activeMacroShared);
1078 var saveableBackends = [];
1079 $.each( backends, function( id, backend ) {
1080 if ( backend.save ) saveableBackends.push( [ backend.saveLabel, id ] );
1082 saveableBackends.sort();
1083 $.each( saveableBackends, function( undef, backend ) {
1084 $( '#save-dropdown' ).append( '<li><a href="#" data-backend="' + backend[1] + '">' + backend[0] + '</a></li>' );
1088 $( '#save-record, #save-dropdown a' ).click( function() {
1089 $( '#save-record' ).find('i').attr( 'class', 'fa fa-spinner fa-spin' ).siblings( 'span' ).text( _("Saving...") );
1091 function finishCb(result) {
1092 if ( result.error == 'syntax' ) {
1093 humanMsg.displayAlert( _("Incorrect syntax, cannot save"), { className: 'humanError' } );
1094 } else if ( result.error == 'invalid' ) {
1095 humanMsg.displayAlert( _("Record structure invalid, cannot save"), { className: 'humanError' } );
1096 } else if ( result.error ) {
1097 humanMsg.displayAlert( _("Something went wrong, cannot save"), { className: 'humanError' } );
1098 } else if ( !result.error ) {
1099 humanMsg.displayAlert( _("Record saved "), { className: 'humanSuccess' } );
1102 $.each( result.errors || [], function( undef, error ) {
1103 switch ( error.type ) {
1105 editor.addError( error.line, _("Invalid tag number") );
1107 case 'noIndicators':
1108 editor.addError( error.line, _("Invalid indicators") );
1111 editor.addError( error.line, _("Tag has no subfields") );
1114 editor.addError( null, _("Missing mandatory tag: ") + error.tag );
1116 case 'missingSubfield':
1117 if ( error.subfield == '@' ) {
1118 editor.addError( error.line, _("Missing control field contents") );
1120 editor.addError( error.line, _("Missing mandatory subfield: ‡") + error.subfield );
1123 case 'unrepeatableTag':
1124 editor.addError( error.line, _("Tag ") + error.tag + _(" cannot be repeated") );
1126 case 'unrepeatableSubfield':
1127 editor.addError( error.line, _("Subfield ‡") + error.subfield + _(" cannot be repeated") );
1129 case 'itemTagUnsupported':
1130 editor.addError( error.line, _("Item tags cannot currently be saved") );
1135 $( '#save-record' ).find('i').attr( 'class', 'fa-solid fa-hard-drive' );
1137 if ( result.error ) {
1138 // Reset backend info
1139 setSource( [ state.backend, state.recordID ] );
1143 var backend = $( this ).data( 'backend' ) || ( state.saveBackend );
1144 if ( state.backend == backend ) {
1145 saveRecord( backend + '/' + state.recordID, editor, finishCb );
1147 saveRecord( backend + '/', editor, finishCb );
1153 $('#import-records').click( function() {
1154 $('#import-records-input')
1156 .change( function() {
1157 if ( !this.files || !this.files.length ) return;
1159 var file = this.files[0];
1160 var reader = new FileReader();
1162 reader.onload = function() {
1163 var record = new MARC.Record();
1165 if ( /\.(mrc|marc|iso|iso2709|marcstd)$/.test( file.name ) ) {
1166 record.loadISO2709( reader.result );
1167 } else if ( /\.(xml|marcxml)$/.test( file.name ) ) {
1168 record.loadMARCXML( reader.result );
1170 humanMsg.displayAlert( _("Unknown record type, cannot import"), { className: 'humanError' } );
1174 if (record.marc8_corrupted) humanMsg.displayMsg( '<h3>' + _("Possible record corruption") + '</h3><p>' + _("Record not marked as UTF-8, may be corrupted") + '</p>', { className: 'humanError' } );
1176 editor.displayRecord( record );
1179 reader.readAsText( file );
1186 $('#open-macros').click( function() {
1187 $('#macro-ui').modal('show');
1192 $('#run-macro').click( function() {
1193 var result = Macros.Run( editor, 'rancor', macroEditor.getValue() );
1195 if ( !result.errors.length ) {
1196 $('#macro-ui').modal('hide');
1197 editor.focus(); //Return cursor to editor after macro run
1202 $.each( result.errors, function() {
1203 var error = '<strong>' + _("Line ") + (this.line + 1) + ':</strong> ';
1205 switch ( this.error ) {
1206 case 'failed': error += _("failed to run"); break;
1207 case 'unrecognized': error += _("unrecognized command"); break;
1213 humanMsg.displayMsg( '<h3>' + _("Failed to run macro:") + '</h3><ul><li>' + errors.join('</li><li>') + '</li></ul>', { className: 'humanError' } );
1218 $('#delete-macro').click( function() {
1219 if ( !macroEditor.activeMacro || !confirm( _("Are you sure you want to delete this macro?") ) ) return;
1221 loadMacro( undefined );
1226 $( '#switch-editor' ).click( function() {
1227 if ( !confirm( _("Any changes will not be saved. Continue?") ) ) return;
1229 Cookies.set( "catalogue_editor_[% logged_in_user.borrowernumber | html %]", "basic", { expires: 365, path: '/', sameSite: 'Lax'} );
1231 if ( state.backend == 'catalog' ) {
1232 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=' + state.recordID;
1233 } else if ( state.backend == 'new' ) {
1234 window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl';
1236 humanMsg.displayAlert( _("Cannot open this record in the basic editor"), { className: 'humanError' } );
1240 $( '#show-advanced-search' ).click( function() {
1241 showAdvancedSearch();
1246 $('#advanced-search').submit( function() {
1247 startAdvancedSearch();
1252 $( document ).on( 'click', 'a.search-nav', function() {
1253 if ( Search.Fetch( { offset: $( this ).data( 'offset' ) } ) ) {
1254 $("#search-overlay").show();
1260 $( document ).on( 'click', 'th[data-sort-label]', function() {
1263 if ( $( this ).hasClass( 'sorting_asc' ) ) {
1269 if ( Search.Fetch( { sort_key: $( this ).data( 'sort-label' ), sort_direction: direction } ) ) {
1270 showSearchSorting( $( this ).data( 'sort-label' ), direction );
1272 $("#search-overlay").show();
1278 $( document ).on( 'change', 'input.search-toggle-server', function() {
1279 var server = z3950Servers[ $( this ).closest('li').data('server-id') ];
1280 server.checked = this.checked;
1282 if ( $('#search-results-ui').is( ':visible' ) && Search.Fetch() ) {
1283 $("#search-overlay").show();
1291 $("#advanced-search-ui, #search-results-ui, #macro-ui").each( function() {
1292 $(this).modal({ show: false });
1295 var $quicksearch = $('#quicksearch fieldset');
1296 $('<div id="quicksearch-overlay"><h3>' + _("Search unavailable") + '</h3> <p></p></div>').css({
1297 position: 'absolute',
1298 top: $quicksearch.offset().top,
1299 left: $quicksearch.offset().left,
1300 height: $quicksearch.outerHeight(),
1301 width: $quicksearch.outerWidth(),
1302 }).appendTo(document.body).hide();
1304 var prevAlerts = [];
1305 humanMsg.logMsg = function(msg, options) {
1306 $('#show-alerts').popover('hide');
1307 prevAlerts.unshift('<li>' + msg + '</li>');
1308 prevAlerts.splice(5, 999); // Truncate old messages
1311 $('#show-alerts').popover({
1313 placement: 'bottom',
1314 content: function() {
1315 return '<div id="alerts-container"><ul>' + prevAlerts.join('') + '</ul></div>';
1319 // Whitelist table elements to ensure popover displays all the info
1320 $.fn.popover.Constructor.DEFAULTS.whiteList.table = [];
1321 $.fn.popover.Constructor.DEFAULTS.whiteList.tr = [];
1322 $.fn.popover.Constructor.DEFAULTS.whiteList.td = [];
1323 $.fn.popover.Constructor.DEFAULTS.whiteList.div = [];
1324 $.fn.popover.Constructor.DEFAULTS.whiteList.tbody = [];
1325 $.fn.popover.Constructor.DEFAULTS.whiteList.thead = [];
1326 // https://getbootstrap.com/docs/3.4/javascript/#popovers
1328 $('#show-shortcuts').popover({
1330 placement: 'bottom',
1331 content: function() {
1332 return '<div id="shortcuts-container">' + $('#shortcuts-contents').html() + '</div>';
1336 $('#new-record' ).click( function() {
1337 if ( editor.modified && !confirm( _("Are you sure you want to erase your changes?") ) ) return;
1339 openRecord( 'new/', editor );
1343 window.onbeforeunload = function() {
1344 if(editor.modified )
1347 { return undefined; }
1350 $('a.change-framework').click( function() {
1351 $("#loading").show();
1352 editor.setFrameworkCode(
1353 $(this).data( 'frameworkcode' ),
1355 function ( error ) {
1356 if ( typeof error !== 'undefined' ) {
1357 humanMsg.displayAlert( _("Failed to change framework"), { className: 'humanError' } );
1359 $('#loading').hide();
1365 Preferences.Load( [% logged_in_user.borrowernumber || 0 | html %] );
1366 displayPreferences(editor);
1367 makeAuthorisedValueWidgets( '' );
1370 onresults: function(data) { showSearchResults( editor, data ) },
1371 onerror: handleSearchError,
1374 function finishCb( data ) {
1376 humanMsg.displayAlert( data.error );
1377 openRecord( 'new/', editor, finishCb );
1380 Resources.GetAll().done( function() {
1381 $("#loading").hide();
1382 $( window ).resize( onResize ).resize();
1387 if ( "[% auth_forwarded_hash | html %]" ) {
1388 document.location.hash = "[% auth_forwarded_hash | html %]";
1391 if ( !document.location.hash || !openRecord( document.location.hash.slice(1), editor, finishCb ) ) {
1392 openRecord( 'new/', editor, finishCb );
1398 <!-- / cateditor-ui.inc -->