Bug 18904: Remove debugging code
[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
139     var _editorKeys = {
140         'Alt-C': function( cm ) {
141             cm.replaceRange( '©', cm.getCursor() );
142         },
143
144         'Alt-P': function( cm ) {
145             cm.replaceRange( '℗', cm.getCursor() );
146         },
147
148         Enter: 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         'Shift-Enter': function( cm ) {
155             var cur = cm.getCursor();
156
157             cm.replaceRange( "\n", cur, null );
158         },
159
160         'Ctrl-X': function( cm ) {
161             // Delete line (or cut)
162             if ( cm.somethingSelected() ) return true;
163
164             cm.execCommand('deleteLine');
165         },
166
167         'Ctrl-L': function( cm ) {
168             // Launch the auth search popup
169             var field = cm.marceditor.getCurrentField();
170
171             if ( !field ) return;
172             if ( authInfo[field.tag] == undefined ) return;
173             authtype = authInfo[field.tag].authtypecode;
174             index = 'rancor';
175             var mainmainstring = '';
176             if( field.getSubfields( authInfo[field.tag].subfield ).length != 0 ){
177                 mainmainstring += field.getSubfields( authInfo[field.tag].subfield )[0].text;
178             }
179
180             var subfields = field.getSubfields();
181             var mainstring= '';
182             for(i=0;i < subfields.length ;i++){
183                 if ( authInfo[field.tag].subfield == subfields[i].code ) continue;
184                 mainstring += subfields[i].text+' ';
185             }
186             newin=window.open("../authorities/auth_finder.pl?source=biblio&authtypecode="+authtype+"&index="+index+"&value_mainstr="+encodeURI(mainmainstring)+"&value_main="+encodeURI(mainstring), "_blank",'width=700,height=550,toolbar=false,scrollbars=yes');
187
188         },
189
190         'Shift-Ctrl-X': function( cm ) {
191             // Delete subfield
192             var field = cm.marceditor.getCurrentField();
193             if ( !field ) return;
194
195             var subfield = field.getSubfieldAt( cm.getCursor().ch );
196             if ( subfield ) subfield.delete();
197         },
198
199         Tab: function( cm ) {
200             // Move through parts of tag/fixed fields
201             var positions = getTabPositions( cm.marceditor );
202             var cur = cm.getCursor();
203
204             for ( var i = 0; i < positions.length; i++ ) {
205                 if ( positions[i].ch > cur.ch ) {
206                     activateTabPosition( cm, positions[i] );
207                     return false;
208                 }
209             }
210
211             cm.setCursor( { line: cur.line + 1, ch: 0 } );
212         },
213
214         'Shift-Tab': function( cm ) {
215             // Move backwards through parts of tag/fixed fields
216             var positions = getTabPositions( cm.marceditor );
217             var cur = cm.getCursor();
218
219             for ( var i = positions.length - 1; i >= 0; i-- ) {
220                 if ( positions[i].ch < cur.ch ) {
221                     activateTabPosition( cm, positions[i], -1 );
222                     return false;
223                 }
224             }
225
226             if ( cur.line == 0 ) return;
227
228             var prevPositions = getTabPositions( cm.marceditor, { line: cur.line - 1, ch: cm.getLine( cur.line - 1 ).length } );
229
230             if ( prevPositions.length ) {
231                 activateTabPosition( cm, prevPositions[ prevPositions.length - 1 ], -1 );
232             } else {
233                 cm.setCursor( { line: cur.line - 1, ch: 0 } );
234             }
235         },
236
237         'Ctrl-D': function( cm ) {
238             // Insert subfield delimiter
239             var cur = cm.getCursor();
240
241             cm.replaceRange( "‡", cur, null );
242         },
243     };
244
245     // The objects below are part of a field/subfield manipulation API, accessed through the base
246     // editor object.
247     //
248     // Each one is tied to a particular line; this means that using a field or subfield object after
249     // any other changes to the record will cause entertaining explosions. The objects are meant to
250     // be temporary, and should only be reused with great care. The macro code does this only
251     // because it is careful to dispose of the object after any other updates.
252     //
253     // Note, however, tha you can continue to use a field object after changing subfields. It's just
254     // the subfield objects that become invalid.
255
256     // This is an exception raised by the EditorSubfield and EditorField when an invalid change is
257     // attempted.
258     function FieldError(line, message) {
259         this.line = line;
260         this.message = message;
261     };
262
263     FieldError.prototype.toString = function() {
264         return 'FieldError(' + this.line + ', "' + this.message + '")';
265     };
266
267     // This is the temporary object for a particular subfield in a field. Any change to any other
268     // subfields will invalidate this subfield object.
269     function EditorSubfield( field, index, start, end ) {
270         this.field = field;
271         this.index = index;
272         this.start = start;
273         this.end = end;
274
275         if ( this.field.isControlField ) {
276             this.contentsStart = start;
277             this.code = '@';
278         } else {
279             this.contentsStart = start + 2;
280             this.code =  this.field.contents.substr( this.start + 1, 1 );
281         }
282
283         this.cm = field.cm;
284
285         var marks = this.cm.findMarksAt( { line: field.line, ch: this.contentsStart } );
286         if ( marks[0] && marks[0].widget ) {
287             this.widget = marks[0].widget;
288
289             this.text = this.widget.text;
290             this.setText = this.widget.setText;
291             this.getFixed = this.widget.getFixed;
292             this.setFixed = this.widget.setFixed;
293         } else {
294             this.widget = null;
295             this.text = this.field.contents.substr( this.contentsStart, end - this.contentsStart );
296         }
297     };
298
299     $.extend( EditorSubfield.prototype, {
300         _invalid: function() {
301             return this.field._subfieldsInvalid();
302         },
303
304         delete: function() {
305             this.cm.replaceRange( "", { line: this.field.line, ch: this.start }, { line: this.field.line, ch: this.end }, 'marcAware' );
306         },
307         focus: function() {
308             this.cm.setCursor( { line: this.field.line, ch: this.contentsStart } );
309         },
310         focusEnd: function() {
311             this.cm.setCursor( { line: this.field.line, ch: this.end } );
312         },
313         getText: function() {
314             return this.text;
315         },
316         setText: function( text ) {
317             if ( !this._invalid() ) throw new FieldError( this.field.line, 'subfield invalid' );
318             this.cm.replaceRange( text, { line: this.field.line, ch: this.contentsStart }, { line: this.field.line, ch: this.end }, 'marcAware' );
319             this.field._invalidateSubfields();
320         },
321     } );
322
323     function EditorField( editor, line ) {
324         this.editor = editor;
325         this.line = line;
326
327         this.cm = editor.cm;
328
329         this._updateInfo();
330         this.tag = this.contents.substr( 0, 3 );
331         this.isControlField = ( this.tag < '010' );
332
333         if ( this.isControlField ) {
334             this._ind1 = this.contents.substr( 4, 1 );
335             this._ind2 = this.contents.substr( 6, 1 );
336         } else {
337             this._ind1 = null;
338             this._ind2 = null;
339         }
340
341         this.subfields = null;
342     }
343
344     $.extend( EditorField.prototype, {
345         _subfieldsInvalid: function() {
346             return !this.subfields;
347         },
348         _invalidateSubfields: function() {
349             this._subfields = null;
350         },
351
352         _updateInfo: function() {
353             this.info = this.editor.getLineInfo( { line: this.line, ch: 0 } );
354             if ( this.info == null ) throw new FieldError( 'Invalid field' );
355             this.contents = this.info.contents;
356         },
357         _scanSubfields: function() {
358             this._updateInfo();
359
360             if ( this.isControlField ) {
361                 this._subfields = [ new EditorSubfield( this, 0, 4, this.contents.length ) ];
362             } else {
363                 var field = this;
364                 var subfields = this.info.subfields;
365                 this._subfields = [];
366
367                 for (var i = 0; i < this.info.subfields.length; i++) {
368                     var end = i == subfields.length - 1 ? this.contents.length : subfields[i+1].ch;
369
370                     this._subfields.push( new EditorSubfield( this, i, subfields[i].ch, end ) );
371                 }
372             }
373         },
374
375         delete: function() {
376             this.cm.replaceRange( "", { line: this.line, ch: 0 }, { line: this.line + 1, ch: 0 }, 'marcAware' );
377         },
378         focus: function() {
379             this.cm.setCursor( { line: this.line, ch: 0 } );
380
381             return this;
382         },
383
384         getText: function() {
385             var result = '';
386
387             $.each( this.getSubfields(), function() {
388                 if ( this.code != '@' ) result += '‡' + this.code;
389
390                 result += this.getText();
391             } );
392
393             return result;
394         },
395         setText: function( text ) {
396             var indicator_match = /^([_ 0-9])([_ 0-9])\‡/.exec( text );
397             if ( indicator_match ) {
398                 text = text.substr(2);
399                 this.setIndicator1( indicator_match[1] );
400                 this.setIndicator2( indicator_match[2] );
401             }
402
403             this.cm.replaceRange( text, { line: this.line, ch: this.isControlField ? 4 : 8 }, { line: this.line }, 'marcAware' );
404             this._invalidateSubfields();
405
406             return this;
407         },
408
409         getIndicator1: function() {
410             return this._ind1;
411         },
412         getIndicator2: function() {
413             return this._ind2;
414         },
415         setIndicator1: function(val) {
416             if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field');
417
418             this._ind1 = ( !val || val == ' ' ) ? '_' : val;
419             this.cm.replaceRange( this._ind1, { line: this.line, ch: 4 }, { line: this.line, ch: 5 }, 'marcAware' );
420
421             return this;
422         },
423         setIndicator2: function(val) {
424             if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field');
425
426             this._ind2 = ( !val || val == ' ' ) ? '_' : val;
427             this.cm.replaceRange( this._ind2, { line: this.line, ch: 6 }, { line: this.line, ch: 7 }, 'marcAware' );
428
429             return this;
430         },
431
432         appendSubfield: function( code ) {
433             if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field');
434
435             this._invalidateSubfields();
436             this.cm.replaceRange( '‡' + code, { line: this.line }, null, 'marcAware' );
437             var subfields = this.getSubfields();
438
439             return subfields[ subfields.length - 1 ];
440         },
441         insertSubfield: function( code, position ) {
442             if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field');
443
444             position = position || 0;
445
446             var subfields = this.getSubfields();
447             this._invalidateSubfields();
448             this.cm.replaceRange( '‡' + code, { line: this.line, ch: subfields[position] ? subfields[position].start : null }, null, 'marcAware' );
449             subfields = this.getSubfields();
450
451             return subfields[ position ];
452         },
453         getSubfields: function( code ) {
454             if ( !this._subfields ) this._scanSubfields();
455             if ( code == null ) return this._subfields;
456
457             var result = [];
458
459             $.each( this._subfields, function() {
460                 if ( code == null || this.code == code ) result.push(this);
461             } );
462
463             return result;
464         },
465         getFirstSubfield: function( code ) {
466             var result = this.getSubfields( code );
467
468             return ( result && result.length ) ? result[0] : null;
469         },
470         getSubfieldAt: function( ch ) {
471             var subfields = this.getSubfields();
472
473             for (var i = 0; i < subfields.length; i++) {
474                 if ( subfields[i].start < ch && subfields[i].end >= ch ) return subfields[i];
475             }
476         },
477     } );
478
479     function MARCEditor( options ) {
480         this.cm = CodeMirror(
481             options.position,
482             {
483                 extraKeys: _editorKeys,
484                 gutters: [
485                     'modified-line-gutter',
486                 ],
487                 lineWrapping: true,
488                 mode: {
489                     name: 'marc',
490                     nonRepeatableTags: KohaBackend.GetTagsBy( '', 'repeatable', '0' ),
491                     nonRepeatableSubfields: KohaBackend.GetSubfieldsBy( '', 'repeatable', '0' )
492                 }
493             }
494         );
495         this.cm.marceditor = this;
496
497         this.cm.on( 'beforeChange', editorBeforeChange );
498         this.cm.on( 'changes', editorChanges );
499         this.cm.on( 'cursorActivity', editorCursorActivity );
500         this.cm.on( 'overwriteToggle', editorSetOverwriteMode );
501
502         this.onCursorActivity = options.onCursorActivity;
503
504         this.subscribers = [];
505         this.subscribe( function( marceditor ) {
506             Widget.Notify( marceditor );
507         } );
508     }
509
510     MARCEditor.FieldError = FieldError;
511
512     $.extend( MARCEditor.prototype, {
513         setUseWidgets: function( val ) {
514             if ( val ) {
515                 for ( var line = 0; line <= this.cm.lastLine(); line++ ) {
516                     Widget.UpdateLine( this, line );
517                 }
518             } else {
519                 $.each( this.cm.getAllMarks(), function( undef, mark ) {
520                     if ( mark.widget ) mark.widget.clearToText();
521                 } );
522             }
523         },
524
525         focus: function() {
526             this.cm.focus();
527         },
528
529         getCursor: function() {
530             return this.cm.getCursor();
531         },
532
533         refresh: function() {
534             this.cm.refresh();
535         },
536
537         displayRecord: function( record ) {
538             this.cm.setValue( TextMARC.RecordToText(record) );
539             this.modified = false;
540         },
541
542         getRecord: function() {
543             this.textMode = true;
544
545             $.each( this.cm.getAllMarks(), function( undef, mark ) {
546                 if ( mark.widget ) mark.widget.clearToText();
547             } );
548             var record = TextMARC.TextToRecord( this.cm.getValue() );
549             for ( var line = 0; line <= this.cm.lastLine(); line++ ) {
550                 if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( this, line );
551             }
552
553             this.textMode = false;
554
555             return record;
556         },
557
558         getLineInfo: function( pos ) {
559             var contents = this.cm.getLine( pos.line );
560             if ( contents == null ) return {};
561
562             var tagNumber = contents.match( /^([A-Za-z0-9]{3})/ );
563
564             if ( !tagNumber ) return null; // No tag at all on this line
565             tagNumber = tagNumber[1];
566
567             if ( tagNumber < '010' ) return { tagNumber: tagNumber, contents: contents }; // No current subfield
568
569             var matcher = /‡([a-z0-9%])/g;
570             var match;
571
572             var subfields = [];
573             var currentSubfield;
574
575             while ( ( match = matcher.exec(contents) ) ) {
576                 subfields.push( { code: match[1], ch: match.index } );
577                 if ( match.index < pos.ch ) currentSubfield = match[1];
578             }
579
580             return { tagNumber: tagNumber, subfields: subfields, currentSubfield: currentSubfield, contents: contents };
581         },
582
583         addError: function( line, error ) {
584             var found = false;
585             var options = {};
586
587             if ( line == null ) {
588                 line = 0;
589                 options.above = true;
590             }
591
592             $.each( this.cm.getLineHandle(line).widgets || [], function( undef, widget ) {
593                 if ( !widget.isErrorMarker ) return;
594
595                 found = true;
596
597                 $( widget.node ).append( '; ' + error );
598                 widget.changed();
599
600                 return false;
601             } );
602
603             if ( found ) return;
604
605             var node = $( '<div class="structure-error"><i class="fa fa-remove"></i> ' + error + '</div>' )[0];
606             var widget = this.cm.addLineWidget( line, node, options );
607
608             widget.node = node;
609             widget.isErrorMarker = true;
610         },
611
612         removeErrors: function() {
613             for ( var line = 0; line < this.cm.lineCount(); line++ ) {
614                 $.each( this.cm.getLineHandle( line ).widgets || [], function( undef, lineWidget ) {
615                     if ( lineWidget.isErrorMarker ) lineWidget.clear();
616                 } );
617             }
618         },
619
620         startNotify: function() {
621             if ( this.notifyTimeout ) clearTimeout( this.notifyTimeout );
622             this.notifyTimeout = setTimeout( $.proxy( function() {
623                 this.notifyAll();
624
625                 this.notifyTimeout = null;
626             }, this ), NOTIFY_TIMEOUT );
627         },
628
629         notifyAll: function() {
630             $.each( this.subscribers, $.proxy( function( undef, subscriber ) {
631                 subscriber(this);
632             }, this ) );
633         },
634
635         subscribe: function( subscriber ) {
636             this.subscribers.push( subscriber );
637         },
638
639         createField: function( tag, line ) {
640             var contents = tag + ( tag < '010' ? ' ' : ' _ _ ' );
641
642             if ( line > this.cm.lastLine() ) {
643                 contents = '\n' + contents;
644             } else {
645                 contents = contents + '\n';
646             }
647
648             this.cm.replaceRange( contents, { line: line, ch: 0 }, null, 'marcAware' );
649
650             return new EditorField( this, line );
651         },
652
653         createFieldOrdered: function( tag ) {
654             var line, contents;
655
656             for ( line = 0; (contents = this.cm.getLine(line)); line++ ) {
657                 if ( contents && contents.substr(0, 3) > tag ) break;
658             }
659
660             return this.createField( tag, line );
661         },
662
663         createFieldGrouped: function( tag ) {
664             // Control fields should be inserted in actual order, whereas other fields should be
665             // inserted grouped
666             if ( tag < '010' ) return this.createFieldOrdered( tag );
667
668             var line, contents;
669
670             for ( line = 0; (contents = this.cm.getLine(line)); line++ ) {
671                 if ( contents && contents[0] > tag[0] ) break;
672             }
673
674             return this.createField( tag, line );
675         },
676
677         getFieldAt: function( line ) {
678             try {
679                 return new EditorField( this, line );
680             } catch (e) {
681                 return null;
682             }
683         },
684
685         getCurrentField: function() {
686             return this.getFieldAt( this.cm.getCursor().line );
687         },
688
689         getFields: function( tag ) {
690             var result = [];
691
692             if ( tag != null ) tag += ' ';
693
694             for ( var line = 0; line < this.cm.lineCount(); line++ ) {
695                 if ( tag && this.cm.getLine(line).substr( 0, 4 ) != tag ) continue;
696
697                 // If this throws a FieldError, pretend it doesn't exist
698                 try {
699                     result.push( new EditorField( this, line ) );
700                 } catch (e) {
701                     if ( !( e instanceof FieldError ) ) throw e;
702                 }
703             }
704
705             return result;
706         },
707
708         getFirstField: function( tag ) {
709             var result = this.getFields( tag );
710
711             return ( result && result.length ) ? result[0] : null;
712         },
713
714         getAllFields: function( tag ) {
715             return this.getFields( null );
716         },
717     } );
718
719     return MARCEditor;
720 } );