Bug 11446: Use encodeURIComponent on search terms in authority lookup plugin
[koha.git] / koha-tmpl / intranet-tmpl / lib / koha / cateditor / marc-editor.js
1 /**
2  * Copyright 2015 ByWater Solutions
3  *
4  * This file is part of Koha.
5  *
6  * Koha is free software; you can redistribute it and/or modify it
7  * under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Koha is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Koha; if not, see <http://www.gnu.org/licenses>.
18  */
19
20 define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], function( MARC, KohaBackend, Preferences, TextMARC, Widget ) {
21
22     var NOTIFY_TIMEOUT = 250;
23
24     function editorCursorActivity( cm ) {
25         var editor = cm.marceditor;
26         var field = editor.getCurrentField();
27         if ( !field ) return;
28
29         // Set overwrite mode for tag numbers/indicators and contents of fixed fields
30         if ( field.isControlField || cm.getCursor().ch < 8 ) {
31             cm.toggleOverwrite(true);
32         } else {
33             cm.toggleOverwrite(false);
34         }
35
36         editor.onCursorActivity();
37     }
38
39     // This function exists to prevent inserting or partially deleting text that belongs to a
40     // widget. The 'marcAware' change source exists for other parts of the editor code to bypass
41     // this check.
42     function editorBeforeChange( cm, change ) {
43         var editor = cm.marceditor;
44         if ( editor.textMode || change.origin == 'marcAware' || change.origin == 'widget.clearToText' ) return;
45
46         // FIXME: Should only cancel changes if this is a control field/subfield widget
47         if ( change.from.line !== change.to.line || Math.abs( change.from.ch - change.to.ch ) > 1 || change.text.length != 1 || change.text[0].length != 0 ) return; // Not single-char change
48
49         if ( change.from.ch == change.to.ch - 1 && cm.findMarksAt( { line: change.from.line, ch: change.from.ch + 1 } ).length ) {
50             change.cancel();
51         } else if ( change.from.ch == change.to.ch && cm.findMarksAt(change.from).length && !change.text[0] == '‡' ) {
52             change.cancel();
53         }
54     }
55
56     function editorChanges( cm, changes ) {
57         var editor = cm.marceditor;
58         if ( editor.textMode ) return;
59
60         for (var i = 0; i < changes.length; i++) {
61             var change = changes[i];
62
63             var origin = change.from.line;
64             var newTo = CodeMirror.changeEnd(change);
65
66             for (var delLine = origin; delLine <= change.to.line; delLine++) {
67                 // Line deleted; currently nothing to do
68             }
69
70             for (var line = origin; line <= newTo.line; line++) {
71                 if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( cm.marceditor, line );
72                 if ( change.origin != 'setValue' && change.origin != 'marcWidgetPrefill' && change.origin != 'widget.clearToText' ) {
73                     cm.addLineClass( line, 'wrapper', 'modified-line' );
74                     editor.modified = true;
75                 }
76             }
77         }
78
79         Widget.ActivateAt( cm, cm.getCursor() );
80         cm.marceditor.startNotify();
81     }
82
83     function editorSetOverwriteMode( cm, newState ) {
84         var editor = cm.marceditor;
85
86         editor.overwriteMode = newState;
87     }
88
89     // Editor helper functions
90     function activateTabPosition( cm, pos, idx ) {
91         // Allow tabbing to as-yet-nonexistent positions
92         var lenDiff = pos.ch - cm.getLine( pos.line ).length;
93         if ( lenDiff > 0 ) {
94             var extra = '';
95             while ( lenDiff-- > 0 ) extra += ' ';
96             if ( pos.prefill ) extra += pos.prefill;
97             cm.replaceRange( extra, { line: pos.line } );
98         }
99
100         cm.setCursor( pos );
101         Widget.ActivateAt( cm, pos, idx );
102     }
103
104     function getTabPositions( editor, cur ) {
105         cur = cur || editor.cm.getCursor();
106         var field = editor.getFieldAt( cur.line );
107
108         if ( field ) {
109             if ( field.isControlField ) {
110                 var positions = [ { ch: 0 }, { ch: 4 } ];
111
112                 $.each( positions, function( undef, pos ) {
113                     pos.line = cur.line;
114                 } );
115
116                 return positions;
117             } else {
118                 var positions = [ { ch: 0 }, { ch: 4, prefill: '_' }, { ch: 6, prefill: '_' } ];
119
120                 $.each( positions, function( undef, pos ) {
121                     pos.line = cur.line;
122                 } );
123                 $.each( field.getSubfields(), function( undef, subfield ) {
124                     positions.push( { line: cur.line, ch: subfield.contentsStart } );
125                 } );
126
127                 // Allow to tab to start of empty field
128                 if ( field.getSubfields().length == 0 ) {
129                     positions.push( { line: cur.line, ch: 8 } );
130                 }
131
132                 return positions;
133             }
134         } else {
135             return [];
136         }
137     }
138     var _editorKeys = {};
139
140     _editorKeys[insert_copyright] =  function( cm ) {
141             cm.replaceRange( '©', cm.getCursor() );
142         }
143
144     _editorKeys[insert_copyright_sound] = function( cm ) {
145             cm.replaceRange( '℗', cm.getCursor() );
146         }
147
148     _editorKeys[new_line] = function( cm ) {
149             var cursor = cm.getCursor();
150             cm.replaceRange( '\n', { line: cursor.line }, null, 'marcAware' );
151             cm.setCursor( { line: cursor.line + 1, ch: 0 } );
152         }
153
154     _editorKeys[line_break] =  function( cm ) {
155             var cur = cm.getCursor();
156
157             cm.replaceRange( "\n", cur, null );
158         }
159
160     _editorKeys[delete_field] =  function( cm ) {
161             // Delete line (or cut)
162             if ( cm.somethingSelected() ) return true;
163             var curLine = cm.getLine( cm.getCursor().line );
164
165             $("#clipboard").prepend('<option>'+curLine+'</option>');
166             $("#clipboard option:first-child").prop('selected', true);
167
168             cm.execCommand('deleteLine');
169         }
170
171     _editorKeys[link_authorities] =  function( cm ) {
172             // Launch the auth search popup
173             var field = cm.marceditor.getCurrentField();
174
175             if ( !field ) return;
176             if ( authInfo[field.tag] == undefined ) return;
177             authtype = authInfo[field.tag].authtypecode;
178             index = 'tag_'+field.tag+'_rancor';
179             var mainmainstring = '';
180             if( field.getSubfields( authInfo[field.tag].subfield ).length != 0 ){
181                 mainmainstring += field.getSubfields( authInfo[field.tag].subfield )[0].text;
182             }
183
184             var subfields = field.getSubfields();
185             var mainstring= '';
186             for(i=0;i < subfields.length ;i++){
187                 if ( authInfo[field.tag].subfield == subfields[i].code ) continue;
188                 if( subfields[i].code == '9' ) continue;
189                 mainstring += subfields[i].text+' ';
190             }
191             newin=window.open("../authorities/auth_finder.pl?source=biblio&authtypecode="+authtype+"&index="+index+"&value_mainstr="+encodeURIComponent(mainmainstring)+"&value_main="+encodeURIComponent(mainstring), "_blank",'width=700,height=550,toolbar=false,scrollbars=yes');
192
193         }
194
195     _editorKeys[delete_subfield] = function( cm ) {
196             // Delete subfield
197             var field = cm.marceditor.getCurrentField();
198             if ( !field ) return;
199
200             var curCursor = cm.getCursor();
201             var subfield = field.getSubfieldAt( curCursor.ch );
202             var subfieldText= cm.getRange({line:curCursor.line,ch:subfield.start},{line:curCursor.line,ch:subfield.end});
203             if ( subfield ) {
204                 $("#clipboard").prepend('<option>'+subfieldText+'</option>');
205                 $("#clipboard option:first-child").prop('selected', true);
206                 subfield.delete();
207             }
208         }
209
210     _editorKeys[copy_line] = function( cm ) {
211             // Copy line
212             if ( cm.somethingSelected() ) return true;
213             var curLine = cm.getLine( cm.getCursor().line );
214             $("#clipboard").prepend('<option>'+curLine+'</option>');
215             $("#clipboard option:first-child").prop('selected', true);
216         }
217
218     _editorKeys[copy_subfield] = function( cm ) {
219             // Copy subfield
220             var field = cm.marceditor.getCurrentField();
221             if ( !field ) return;
222
223             var curCursor = cm.getCursor();
224             var subfield = field.getSubfieldAt( curCursor.ch );
225             var subfieldText= cm.getRange({line:curCursor.line,ch:subfield.start},{line:curCursor.line,ch:subfield.end});
226             if ( subfield ) {
227                 $("#clipboard").prepend('<option>'+subfieldText+'</option>');
228                 $("#clipboard option:first-child").prop('selected', true);
229             }
230         }
231
232     _editorKeys[paste_line] = function( cm ) {
233             // Paste line from "clipboard"
234             if ( cm.somethingSelected() ) return true;
235             var cBoard = document.getElementById("clipboard");
236             var strUser = cBoard.options[cBoard.selectedIndex].text;
237             cm.replaceRange( strUser, cm.getCursor(), null );
238         }
239
240     _editorKeys[insert_line] = function( cm ) {
241             // Copy line and insert below
242             if ( cm.somethingSelected() ) return true;
243             var curLine = cm.getLine( cm.getCursor().line );
244             cm.execCommand('newlineAndIndent');
245             cm.replaceRange( curLine, cm.getCursor(), null );
246         }
247
248      _editorKeys[next_position] =  function( cm ) {
249             // Move through parts of tag/fixed fields
250             var positions = getTabPositions( cm.marceditor );
251             var cur = cm.getCursor();
252
253             for ( var i = 0; i < positions.length; i++ ) {
254                 if ( positions[i].ch > cur.ch ) {
255                     activateTabPosition( cm, positions[i] );
256                     return false;
257                 }
258             }
259
260             cm.setCursor( { line: cur.line + 1, ch: 0 } );
261         }
262
263     _editorKeys[prev_position] = function( cm ) {
264             // Move backwards through parts of tag/fixed fields
265             var positions = getTabPositions( cm.marceditor );
266             var cur = cm.getCursor();
267
268             for ( var i = positions.length - 1; i >= 0; i-- ) {
269                 if ( positions[i].ch < cur.ch ) {
270                     activateTabPosition( cm, positions[i], -1 );
271                     return false;
272                 }
273             }
274
275             if ( cur.line == 0 ) return;
276
277             var prevPositions = getTabPositions( cm.marceditor, { line: cur.line - 1, ch: cm.getLine( cur.line - 1 ).length } );
278
279             if ( prevPositions.length ) {
280                 activateTabPosition( cm, prevPositions[ prevPositions.length - 1 ], -1 );
281             } else {
282                 cm.setCursor( { line: cur.line - 1, ch: 0 } );
283             }
284         }
285
286     _editorKeys[insert_delimiter] = function(cm){
287         var cur = cm.getCursor();
288
289         cm.replaceRange( "‡", cur, null );
290     }
291
292     _editorKeys[toggle_keyboard] = function( cm ) {
293        let keyboard = $(cm.getInputField()).getkeyboard();
294        keyboard.isVisible()?keyboard.close():keyboard.reveal();
295     }
296
297     // The objects below are part of a field/subfield manipulation API, accessed through the base
298     // editor object.
299     //
300     // Each one is tied to a particular line; this means that using a field or subfield object after
301     // any other changes to the record will cause entertaining explosions. The objects are meant to
302     // be temporary, and should only be reused with great care. The macro code does this only
303     // because it is careful to dispose of the object after any other updates.
304     //
305     // Note, however, tha you can continue to use a field object after changing subfields. It's just
306     // the subfield objects that become invalid.
307
308     // This is an exception raised by the EditorSubfield and EditorField when an invalid change is
309     // attempted.
310     function FieldError(line, message) {
311         this.line = line;
312         this.message = message;
313     };
314
315     FieldError.prototype.toString = function() {
316         return 'FieldError(' + this.line + ', "' + this.message + '")';
317     };
318
319     // This is the temporary object for a particular subfield in a field. Any change to any other
320     // subfields will invalidate this subfield object.
321     function EditorSubfield( field, index, start, end ) {
322         this.field = field;
323         this.index = index;
324         this.start = start;
325         this.end = end;
326
327         if ( this.field.isControlField ) {
328             this.contentsStart = start;
329             this.code = '@';
330         } else {
331             this.contentsStart = start + 2;
332             this.code =  this.field.contents.substr( this.start + 1, 1 );
333         }
334
335         this.cm = field.cm;
336
337         var marks = this.cm.findMarksAt( { line: field.line, ch: this.contentsStart } );
338         if ( marks[0] && marks[0].widget ) {
339             this.widget = marks[0].widget;
340
341             this.text = this.widget.text;
342             this.setText = this.widget.setText;
343             this.getFixed = this.widget.getFixed;
344             this.setFixed = this.widget.setFixed;
345         } else {
346             this.widget = null;
347             this.text = this.field.contents.substr( this.contentsStart, end - this.contentsStart );
348         }
349     };
350
351     $.extend( EditorSubfield.prototype, {
352         _invalid: function() {
353             return this.field._subfieldsInvalid();
354         },
355
356         delete: function() {
357             this.cm.replaceRange( "", { line: this.field.line, ch: this.start }, { line: this.field.line, ch: this.end }, 'marcAware' );
358         },
359         focus: function() {
360             this.cm.setCursor( { line: this.field.line, ch: this.contentsStart } );
361         },
362         focusEnd: function() {
363             this.cm.setCursor( { line: this.field.line, ch: this.end } );
364         },
365         getText: function() {
366             return this.text;
367         },
368         setText: function( text ) {
369             if ( !this._invalid() ) throw new FieldError( this.field.line, 'subfield invalid' );
370             this.cm.replaceRange( text, { line: this.field.line, ch: this.contentsStart }, { line: this.field.line, ch: this.end }, 'marcAware' );
371             this.field._invalidateSubfields();
372         },
373     } );
374
375     function EditorField( editor, line ) {
376         this.editor = editor;
377         this.line = line;
378
379         this.cm = editor.cm;
380
381         this._updateInfo();
382         this.tag = this.contents.substr( 0, 3 );
383         this.isControlField = ( this.tag < '010' );
384
385         if ( this.isControlField ) {
386             this._ind1 = this.contents.substr( 4, 1 );
387             this._ind2 = this.contents.substr( 6, 1 );
388         } else {
389             this._ind1 = null;
390             this._ind2 = null;
391         }
392
393         this.subfields = null;
394     }
395
396     $.extend( EditorField.prototype, {
397         _subfieldsInvalid: function() {
398             return !this.subfields;
399         },
400         _invalidateSubfields: function() {
401             this._subfields = null;
402         },
403
404         _updateInfo: function() {
405             this.info = this.editor.getLineInfo( { line: this.line, ch: 0 } );
406             if ( this.info == null ) throw new FieldError( 'Invalid field' );
407             this.contents = this.info.contents;
408         },
409         _scanSubfields: function() {
410             this._updateInfo();
411
412             if ( this.isControlField ) {
413                 this._subfields = [ new EditorSubfield( this, 0, 4, this.contents.length ) ];
414             } else {
415                 var field = this;
416                 var subfields = this.info.subfields;
417                 this._subfields = [];
418
419                 for (var i = 0; i < this.info.subfields.length; i++) {
420                     var end = i == subfields.length - 1 ? this.contents.length : subfields[i+1].ch;
421
422                     this._subfields.push( new EditorSubfield( this, i, subfields[i].ch, end ) );
423                 }
424             }
425         },
426
427         delete: function() {
428             this.cm.replaceRange( "", { line: this.line, ch: 0 }, { line: this.line + 1, ch: 0 }, 'marcAware' );
429         },
430         focus: function() {
431             this.cm.setCursor( { line: this.line, ch: 0 } );
432
433             return this;
434         },
435
436         getText: function() {
437             var result = '';
438
439             $.each( this.getSubfields(), function() {
440                 if ( this.code != '@' ) result += '‡' + this.code;
441
442                 result += this.getText();
443             } );
444
445             return result;
446         },
447         setText: function( text ) {
448             var indicator_match = /^([_ 0-9])([_ 0-9])\‡/.exec( text );
449             if ( indicator_match ) {
450                 text = text.substr(2);
451                 this.setIndicator1( indicator_match[1] );
452                 this.setIndicator2( indicator_match[2] );
453             }
454
455             this.cm.replaceRange( text, { line: this.line, ch: this.isControlField ? 4 : 8 }, { line: this.line }, 'marcAware' );
456             this._invalidateSubfields();
457
458             return this;
459         },
460
461         getIndicator1: function() {
462             return this._ind1;
463         },
464         getIndicator2: function() {
465             return this._ind2;
466         },
467         setIndicator1: function(val) {
468             if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field');
469
470             this._ind1 = ( !val || val == ' ' ) ? '_' : val;
471             this.cm.replaceRange( this._ind1, { line: this.line, ch: 4 }, { line: this.line, ch: 5 }, 'marcAware' );
472
473             return this;
474         },
475         setIndicator2: function(val) {
476             if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field');
477
478             this._ind2 = ( !val || val == ' ' ) ? '_' : val;
479             this.cm.replaceRange( this._ind2, { line: this.line, ch: 6 }, { line: this.line, ch: 7 }, 'marcAware' );
480
481             return this;
482         },
483
484         appendSubfield: function( code ) {
485             if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field');
486
487             this._invalidateSubfields();
488             this.cm.replaceRange( '‡' + code, { line: this.line }, null, 'marcAware' );
489             var subfields = this.getSubfields();
490
491             return subfields[ subfields.length - 1 ];
492         },
493         insertSubfield: function( code, position ) {
494             if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field');
495
496             position = position || 0;
497
498             var subfields = this.getSubfields();
499             this._invalidateSubfields();
500             this.cm.replaceRange( '‡' + code, { line: this.line, ch: subfields[position] ? subfields[position].start : null }, null, 'marcAware' );
501             subfields = this.getSubfields();
502
503             return subfields[ position ];
504         },
505         getSubfields: function( code ) {
506             if ( !this._subfields ) this._scanSubfields();
507             if ( code == null ) return this._subfields;
508
509             var result = [];
510
511             $.each( this._subfields, function() {
512                 if ( code == null || this.code == code ) result.push(this);
513             } );
514
515             return result;
516         },
517         getFirstSubfield: function( code ) {
518             var result = this.getSubfields( code );
519
520             return ( result && result.length ) ? result[0] : null;
521         },
522         getSubfieldAt: function( ch ) {
523             var subfields = this.getSubfields();
524
525             for (var i = 0; i < subfields.length; i++) {
526                 if ( subfields[i].start < ch && subfields[i].end >= ch ) return subfields[i];
527             }
528         },
529     } );
530
531     function MARCEditor( options ) {
532         this.frameworkcode = '';
533
534         this.cm = CodeMirror(
535             options.position,
536             {
537                 extraKeys: _editorKeys,
538                 gutters: [
539                     'modified-line-gutter',
540                 ],
541                 lineWrapping: true,
542                 mode: {
543                     name: 'marc',
544                     nonRepeatableTags: KohaBackend.GetTagsBy( '', 'repeatable', '0' ),
545                     nonRepeatableSubfields: KohaBackend.GetSubfieldsBy( '', 'repeatable', '0' )
546                 }
547             }
548         );
549         var inf = this.cm.getInputField();
550         var self = this;
551         var kb = $(inf).keyboard({
552             //keyBinding: "mousedown touchstart",
553             usePreview: false,
554             lockInput: false,
555             autoAccept: true,
556             autoAcceptOnEsc: true,
557             userClosed: true,
558             //alwaysOpen: true,
559             openOn : '',
560             position: {
561               of: $("#statusbar"), // optional - null (attach to input/textarea) or a jQuery object (attach elsewhere)
562               my: 'center top',
563               at: 'center bottom',
564               at2: 'center bottom' // used when "usePreview" is false (centers keyboard at bottom of the input/textarea)
565             },
566             beforeInsert: function(evnt, keyboard, elem, txt) {
567               var position = self.cm.getCursor();
568               if (txt === "\b") {
569                 self.cm.execCommand("delCharBefore");
570               }
571               if (txt === "\b" && position.ch === 0 && position.line !== 0) {
572                 elem.value = self.cm.getLine(position.line) || "";
573                 txt = "";
574               }
575               return txt;
576             },
577             visible: function() {
578                 $('#set-keyboard-layout').removeClass('hide');
579             },
580             hidden: function(e, keyboard, el, accepted) {
581                 inf.focus();
582                 $('#set-keyboard-layout').addClass('hide');
583             }
584           }).getkeyboard();
585
586
587         Object.keys($.keyboard.layouts).forEach(function(layout) {
588             var div = $('#keyboard-layout .layouts').append('<div class="layout" data-layout="'+layout+'" data-name="'+($.keyboard.layouts[layout].name||layout)+'" >'+($.keyboard.layouts[layout].name||layout)+'</div>')
589             if(kb.layout == layout) {
590                 div.addClass('active');
591             }
592         });
593         $('#keyboard-layout')
594             .on('show.bs.modal', function() {
595                 kb.close();
596                 $('#keyboard-layout .filter').focus();
597                 $('#set-keyboard-layout').removeClass('hide');
598             })
599             .on('hide.bs.modal', function() {
600                 !kb.isVisible() && kb.reveal();
601             });
602         $('#keyboard-layout .layout').click(function(event) {
603             $('#keyboard-layout .layout').removeClass('active');
604             $(this).addClass('active');
605             var layout = $(this).data().layout;
606             kb.redraw(layout);
607             $('#keyboard-layout').modal('hide');
608             $('#keyboard-layout .filter').val('');
609             $('#keyboard-layout .layout').show();
610         });
611         $('#keyboard-layout .filter').keyup(function() {
612             var val = $(this).val();
613             if(!val||!val.length) return $('#keyboard-layout .layout').show();
614             var filter = new RegExp(val, 'i');
615             $('#keyboard-layout .layout').hide();
616             $('#keyboard-layout .layout').each(function() {
617                 var name = $(this).data().name;
618                 if(filter.test(name)) $(this).show();
619             })
620         });
621
622         this.cm.marceditor = this;
623
624         this.cm.on( 'beforeChange', editorBeforeChange );
625         this.cm.on( 'changes', editorChanges );
626         this.cm.on( 'cursorActivity', editorCursorActivity );
627         this.cm.on( 'overwriteToggle', editorSetOverwriteMode );
628
629         this.onCursorActivity = options.onCursorActivity;
630
631         this.subscribers = [];
632         this.subscribe( function( marceditor ) {
633             Widget.Notify( marceditor );
634         } );
635     }
636
637     MARCEditor.FieldError = FieldError;
638
639     $.extend( MARCEditor.prototype, {
640         setUseWidgets: function( val ) {
641             if ( val ) {
642                 for ( var line = 0; line <= this.cm.lastLine(); line++ ) {
643                     Widget.UpdateLine( this, line );
644                 }
645             } else {
646                 $.each( this.cm.getAllMarks(), function( undef, mark ) {
647                     if ( mark.widget ) mark.widget.clearToText();
648                 } );
649             }
650         },
651
652         focus: function() {
653             this.cm.focus();
654         },
655
656         getCursor: function() {
657             return this.cm.getCursor();
658         },
659
660         refresh: function() {
661             this.cm.refresh();
662         },
663
664         setFrameworkCode: function( code, updateFields, callback ) {
665             this.frameworkcode = code;
666             $( 'a.change-framework i.selected' ).addClass( 'hidden' );
667             $( 'a.change-framework i.unselected' ).removeClass( 'hidden' );
668             $( 'a.change-framework[data-frameworkcode="' + code + '"] i.unselected' ).addClass( 'hidden' );
669             $( 'a.change-framework[data-frameworkcode="' + code + '"] i.selected' ).removeClass( 'hidden ');
670             var cm = this.cm;
671             KohaBackend.InitFramework( code, function ( error ) {
672                 cm.setOption( 'mode', {
673                     name: 'marc',
674                     nonRepeatableTags: KohaBackend.GetTagsBy( code, 'repeatable', '0' ),
675                     nonRepeatableSubfields: KohaBackend.GetSubfieldsBy( code, 'repeatable', '0' )
676                 });
677                 if ( updateFields ) {
678                     var record = TextMARC.TextToRecord( cm.getValue() );
679                     KohaBackend.FillRecord( code, record );
680                     cm.setValue( TextMARC.RecordToText(record) );
681                 }
682                 callback( error );
683             } );
684         },
685
686         displayRecord: function( record ) {
687             this.cm.setValue( TextMARC.RecordToText(record) );
688             this.modified = false;
689             this.setFrameworkCode(
690                 typeof record.frameworkcode !== 'undefined' ? record.frameworkcode : '',
691                 false,
692                 function ( error ) {
693                     if ( typeof error !== 'undefined' ) {
694                         humanMsg.displayAlert( _(error), { className: 'humanError' } );
695                     }
696                 }
697             );
698         },
699
700         getRecord: function() {
701             this.textMode = true;
702
703             $.each( this.cm.getAllMarks(), function( undef, mark ) {
704                 if ( mark.widget ) mark.widget.clearToText();
705             } );
706             var record = TextMARC.TextToRecord( this.cm.getValue() );
707             for ( var line = 0; line <= this.cm.lastLine(); line++ ) {
708                 if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( this, line );
709             }
710
711             this.textMode = false;
712
713             record.frameworkcode = this.frameworkcode;
714             return record;
715         },
716
717         getLineInfo: function( pos ) {
718             var contents = this.cm.getLine( pos.line );
719             if ( contents == null ) return {};
720
721             var tagNumber = contents.match( /^([A-Za-z0-9]{3})/ );
722
723             if ( !tagNumber ) return null; // No tag at all on this line
724             tagNumber = tagNumber[1];
725
726             if ( tagNumber < '010' ) return { tagNumber: tagNumber, contents: contents }; // No current subfield
727
728             var matcher = /‡([a-z0-9%])/g;
729             var match;
730
731             var subfields = [];
732             var currentSubfield;
733
734             while ( ( match = matcher.exec(contents) ) ) {
735                 subfields.push( { code: match[1], ch: match.index } );
736                 if ( match.index < pos.ch ) currentSubfield = match[1];
737             }
738
739             return { tagNumber: tagNumber, subfields: subfields, currentSubfield: currentSubfield, contents: contents };
740         },
741
742         addError: function( line, error ) {
743             var found = false;
744             var options = {};
745
746             if ( line == null ) {
747                 line = 0;
748                 options.above = true;
749             }
750
751             $.each( this.cm.getLineHandle(line).widgets || [], function( undef, widget ) {
752                 if ( !widget.isErrorMarker ) return;
753
754                 found = true;
755
756                 $( widget.node ).append( '; ' + error );
757                 widget.changed();
758
759                 return false;
760             } );
761
762             if ( found ) return;
763
764             var node = $( '<div class="structure-error"><i class="fa fa-remove"></i> ' + error + '</div>' )[0];
765             var widget = this.cm.addLineWidget( line, node, options );
766
767             widget.node = node;
768             widget.isErrorMarker = true;
769         },
770
771         removeErrors: function() {
772             for ( var line = 0; line < this.cm.lineCount(); line++ ) {
773                 $.each( this.cm.getLineHandle( line ).widgets || [], function( undef, lineWidget ) {
774                     if ( lineWidget.isErrorMarker ) lineWidget.clear();
775                 } );
776             }
777         },
778
779         startNotify: function() {
780             if ( this.notifyTimeout ) clearTimeout( this.notifyTimeout );
781             this.notifyTimeout = setTimeout( $.proxy( function() {
782                 this.notifyAll();
783
784                 this.notifyTimeout = null;
785             }, this ), NOTIFY_TIMEOUT );
786         },
787
788         notifyAll: function() {
789             $.each( this.subscribers, $.proxy( function( undef, subscriber ) {
790                 subscriber(this);
791             }, this ) );
792         },
793
794         subscribe: function( subscriber ) {
795             this.subscribers.push( subscriber );
796         },
797
798         createField: function( tag, line ) {
799             var contents = tag + ( tag < '010' ? ' ' : ' _ _ ' );
800
801             if ( line > this.cm.lastLine() ) {
802                 contents = '\n' + contents;
803             } else {
804                 contents = contents + '\n';
805             }
806
807             this.cm.replaceRange( contents, { line: line, ch: 0 }, null, 'marcAware' );
808
809             return new EditorField( this, line );
810         },
811
812         createFieldOrdered: function( tag ) {
813             var line, contents;
814
815             for ( line = 0; (contents = this.cm.getLine(line)); line++ ) {
816                 if ( contents && contents.substr(0, 3) > tag ) break;
817             }
818
819             return this.createField( tag, line );
820         },
821
822         createFieldGrouped: function( tag ) {
823             // Control fields should be inserted in actual order, whereas other fields should be
824             // inserted grouped
825             if ( tag < '010' ) return this.createFieldOrdered( tag );
826
827             var line, contents;
828
829             for ( line = 0; (contents = this.cm.getLine(line)); line++ ) {
830                 if ( contents && contents[0] > tag[0] ) break;
831             }
832
833             return this.createField( tag, line );
834         },
835
836         getFieldAt: function( line ) {
837             try {
838                 return new EditorField( this, line );
839             } catch (e) {
840                 return null;
841             }
842         },
843
844         getCurrentField: function() {
845             return this.getFieldAt( this.cm.getCursor().line );
846         },
847
848         getFields: function( tag ) {
849             var result = [];
850
851             if ( tag != null ) tag += ' ';
852
853             for ( var line = 0; line < this.cm.lineCount(); line++ ) {
854                 if ( tag && this.cm.getLine(line).substr( 0, 4 ) != tag ) continue;
855
856                 // If this throws a FieldError, pretend it doesn't exist
857                 try {
858                     result.push( new EditorField( this, line ) );
859                 } catch (e) {
860                     if ( !( e instanceof FieldError ) ) throw e;
861                 }
862             }
863
864             return result;
865         },
866
867         getFirstField: function( tag ) {
868             var result = this.getFields( tag );
869
870             return ( result && result.length ) ? result[0] : null;
871         },
872
873         getAllFields: function( tag ) {
874             return this.getFields( null );
875         },
876     } );
877
878     return MARCEditor;
879 } );