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