2 * Copyright 2015 ByWater Solutions
4 * This file is part of Koha.
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.
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.
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>.
20 define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], function( MARC, KohaBackend, Preferences, TextMARC, Widget ) {
22 var NOTIFY_TIMEOUT = 250;
24 function editorCursorActivity( cm ) {
25 var editor = cm.marceditor;
26 var field = editor.getCurrentField();
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);
33 cm.toggleOverwrite(false);
36 editor.onCursorActivity();
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
42 function editorBeforeChange( cm, change ) {
43 var editor = cm.marceditor;
44 if ( editor.textMode || change.origin == 'marcAware' || change.origin == 'widget.clearToText' ) return;
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
49 if ( change.from.ch == change.to.ch - 1 && cm.findMarksAt( { line: change.from.line, ch: change.from.ch + 1 } ).length ) {
51 } else if ( change.from.ch == change.to.ch && cm.findMarksAt(change.from).length && !change.text[0] == '‡' ) {
56 function editorChanges( cm, changes ) {
57 var editor = cm.marceditor;
58 if ( editor.textMode ) return;
60 for (var i = 0; i < changes.length; i++) {
61 var change = changes[i];
63 var origin = change.from.line;
64 var newTo = CodeMirror.changeEnd(change);
66 for (var delLine = origin; delLine <= change.to.line; delLine++) {
67 // Line deleted; currently nothing to do
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;
79 Widget.ActivateAt( cm, cm.getCursor() );
80 cm.marceditor.startNotify();
83 function editorSetOverwriteMode( cm, newState ) {
84 var editor = cm.marceditor;
86 editor.overwriteMode = newState;
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;
95 while ( lenDiff-- > 0 ) extra += ' ';
96 if ( pos.prefill ) extra += pos.prefill;
97 cm.replaceRange( extra, { line: pos.line } );
101 Widget.ActivateAt( cm, pos, idx );
104 function getTabPositions( editor, cur ) {
105 cur = cur || editor.cm.getCursor();
106 var field = editor.getFieldAt( cur.line );
109 if ( field.isControlField ) {
110 var positions = [ { ch: 0 }, { ch: 4 } ];
112 $.each( positions, function( undef, pos ) {
118 var positions = [ { ch: 0 }, { ch: 4, prefill: '_' }, { ch: 6, prefill: '_' } ];
120 $.each( positions, function( undef, pos ) {
123 $.each( field.getSubfields(), function( undef, subfield ) {
124 positions.push( { line: cur.line, ch: subfield.contentsStart } );
127 // Allow to tab to start of empty field
128 if ( field.getSubfields().length == 0 ) {
129 positions.push( { line: cur.line, ch: 8 } );
138 var _editorKeys = {};
140 _editorKeys[insert_copyright] = function( cm ) {
141 cm.replaceRange( '©', cm.getCursor() );
144 _editorKeys[insert_copyright_sound] = function( cm ) {
145 cm.replaceRange( '℗', cm.getCursor() );
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 } );
154 _editorKeys[line_break] = function( cm ) {
155 var cur = cm.getCursor();
157 cm.replaceRange( "\n", cur, null );
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 );
165 $("#clipboard").prepend('<option>'+curLine+'</option>');
166 $("#clipboard option:first-child").prop('selected', true);
168 cm.execCommand('deleteLine');
171 _editorKeys[link_authorities] = function( cm ) {
172 // Launch the auth search popup
173 var field = cm.marceditor.getCurrentField();
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;
184 var subfields = field.getSubfields();
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+' ';
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');
195 _editorKeys[delete_subfield] = function( cm ) {
197 var field = cm.marceditor.getCurrentField();
198 if ( !field ) return;
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});
204 $("#clipboard").prepend('<option>'+subfieldText+'</option>');
205 $("#clipboard option:first-child").prop('selected', true);
210 _editorKeys[copy_line] = function( cm ) {
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);
218 _editorKeys[copy_subfield] = function( cm ) {
220 var field = cm.marceditor.getCurrentField();
221 if ( !field ) return;
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});
227 $("#clipboard").prepend('<option>'+subfieldText+'</option>');
228 $("#clipboard option:first-child").prop('selected', true);
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 );
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 );
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();
253 for ( var i = 0; i < positions.length; i++ ) {
254 if ( positions[i].ch > cur.ch ) {
255 activateTabPosition( cm, positions[i] );
260 cm.setCursor( { line: cur.line + 1, ch: 0 } );
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();
268 for ( var i = positions.length - 1; i >= 0; i-- ) {
269 if ( positions[i].ch < cur.ch ) {
270 activateTabPosition( cm, positions[i], -1 );
275 if ( cur.line == 0 ) return;
277 var prevPositions = getTabPositions( cm.marceditor, { line: cur.line - 1, ch: cm.getLine( cur.line - 1 ).length } );
279 if ( prevPositions.length ) {
280 activateTabPosition( cm, prevPositions[ prevPositions.length - 1 ], -1 );
282 cm.setCursor( { line: cur.line - 1, ch: 0 } );
286 _editorKeys[insert_delimiter] = function(cm){
287 var cur = cm.getCursor();
289 cm.replaceRange( "‡", cur, null );
292 _editorKeys[toggle_keyboard] = function( cm ) {
293 let keyboard = $(cm.getInputField()).getkeyboard();
294 keyboard.isVisible()?keyboard.close():keyboard.reveal();
297 // The objects below are part of a field/subfield manipulation API, accessed through the base
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.
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.
308 // This is an exception raised by the EditorSubfield and EditorField when an invalid change is
310 function FieldError(line, message) {
312 this.message = message;
315 FieldError.prototype.toString = function() {
316 return 'FieldError(' + this.line + ', "' + this.message + '")';
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 ) {
327 if ( this.field.isControlField ) {
328 this.contentsStart = start;
331 this.contentsStart = start + 2;
332 this.code = this.field.contents.substr( this.start + 1, 1 );
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;
341 this.text = this.widget.text;
342 this.setText = this.widget.setText;
343 this.getFixed = this.widget.getFixed;
344 this.setFixed = this.widget.setFixed;
347 this.text = this.field.contents.substr( this.contentsStart, end - this.contentsStart );
351 $.extend( EditorSubfield.prototype, {
352 _invalid: function() {
353 return this.field._subfieldsInvalid();
357 this.cm.replaceRange( "", { line: this.field.line, ch: this.start }, { line: this.field.line, ch: this.end }, 'marcAware' );
360 this.cm.setCursor( { line: this.field.line, ch: this.contentsStart } );
362 focusEnd: function() {
363 this.cm.setCursor( { line: this.field.line, ch: this.end } );
365 getText: function() {
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();
375 function EditorField( editor, line ) {
376 this.editor = editor;
382 this.tag = this.contents.substr( 0, 3 );
383 this.isControlField = ( this.tag < '010' );
385 if ( this.isControlField ) {
386 this._ind1 = this.contents.substr( 4, 1 );
387 this._ind2 = this.contents.substr( 6, 1 );
393 this.subfields = null;
396 $.extend( EditorField.prototype, {
397 _subfieldsInvalid: function() {
398 return !this.subfields;
400 _invalidateSubfields: function() {
401 this._subfields = null;
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;
409 _scanSubfields: function() {
412 if ( this.isControlField ) {
413 this._subfields = [ new EditorSubfield( this, 0, 4, this.contents.length ) ];
416 var subfields = this.info.subfields;
417 this._subfields = [];
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;
422 this._subfields.push( new EditorSubfield( this, i, subfields[i].ch, end ) );
428 this.cm.replaceRange( "", { line: this.line, ch: 0 }, { line: this.line + 1, ch: 0 }, 'marcAware' );
431 this.cm.setCursor( { line: this.line, ch: 0 } );
436 getText: function() {
439 $.each( this.getSubfields(), function() {
440 if ( this.code != '@' ) result += '‡' + this.code;
442 result += this.getText();
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] );
455 this.cm.replaceRange( text, { line: this.line, ch: this.isControlField ? 4 : 8 }, { line: this.line }, 'marcAware' );
456 this._invalidateSubfields();
461 getIndicator1: function() {
464 getIndicator2: function() {
467 setIndicator1: function(val) {
468 if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field');
470 this._ind1 = ( !val || val == ' ' ) ? '_' : val;
471 this.cm.replaceRange( this._ind1, { line: this.line, ch: 4 }, { line: this.line, ch: 5 }, 'marcAware' );
475 setIndicator2: function(val) {
476 if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field');
478 this._ind2 = ( !val || val == ' ' ) ? '_' : val;
479 this.cm.replaceRange( this._ind2, { line: this.line, ch: 6 }, { line: this.line, ch: 7 }, 'marcAware' );
484 appendSubfield: function( code ) {
485 if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field');
487 this._invalidateSubfields();
488 this.cm.replaceRange( '‡' + code, { line: this.line }, null, 'marcAware' );
489 var subfields = this.getSubfields();
491 return subfields[ subfields.length - 1 ];
493 insertSubfield: function( code, position ) {
494 if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field');
496 position = position || 0;
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();
503 return subfields[ position ];
505 getSubfields: function( code ) {
506 if ( !this._subfields ) this._scanSubfields();
507 if ( code == null ) return this._subfields;
511 $.each( this._subfields, function() {
512 if ( code == null || this.code == code ) result.push(this);
517 getFirstSubfield: function( code ) {
518 var result = this.getSubfields( code );
520 return ( result && result.length ) ? result[0] : null;
522 getSubfieldAt: function( ch ) {
523 var subfields = this.getSubfields();
525 for (var i = 0; i < subfields.length; i++) {
526 if ( subfields[i].start < ch && subfields[i].end >= ch ) return subfields[i];
531 function MARCEditor( options ) {
532 this.frameworkcode = '';
534 this.cm = CodeMirror(
537 extraKeys: _editorKeys,
539 'modified-line-gutter',
544 nonRepeatableTags: KohaBackend.GetTagsBy( '', 'repeatable', '0' ),
545 nonRepeatableSubfields: KohaBackend.GetSubfieldsBy( '', 'repeatable', '0' )
549 var inf = this.cm.getInputField();
551 var kb = $(inf).keyboard({
552 //keyBinding: "mousedown touchstart",
556 autoAcceptOnEsc: true,
561 of: $("#statusbar"), // optional - null (attach to input/textarea) or a jQuery object (attach elsewhere)
564 at2: 'center bottom' // used when "usePreview" is false (centers keyboard at bottom of the input/textarea)
566 beforeInsert: function(evnt, keyboard, elem, txt) {
567 var position = self.cm.getCursor();
569 self.cm.execCommand("delCharBefore");
571 if (txt === "\b" && position.ch === 0 && position.line !== 0) {
572 elem.value = self.cm.getLine(position.line) || "";
577 visible: function() {
578 $('#set-keyboard-layout').removeClass('hide');
580 hidden: function(e, keyboard, el, accepted) {
582 $('#set-keyboard-layout').addClass('hide');
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');
593 $('#keyboard-layout')
594 .on('show.bs.modal', function() {
596 $('#keyboard-layout .filter').focus();
597 $('#set-keyboard-layout').removeClass('hide');
599 .on('hide.bs.modal', function() {
600 !kb.isVisible() && kb.reveal();
602 $('#keyboard-layout .layout').click(function(event) {
603 $('#keyboard-layout .layout').removeClass('active');
604 $(this).addClass('active');
605 var layout = $(this).data().layout;
607 $('#keyboard-layout').modal('hide');
608 $('#keyboard-layout .filter').val('');
609 $('#keyboard-layout .layout').show();
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();
622 this.cm.marceditor = this;
624 this.cm.on( 'beforeChange', editorBeforeChange );
625 this.cm.on( 'changes', editorChanges );
626 this.cm.on( 'cursorActivity', editorCursorActivity );
627 this.cm.on( 'overwriteToggle', editorSetOverwriteMode );
629 this.onCursorActivity = options.onCursorActivity;
631 this.subscribers = [];
632 this.subscribe( function( marceditor ) {
633 Widget.Notify( marceditor );
637 MARCEditor.FieldError = FieldError;
639 $.extend( MARCEditor.prototype, {
640 setUseWidgets: function( val ) {
642 for ( var line = 0; line <= this.cm.lastLine(); line++ ) {
643 Widget.UpdateLine( this, line );
646 $.each( this.cm.getAllMarks(), function( undef, mark ) {
647 if ( mark.widget ) mark.widget.clearToText();
656 getCursor: function() {
657 return this.cm.getCursor();
660 refresh: function() {
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 ');
671 KohaBackend.InitFramework( code, function ( error ) {
672 cm.setOption( 'mode', {
674 nonRepeatableTags: KohaBackend.GetTagsBy( code, 'repeatable', '0' ),
675 nonRepeatableSubfields: KohaBackend.GetSubfieldsBy( code, 'repeatable', '0' )
677 if ( updateFields ) {
678 var record = TextMARC.TextToRecord( cm.getValue() );
679 KohaBackend.FillRecord( code, record );
680 cm.setValue( TextMARC.RecordToText(record) );
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 : '',
693 if ( typeof error !== 'undefined' ) {
694 humanMsg.displayAlert( _(error), { className: 'humanError' } );
700 getRecord: function() {
701 this.textMode = true;
703 $.each( this.cm.getAllMarks(), function( undef, mark ) {
704 if ( mark.widget ) mark.widget.clearToText();
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 );
711 this.textMode = false;
713 record.frameworkcode = this.frameworkcode;
717 getLineInfo: function( pos ) {
718 var contents = this.cm.getLine( pos.line );
719 if ( contents == null ) return {};
721 var tagNumber = contents.match( /^([A-Za-z0-9]{3})/ );
723 if ( !tagNumber ) return null; // No tag at all on this line
724 tagNumber = tagNumber[1];
726 if ( tagNumber < '010' ) return { tagNumber: tagNumber, contents: contents }; // No current subfield
728 var matcher = /‡([a-z0-9%])/g;
734 while ( ( match = matcher.exec(contents) ) ) {
735 subfields.push( { code: match[1], ch: match.index } );
736 if ( match.index < pos.ch ) currentSubfield = match[1];
739 return { tagNumber: tagNumber, subfields: subfields, currentSubfield: currentSubfield, contents: contents };
742 addError: function( line, error ) {
746 if ( line == null ) {
748 options.above = true;
751 $.each( this.cm.getLineHandle(line).widgets || [], function( undef, widget ) {
752 if ( !widget.isErrorMarker ) return;
756 $( widget.node ).append( '; ' + error );
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 );
768 widget.isErrorMarker = true;
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();
779 startNotify: function() {
780 if ( this.notifyTimeout ) clearTimeout( this.notifyTimeout );
781 this.notifyTimeout = setTimeout( $.proxy( function() {
784 this.notifyTimeout = null;
785 }, this ), NOTIFY_TIMEOUT );
788 notifyAll: function() {
789 $.each( this.subscribers, $.proxy( function( undef, subscriber ) {
794 subscribe: function( subscriber ) {
795 this.subscribers.push( subscriber );
798 createField: function( tag, line ) {
799 var contents = tag + ( tag < '010' ? ' ' : ' _ _ ' );
801 if ( line > this.cm.lastLine() ) {
802 contents = '\n' + contents;
804 contents = contents + '\n';
807 this.cm.replaceRange( contents, { line: line, ch: 0 }, null, 'marcAware' );
809 return new EditorField( this, line );
812 createFieldOrdered: function( tag ) {
815 for ( line = 0; (contents = this.cm.getLine(line)); line++ ) {
816 if ( contents && contents.substr(0, 3) > tag ) break;
819 return this.createField( tag, line );
822 createFieldGrouped: function( tag ) {
823 // Control fields should be inserted in actual order, whereas other fields should be
825 if ( tag < '010' ) return this.createFieldOrdered( tag );
829 for ( line = 0; (contents = this.cm.getLine(line)); line++ ) {
830 if ( contents && contents[0] > tag[0] ) break;
833 return this.createField( tag, line );
836 getFieldAt: function( line ) {
838 return new EditorField( this, line );
844 getCurrentField: function() {
845 return this.getFieldAt( this.cm.getCursor().line );
848 getFields: function( tag ) {
851 if ( tag != null ) tag += ' ';
853 for ( var line = 0; line < this.cm.lineCount(); line++ ) {
854 if ( tag && this.cm.getLine(line).substr( 0, 4 ) != tag ) continue;
856 // If this throws a FieldError, pretend it doesn't exist
858 result.push( new EditorField( this, line ) );
860 if ( !( e instanceof FieldError ) ) throw e;
867 getFirstField: function( tag ) {
868 var result = this.getFields( tag );
870 return ( result && result.length ) ? result[0] : null;
873 getAllFields: function( tag ) {
874 return this.getFields( null );