/* Ace syntax-highlighting code editor extension for wikiEditor */ ( function( $ ) { $.wikiEditor.modules.codeEditor = { /** * Core Requirements */ 'req': [ 'codeEditor' ], /** * Configuration */ cfg: { // }, /** * API accessible functions */ api: { // }, /** * Event handlers */ evt: { // }, /** * Internally used functions */ fn: { } }; $.wikiEditor.extensions.codeEditor = function( context ) { /* * Event Handlers * * These act as filters returning false if the event should be ignored or returning true if it should be passed * on to all modules. This is also where we can attach some extra information to the events. */ context.evt = $.extend( context.evt, { /** * Filters change events, which occur when the user interacts with the contents of the iframe. The goal of this * function is to both classify the scope of changes as 'division' or 'character' and to prevent further * processing of events which did not actually change the content of the iframe. */ 'keydown': function( event ) { }, 'change': function( event ) { }, 'delayedChange': function( event ) { }, 'cut': function( event ) { }, 'paste': function( event ) { }, 'ready': function( event ) { } } ); /** * Internally used functions */ context.fn = $.extend( 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'); }, /** * Sets up the iframe in place of the textarea to allow more advanced operations */ 'setupCodeEditor': function() { var box = context.$textarea; var matches = /\.(js|css)$/.exec(wgTitle); if (matches && (wgNamespaceNumber == 2 /* User: */ || wgNamespaceNumber == 8 /* MediaWiki: */)) { var ext = matches[1]; var map = {js: 'javascript', css: 'css'}; var lang = map[ext]; // Ace doesn't like replacing a textarea directly. // We'll stub this out to sit on top of it... // line-height is needed to compensate for oddity in WikiEditor extension, which zeroes the line-height on a parent container var container = $('
').insertAfter(box); var editdiv = container.find('.editor'); box.css('display', 'none'); container.width(box.width()) .height(box.height()); editdiv.text(box.val()); context.codeEditor = ace.edit(editdiv[0]); // fakeout for bug 29328 context.$iframe = [ { contentWindow: { focus: function() { context.codeEditor.focus(); } } } ]; box.closest('form').submit(function(event) { box.val(context.fn.getContents()); }); context.codeEditor.getSession().setMode(new (require("ace/mode/" + lang).Mode)); // Force the box to resize horizontally to match in future :D var resize = function() { container.width(box.width()); }; $(window).resize(resize); // Use jquery.ui.resizable so user can make the box taller too container.resizable({ handles: 's', minHeight: box.height(), resize: function() { context.codeEditor.resize(); } }); var summary = $('#wpSummary'); if (summary.val() == '') { summary.val('/* using [[mw:CodeEditor|CodeEditor]] */ '); } // Let modules know we're ready to start working with the content context.fn.trigger( 'ready' ); } }, /* 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 ) { // Does not yet handle 'ownline', 'splitlines' option var sel = context.codeEditor.getSelection(); var range = sel.getRange(); var selText = context.fn.getSelection(); var isSample = false; if ( !selText ) { selText = options.peri; isSample = true; } else if ( options.replace ) { selText = options.peri; } var 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( options ) { 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 ) { // Ace stores positions for ranges as row/column pairs. // To convert from character offsets, we'll need to iterate through the document var doc = context.codeEditor.getSession().getDocument(); var lines = doc.getAllLines(); var offsetToPos = function( offset ) { var row = 0, col = 0; var 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}; } var start = offsetToPos( options.start ), end = offsetToPos( options.end ); var sel = context.codeEditor.getSelection(); var 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( options ) { 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( $element, force ) { mw.log('codeEditor stub function scrollToTop called'); } } ); /* Setup the editor */ context.fn.setupCodeEditor(); } } )( jQuery );