' )
.addClass( 'codeEditor-status-worker' )
.append( $errors )
.append( $warnings )
.append( $infos );
context.$statusBar = $( '
' )
.addClass( 'codeEditor-status' )
.append( $workerStatus )
.append( $message )
.append( $lineAndMode );
// Function to delay/debounce updates for the StatusBar
delayedUpdate = lang.delayedCall( function() {
updateStatusBar( editor );
}.bind( this ) );
/**
* Click handler that allows you to skip to the next annotation
*/
$workerStatus.on( 'click', function( e ) {
if ( nextAnnotation ) {
context.codeEditor.navigateTo( nextAnnotation.row, nextAnnotation.column );
// Scroll up a bit to give some context
context.codeEditor.scrollToRow( nextAnnotation.row - 3 );
e.preventDefault();
}
} );
/* Help function to concatenate strings with different separators */
function addToStatus( status, str, separator ) {
if ( str ) {
status.push( str, separator || '|' );
}
}
/**
* Update all the information in the status bar
*/
function updateStatusBar() {
var annotation,
errors = 0,
warnings = 0,
infos = 0,
distance,
shortestDistance = Infinity,
closestAnnotation,
currentLine = editor.selection.lead.row,
annotations = context.codeEditor.getSession().getAnnotations();
// Reset the next annotation
nextAnnotation = null;
for ( var i = 0; i < annotations.length; i++ ) {
annotation = annotations[i];
distance = Math.abs( currentLine - annotation.row );
if ( distance < shortestDistance ) {
shortestDistance = distance;
closestAnnotation = annotation;
}
if ( nextAnnotation === null && annotation.row > currentLine ) {
nextAnnotation = annotation;
}
switch ( annotations[i].type ) {
case 'error':
errors++;
break;
case 'warning':
warnings++;
break;
case 'info':
infos++;
break;
}
}
// Wrap around to the beginning for nextAnnotation
if ( nextAnnotation === null && annotations.length > 0 ) {
nextAnnotation = annotations[0];
}
// Update the annotation counts
if ( shouldUpdateAnnotations ) {
$errors.text( errors );
$warnings.text( warnings );
$infos.text( infos );
}
// Show the message of the current line, if we have not already done so
if ( closestAnnotation &&
currentLine === closestAnnotation.row &&
closestAnnotation !== $message.data( 'annotation') ) {
$message.data( 'annotation', closestAnnotation );
$message.text( $.ucFirst( closestAnnotation.type ) + ': ' + closestAnnotation.text );
} else if ( $message.data( 'annotation' ) !== null &&
( !closestAnnotation || currentLine !== closestAnnotation.row ) ) {
// If we are on a different line without an annotation, then blank the message
$message.data( 'annotation', null );
$message.text( '' );
}
// The cursor position has changed
if ( shouldUpdateSelection || shouldUpdateLineInfo ) {
// Adapted from Ajax.org's ace/ext/statusbar module
var status = [];
if ( editor.$vimModeHandler ) {
addToStatus( status, editor.$vimModeHandler.getStatusText() );
} else if ( editor.commands.recording ) {
addToStatus( status, 'REC' );
}
var c = editor.selection.lead;
addToStatus( status, ( c.row + 1 ) + ':' + c.column, '');
if ( !editor.selection.isEmpty() ) {
var r = editor.getSelectionRange();
addToStatus( status, '(' + ( r.end.row - r.start.row ) + ':' + ( r.end.column - r.start.column ) + ')' );
}
status.pop();
$lineAndMode.text( status.join('') );
}
shouldUpdateLineInfo = shouldUpdateSelection = shouldUpdateAnnotations = false;
}
editor.getSession().on( 'changeAnnotation', function() {
shouldUpdateAnnotations = true;
delayedUpdate.schedule( 100 );
} );
editor.on( 'changeStatus', function() {
shouldUpdateLineInfo = true;
delayedUpdate.schedule( 100 );
} );
editor.on( 'changeSelection', function() {
shouldUpdateSelection = true;
delayedUpdate.schedule( 100 );
} );
// Force update
shouldUpdateLineInfo = shouldUpdateSelection = shouldUpdateAnnotations = true;
updateStatusBar( editor );
context.$statusBar.insertAfter( $( '.wikiEditor-ui-view-wikitext .wikiEditor-ui-bottom' ) );
},
'removeStatusBar': function () {
context.codeEditor.getSession().removeListener( 'changeAnnotation' );
context.codeEditor.removeListener( 'changeSelection' );
context.codeEditor.removeListener( 'changeStatus' );
context.nextAnnotation = null;
context.$statusBar = null;
$( '.codeEditor-status' ).remove();
}
} );
/**
* Override the base functions in a way that lets
* us fall back to the originals when we turn off.
*/
saveAndExtend = function ( base, extended ) {
var saved, map;
saved = {};
// $.map doesn't handle objects in jQuery < 1.6; need this for compat with MW 1.17
map = function ( obj, callback ) {
var key;
for ( key in extended ) {
if ( obj.hasOwnProperty( key ) ) {
callback( obj[key], key );
}
}
};
map( extended, function ( func, name ) {
if ( name in base ) {
var orig = base[name];
base[name] = function () {
if ( context.codeEditorActive ) {
return func.apply( this, arguments );
} else if ( orig ) {
return orig.apply( this, arguments );
} else {
throw new Error( 'CodeEditor: no original function to call for ' + name );
}
};
} else {
base[name] = func;
}
} );
};
saveAndExtend( context.fn, {
'saveCursorAndScrollTop': function () {
// Stub out textarea behavior
return;
},
'restoreCursorAndScrollTop': function () {
// Stub out textarea behavior
return;
},
'saveSelection': function () {
mw.log( 'codeEditor stub function saveSelection called' );
},
'restoreSelection': function () {
mw.log( 'codeEditor stub function restoreSelection called' );
},
/* Needed for search/replace */
'getContents': function () {
return context.codeEditor.getSession().getValue();
},
/**
* Compatibility with the $.textSelection jQuery plug-in. When the iframe is in use, these functions provide
* equivilant functionality to the otherwise textarea-based functionality.
*/
'getElementAtCursor': function () {
mw.log( 'codeEditor stub function getElementAtCursor called' );
},
/**
* Gets the currently selected text in the content
* DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
*/
'getSelection': function () {
return context.codeEditor.getCopyText();
},
/**
* Inserts text at the begining and end of a text selection, optionally inserting text at the caret when
* selection is empty.
* DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
*/
'encapsulateSelection': function ( options ) {
var sel, range, selText, isSample, text;
// Does not yet handle 'ownline', 'splitlines' option
sel = context.codeEditor.getSelection();
range = sel.getRange();
selText = context.fn.getSelection();
isSample = false;
if ( !selText ) {
selText = options.peri;
isSample = true;
} else if ( options.replace ) {
selText = options.peri;
}
text = options.pre;
text += selText;
text += options.post;
context.codeEditor.insert( text );
if ( isSample && options.selectPeri && !options.splitlines ) {
// May esplode if anything has newlines, be warned. :)
range.setStart( range.start.row, range.start.column + options.pre.length );
range.setEnd( range.start.row, range.start.column + selText.length );
sel.setSelectionRange( range );
}
return context.$textarea;
},
/**
* Gets the position (in resolution of bytes not nessecarily characters) in a textarea
* DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
*/
'getCaretPosition': function ( ) {
mw.log( 'codeEditor stub function getCaretPosition called' );
},
/**
* Sets the selection of the content
* DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
*
* @param start Character offset of selection start
* @param end Character offset of selection end
* @param startContainer Element in iframe to start selection in. If not set, start is a character offset
* @param endContainer Element in iframe to end selection in. If not set, end is a character offset
*/
'setSelection': function ( options ) {
var doc, lines, offsetToPos, start, end, sel, range;
// Ace stores positions for ranges as row/column pairs.
// To convert from character offsets, we'll need to iterate through the document
doc = context.codeEditor.getSession().getDocument();
lines = doc.getAllLines();
offsetToPos = function ( offset ) {
var row, col, pos;
row = 0;
col = 0;
pos = 0;
while ( row < lines.length && pos + lines[row].length < offset ) {
pos += lines[row].length;
pos++; // for the newline
row++;
}
col = offset - pos;
return {row: row, column: col};
};
start = offsetToPos( options.start );
end = offsetToPos( options.end );
sel = context.codeEditor.getSelection();
range = sel.getRange();
range.setStart( start.row, start.column );
range.setEnd( end.row, end.column );
sel.setSelectionRange( range );
return context.$textarea;
},
/**
* Scroll a textarea to the current cursor position. You can set the cursor position with setSelection()
* DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
*/
'scrollToCaretPosition': function ( ) {
mw.log( 'codeEditor stub function scrollToCaretPosition called' );
return context.$textarea;
},
/**
* Scroll an element to the top of the iframe
* DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
*
* @param $element jQuery object containing an element in the iframe
* @param force If true, scroll the element even if it's already visible
*/
'scrollToTop': function () {
mw.log( 'codeEditor stub function scrollToTop called' );
}
} );
/* Setup the editor */
context.fn.setupCodeEditorToolbar();
if ( context.codeEditorActive ) {
context.fn.setupCodeEditor();
}
};
})( jQuery, mediaWiki );