r94302 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r94301‎ | r94302 | r94303 >
Date:23:24, 11 August 2011
Author:neilk
Status:deferred
Tags:
Comment:
initial commit of skeleton remote editor JS, added preference to turn on remote editor
Modified paths:
  • /branches/extensions-realtime/WikiEditor/WikiEditor.hooks.php (modified) (history)
  • /branches/extensions-realtime/WikiEditor/WikiEditor.i18n.php (modified) (history)
  • /branches/extensions-realtime/WikiEditor/WikiEditor.php (modified) (history)
  • /branches/extensions-realtime/WikiEditor/modules/ext.wikiEditor.remote.js (added) (history)
  • /branches/extensions-realtime/WikiEditor/modules/ext.wikiEditor.remoteTabs.js (added) (history)
  • /branches/extensions-realtime/WikiEditor/modules/jquery.wikiEditor.remoteIframe.js (added) (history)

Diff [purge]

Index: branches/extensions-realtime/WikiEditor/WikiEditor.php
@@ -9,8 +9,9 @@
1010 * @author Roan Kattouw <roan.kattouw@gmail.com>
1111 * @author Nimish Gautam <nimish@wikimedia.org>
1212 * @author Adam Miller <amiller@wikimedia.org>
 13+ * @author Neil Kandalgaonkar <neilk@wikimedia.org>
1314 * @license GPL v2 or later
14 - * @version 0.3.0
 15+ * @version 0.3.1
1516 */
1617
1718 /* Configuration */
@@ -54,8 +55,8 @@
5556 $wgExtensionCredits['other'][] = array(
5657 'path' => __FILE__,
5758 'name' => 'WikiEditor',
58 - 'author' => array( 'Trevor Parscal', 'Roan Kattouw', 'Nimish Gautam', 'Adam Miller' ),
59 - 'version' => '0.3.0',
 59+ 'author' => array( 'Trevor Parscal', 'Roan Kattouw', 'Nimish Gautam', 'Adam Miller', 'Neil Kandalgaonkar' ),
 60+ 'version' => '0.3.1',
6061 'url' => 'http://www.mediawiki.org/wiki/Extension:WikiEditor',
6162 'descriptionmsg' => 'wikieditor-desc',
6263 );
Index: branches/extensions-realtime/WikiEditor/WikiEditor.hooks.php
@@ -160,8 +160,16 @@
161161
162162 /* experimental "remote" editor (such as Etherpad) */
163163 'remote' => array(
164 - 'preferences' => array(),
165 - 'requirements' => array(),
 164+ 'preferences' => array(
 165+ 'useremoteeditor' => array(
 166+ 'type' => 'toggle',
 167+ 'label-message' => 'wikieditor-remote-preference',
 168+ 'section' => 'editing/labs',
 169+ ),
 170+ ),
 171+ 'requirements' => array(
 172+ 'useremoteeditor' => true
 173+ ),
166174 'modules' => 'ext.wikiEditor.remote'
167175 )
168176 );
@@ -208,7 +216,14 @@
209217 */
210218 public static function editPageShowEditFormInitial( &$toolbar ) {
211219 global $wgOut;
212 -
 220+
 221+ if ( isset( $feature['remote'] ) && self::isEnabled( 'remote' ) ) {
 222+ $params = IdentityApi::getAuthParams();
 223+ if ( $params !== null ) {
 224+ $wgOut->addInlineScript( Skin::makeVariablesScript( $params ) );
 225+ }
 226+ }
 227+
213228 // Add modules for enabled features
214229 foreach ( self::$features as $name => $feature ) {
215230 if ( isset( $feature['modules'] ) && self::isEnabled( $name ) ) {
Index: branches/extensions-realtime/WikiEditor/modules/ext.wikiEditor.remoteTabs.js
@@ -0,0 +1,46 @@
 2+// Library to fix tabs for a remote editor
 3+// Needs to be called from within WikiEditor (or not...?)
 4+// kind of obsolete...?
 5+
 6+// @author: Neil Kandalgaonkar
 7+
 8+( function( $, mw ) {
 9+
 10+ mw.remoteEditorTabs = function() {
 11+
 12+ function addTab() {
 13+ $( '#p-views ul' ).prepend(
 14+ $( '<li></li>' ).append(
 15+ $( '<span></span>' ).append(
 16+ $( '<a></a>' )
 17+ .attr( { href: getEditUrl() } )
 18+ .html( config.name )
 19+ )
 20+ )
 21+ );
 22+ }
 23+
 24+ function modifyEditTab() {
 25+ $( 'li#ca-edit span a' ).attr( { 'href': getEditUrl() } );
 26+ }
 27+
 28+ function getEditUrl() {
 29+ return mw.config.get( 'wgServer' ) + mw.config.get( 'wgScript' ) + '?'
 30+ + $.param( {
 31+ 'title': mw.config.get( 'wgPageName' ),
 32+ 'action': 'edit',
 33+ 'editor': 'remote'
 34+ } );
 35+ }
 36+
 37+ var config = wgRemoteEditorConfig;
 38+
 39+ // deal with the tabs
 40+ if ( !config.tab || config.tab === 'modify' ) {
 41+ modifyEditTab();
 42+ } else if ( config.tab === 'add' ) {
 43+ addTab();
 44+ }
 45+ };
 46+
 47+} )( jQuery, mediaWiki );
Property changes on: branches/extensions-realtime/WikiEditor/modules/ext.wikiEditor.remoteTabs.js
___________________________________________________________________
Added: svn:eol-style
148 + native
Index: branches/extensions-realtime/WikiEditor/modules/jquery.wikiEditor.remoteIframe.js
@@ -0,0 +1,1448 @@
 2+/* remtoteIframe extension for wikiEditor */
 3+
 4+( function( $ ) { $.wikiEditor.extensions.remoteIframe = function( context ) {
 5+
 6+/*
 7+ * Event Handlers
 8+ *
 9+ * These act as filters returning false if the event should be ignored or returning true if it should be passed
 10+ * on to all modules. This is also where we can attach some extra information to the events.
 11+ */
 12+context.evt = $.extend( context.evt, {
 13+ /**
 14+ * Filters change events, which occur when the user interacts with the contents of the iframe. The goal of this
 15+ * function is to both classify the scope of changes as 'division' or 'character' and to prevent further
 16+ * processing of events which did not actually change the content of the iframe.
 17+ */
 18+ 'keydown': function( event ) {
 19+ switch ( event.which ) {
 20+ case 90: // z
 21+ case 89: // y
 22+ /*
 23+ undo/redo
 24+
 25+ if ( event.which == 89 && !$.browser.msie ) {
 26+ // only handle y events for IE
 27+ return true;
 28+ } else if ( ( event.ctrlKey || event.metaKey ) && context.history.length ) {
 29+ // HistoryPosition is a negative number between -1 and -context.history.length, in other words
 30+ // it's the number of steps backwards from the latest state.
 31+ var newPosition;
 32+ if ( event.shiftKey || event.which == 89 ) {
 33+ // Redo
 34+ newPosition = context.historyPosition + 1;
 35+ } else {
 36+ // Undo
 37+ newPosition = context.historyPosition - 1;
 38+ }
 39+ // Only act if we are switching to a valid state
 40+ if ( newPosition >= ( context.history.length * -1 ) && newPosition < 0 ) {
 41+ // Make sure we run the history storing code before we make this change
 42+ context.fn.updateHistory( context.oldDelayedHTML != context.$content.html() );
 43+ context.oldDelayedHistoryPosition = context.historyPosition;
 44+ context.historyPosition = newPosition;
 45+ // Change state
 46+ // FIXME: Destroys event handlers, will be a problem with template folding
 47+ context.$content.html(
 48+ context.history[context.history.length + context.historyPosition].html
 49+ );
 50+ context.fn.purgeOffsets();
 51+ if( context.history[context.history.length + context.historyPosition].sel ) {
 52+ context.fn.setSelection( {
 53+ start: context.history[context.history.length + context.historyPosition].sel[0],
 54+ end: context.history[context.history.length + context.historyPosition].sel[1]
 55+ } );
 56+ }
 57+ }
 58+ // Prevent the browser from jumping in and doing its stuff
 59+ return false;
 60+ }
 61+ break;
 62+ // Intercept all tab events to provide consisten behavior across browsers
 63+ // Webkit browsers insert tab characters by default into the iframe rather than changing input focus
 64+ */
 65+ case 9: //tab
 66+ /*
 67+ // if any modifier keys are pressed, allow the browser to do it's thing
 68+ if ( event.ctrlKey || event.altKey || event.shiftKey ) {
 69+ return true;
 70+ } else {
 71+ var $tabindexList = $( '[tabindex]:visible' ).sort( function( a, b ) {
 72+ return a.tabIndex - b.tabIndex;
 73+ } );
 74+ for( var i=0; i < $tabindexList.length; i++ ) {
 75+ if( $tabindexList.eq( i ).attr( 'id' ) == context.$iframe.attr( 'id' ) ) {
 76+ $tabindexList.get( i + 1 ).focus();
 77+ break;
 78+ }
 79+ }
 80+ return false;
 81+ }
 82+ break;
 83+ */
 84+ case 86: //v
 85+ /*
 86+ if ( event.ctrlKey && $.browser.msie && 'paste' in context.evt ) {
 87+ //paste, intercepted for IE
 88+ context.evt.paste( event );
 89+ }
 90+ break;
 91+ */
 92+ break;
 93+ }
 94+ return true;
 95+ },
 96+ 'change': function( event ) {
 97+ // triggered when any change has occurred
 98+ /*
 99+ event.data.scope = 'division';
 100+ var newHTML = context.$content.html();
 101+ if ( context.oldHTML != newHTML ) {
 102+ context.fn.purgeOffsets();
 103+ context.oldHTML = newHTML;
 104+ event.data.scope = 'realchange';
 105+ }
 106+ // Never let the body be totally empty
 107+ if ( context.$content.children().length == 0 ) {
 108+ context.$content.append( '<p></p>' );
 109+ }
 110+ */
 111+ return true;
 112+ },
 113+ 'delayedChange': function( event ) {
 114+ // triggered when ?
 115+ /*
 116+ event.data.scope = 'division';
 117+ var newHTML = context.$content.html();
 118+ if ( context.oldDelayedHTML != newHTML ) {
 119+ context.oldDelayedHTML = newHTML;
 120+ event.data.scope = 'realchange';
 121+ // Surround by <p> if it does not already have it
 122+ var cursorPos = context.fn.getCaretPosition();
 123+ var t = context.fn.getOffset( cursorPos[0] );
 124+ if ( ! $.browser.msie && t && t.node.nodeName == '#text' && t.node.parentNode.nodeName.toLowerCase() == 'body' ) {
 125+ $( t.node ).wrap( "<p></p>" );
 126+ context.fn.purgeOffsets();
 127+ context.fn.setSelection( { start: cursorPos[0], end: cursorPos[1] } );
 128+ }
 129+ }
 130+ context.fn.updateHistory( event.data.scope == 'realchange' );
 131+ */
 132+ return true;
 133+ },
 134+ 'cut': function( event ) {
 135+ /*
 136+ setTimeout( function() {
 137+ context.$content.find( 'br' ).each( function() {
 138+ if ( $(this).parent().is( 'body' ) ) {
 139+ $(this).wrap( $( '<p></p>' ) );
 140+ }
 141+ } );
 142+ }, 100 );
 143+ */
 144+ return true;
 145+ },
 146+ 'paste': function( event ) {
 147+ /*
 148+ // Save the cursor position to restore it after all this voodoo
 149+ var cursorPos = context.fn.getCaretPosition();
 150+ var oldLength = context.fn.getContents().length;
 151+ var positionFromEnd = oldLength - cursorPos[1];
 152+
 153+ //give everything the wikiEditor class so that we can easily pick out things without that class as pasted
 154+ context.$content.find( '*' ).addClass( 'wikiEditor' );
 155+ if ( $.layout.name !== 'webkit' ) {
 156+ context.$content.addClass( 'pasting' );
 157+ }
 158+
 159+ setTimeout( function() {
 160+ // Kill stuff we know we don't want
 161+ context.$content.find( 'script,style,img,input,select,textarea,hr,button,link,meta' ).remove();
 162+ var nodeToDelete = [];
 163+ var pastedContent = [];
 164+ var firstDirtyNode;
 165+ var $lastDirtyNode;
 166+ var elementAtCursor;
 167+ if ( $.browser.msie && !context.offsets ) {
 168+ elementAtCursor = null;
 169+ } else {
 170+ elementAtCursor = context.fn.getOffset( cursorPos[0] );
 171+ }
 172+ if ( elementAtCursor == null || elementAtCursor.node == null ) {
 173+ context.$content.prepend( '<p class = wikiEditor></p>' );
 174+ firstDirtyNode = context.$content.children()[0];
 175+ } else {
 176+ firstDirtyNode = elementAtCursor.node;
 177+ }
 178+
 179+ //this is ugly but seems like the best way to handle the case where we select and replace all editor contents
 180+ try {
 181+ firstDirtyNode.parentNode;
 182+ } catch ( err ) {
 183+ context.$content.prepend( '<p class = wikiEditor></p>' );
 184+ firstDirtyNode = context.$content.children()[0];
 185+ }
 186+
 187+ while ( firstDirtyNode != null ) {
 188+ //we're going to replace the contents of the entire parent node.
 189+ while ( firstDirtyNode.parentNode && firstDirtyNode.parentNode.nodeName != 'BODY'
 190+ && ! $( firstDirtyNode ).hasClass( 'wikiEditor' )
 191+ ) {
 192+ firstDirtyNode = firstDirtyNode.parentNode;
 193+ }
 194+ //go back till we find the first pasted node
 195+ while ( firstDirtyNode.previousSibling != null
 196+ && ! $( firstDirtyNode.previousSibling ).hasClass( 'wikiEditor' )
 197+ ) {
 198+
 199+ if ( $( firstDirtyNode.previousSibling ).hasClass( '#comment' ) ) {
 200+ $( firstDirtyNode ).remove();
 201+ } else {
 202+ firstDirtyNode = firstDirtyNode.previousSibling;
 203+ }
 204+ }
 205+
 206+ if ( firstDirtyNode.previousSibling != null ) {
 207+ $lastDirtyNode = $( firstDirtyNode.previousSibling );
 208+ } else {
 209+ $lastDirtyNode = $( firstDirtyNode );
 210+ }
 211+
 212+ var cc = makeContentCollector( $.browser, null );
 213+ while ( firstDirtyNode != null ) {
 214+ cc.collectContent(firstDirtyNode);
 215+ cc.notifyNextNode(firstDirtyNode.nextSibling);
 216+
 217+ nodeToDelete.push( firstDirtyNode );
 218+
 219+ firstDirtyNode = firstDirtyNode.nextSibling;
 220+ if ( $( firstDirtyNode ).hasClass( 'wikiEditor' ) ) {
 221+ break;
 222+ }
 223+ }
 224+
 225+ var ccData = cc.finish();
 226+ pastedContent = ccData.lines;
 227+ var pastedPretty = '';
 228+ for ( var i = 0; i < pastedContent.length; i++ ) {
 229+ //escape html
 230+ pastedPretty = pastedContent[i].replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\r?\n/g, '\\n');
 231+ //replace leading white spaces with &nbsp;
 232+ match = pastedContent[i].match(/^[\s]+[^\s]/);
 233+ if ( match != null && match.length > 0 ) {
 234+ index = match[0].length;
 235+ leadingSpace = match[0].replace(/[\s]/g, '&nbsp;');
 236+ pastedPretty = leadingSpace + pastedPretty.substring(index, pastedPretty.length);
 237+ }
 238+
 239+
 240+ if( !pastedPretty && $.browser.msie && i == 0 ) {
 241+ continue;
 242+ }
 243+ $newElement = $( '<p class="wikiEditor pasted" ></p>' );
 244+ if ( pastedPretty ) {
 245+ $newElement.html( pastedPretty );
 246+ } else {
 247+ $newElement.html( '<br class="wikiEditor">' );
 248+ }
 249+ $newElement.insertAfter( $lastDirtyNode );
 250+
 251+ $lastDirtyNode = $newElement;
 252+
 253+ }
 254+
 255+ //now delete all the original nodes that we prettified already
 256+ while ( nodeToDelete.length > 0 ) {
 257+ $deleteNode = $( nodeToDelete.pop() );
 258+ $deleteNode.remove();
 259+ }
 260+
 261+ //anything without wikiEditor class was pasted.
 262+ $selection = context.$content.find( ':not(.wikiEditor)' );
 263+ if ( $selection.length == 0 ) {
 264+ break;
 265+ } else {
 266+ firstDirtyNode = $selection.eq( 0 )[0];
 267+ }
 268+ }
 269+ context.$content.find( '.wikiEditor' ).removeClass( 'wikiEditor' );
 270+
 271+ //now place the cursor at the end of pasted content
 272+ var newLength = context.fn.getContents().length;
 273+ var newPos = newLength - positionFromEnd;
 274+
 275+ context.fn.purgeOffsets();
 276+ context.fn.setSelection( { start: newPos, end: newPos } );
 277+
 278+ context.fn.scrollToCaretPosition();
 279+ }, 0 );
 280+ */
 281+ return true;
 282+ },
 283+ 'ready': function( event ) {
 284+ /*
 285+ // Initialize our history queue
 286+ if ( context.$content ) {
 287+ context.history.push( { 'html': context.$content.html(), 'sel': context.fn.getCaretPosition() } );
 288+ } else {
 289+ context.history.push( { 'html': '', 'sel': context.fn.getCaretPosition() } );
 290+ }
 291+ return true;
 292+ */
 293+ }
 294+} );
 295+
 296+/**
 297+ * Internally used functions
 298+ */
 299+context.fn = $.extend( context.fn, {
 300+ 'highlightLine': function( $element, mode ) {
 301+ // this is used by templateEditor & toc
 302+ /*
 303+ if ( !$element.is( 'p' ) ) {
 304+ $element = $element.closest( 'p' );
 305+ }
 306+ $element.css( 'backgroundColor', '#AACCFF' );
 307+ setTimeout( function() { $element.animate( { 'backgroundColor': 'white' }, 'slow' ); }, 100 );
 308+ setTimeout( function() { $element.css( 'backgroundColor', 'white' ); }, 1000 );
 309+ */
 310+ },
 311+ 'htmlToText': function( html ) {
 312+ // This function is slow for large inputs, so aggressively cache input/output pairs
 313+ if ( html in context.htmlToTextMap ) {
 314+ return context.htmlToTextMap[html];
 315+ }
 316+ var origHTML = html;
 317+
 318+ // We use this elaborate trickery for cross-browser compatibility
 319+ // IE does overzealous whitespace collapsing for $( '<pre />' ).html( html );
 320+ // We also do <br> and easy cases for <p> conversion here, complicated cases are handled later
 321+ html = html
 322+ .replace( /\r?\n/g, "" ) // IE7 inserts newlines before block elements
 323+ .replace( /&nbsp;/g, " " ) // We inserted these to prevent IE from collapsing spaces
 324+ .replace( /\<br[^\>]*\>\<\/p\>/gi, '</p>' ) // Remove trailing <br> from <p>
 325+ .replace( /\<\/p\>\s*\<p[^\>]*\>/gi, "\n" ) // Easy case for <p> conversion
 326+ .replace( /\<br[^\>]*\>/gi, "\n" ) // <br> conversion
 327+ .replace( /\<\/p\>(\n*)\<p[^\>]*\>/gi, "$1\n" )
 328+ // Un-nest <p> tags
 329+ .replace( /\<p[^\>]*\><p[^\>]*\>/gi, '<p>' )
 330+ .replace( /\<\/p\><\/p\>/gi, '</p>' );
 331+ // Save leading and trailing whitespace now and restore it later. IE eats it all, and even Firefox
 332+ // won't leave everything alone
 333+ var leading = html.match( /^\s*/ )[0];
 334+ var trailing = html.match( /\s*$/ )[0];
 335+ html = html.substr( leading.length, html.length - leading.length - trailing.length );
 336+ var $pre = $( '<pre>' + html + '</pre>' );
 337+ $pre.find( '.wikiEditor-noinclude' ).each( function() { $( this ).remove(); } );
 338+ // Convert tabs, <p>s and <br>s back
 339+ $pre.find( '.wikiEditor-tab' ).each( function() { $( this ).text( "\t" ); } );
 340+ $pre.find( 'br' ).each( function() { $( this ).replaceWith( "\n" ); } );
 341+ // Converting <p>s is wrong if there's nothing before them, so check that.
 342+ // .find( '* + p' ) isn't good enough because textnodes aren't considered
 343+ $pre.find( 'p' ).each( function() {
 344+ var text = $( this ).text();
 345+ // If this <p> is preceded by some text, add a \n at the beginning, and if
 346+ // it's followed by a textnode, add a \n at the end
 347+ // We need the traverser because there can be other weird stuff in between
 348+
 349+ // Check for preceding text
 350+ var t = new context.fn.rawTraverser( this.firstChild, this, $pre.get( 0 ), true ).prev();
 351+ while ( t && t.node.nodeName != '#text' && t.node.nodeName != 'BR' && t.node.nodeName != 'P' ) {
 352+ t = t.prev();
 353+ }
 354+ if ( t ) {
 355+ text = "\n" + text;
 356+ }
 357+
 358+ // Check for following text
 359+ t = new context.fn.rawTraverser( this.lastChild, this, $pre.get( 0 ), true ).next();
 360+ while ( t && t.node.nodeName != '#text' && t.node.nodeName != 'BR' && t.node.nodeName != 'P' ) {
 361+ t = t.next();
 362+ }
 363+ if ( t && !t.inP && t.node.nodeName == '#text' && t.node.nodeValue.charAt( 0 ) != '\n'
 364+ && t.node.nodeValue.charAt( 0 ) != '\r' ) {
 365+ text += "\n";
 366+ }
 367+ $( this ).text( text );
 368+ } );
 369+ var retval;
 370+ if ( $.browser.msie ) {
 371+ // IE aggressively collapses whitespace in .text() after having done DOM manipulation,
 372+ // but for some crazy reason this does work. Also convert \r back to \n
 373+ retval = $( '<pre>' + $pre.html() + '</pre>' ).text().replace( /\r/g, '\n' );
 374+ } else {
 375+ retval = $pre.text();
 376+ }
 377+ return context.htmlToTextMap[origHTML] = leading + retval + trailing;
 378+ },
 379+ /**
 380+ * Get the first element before the selection that's in a certain class
 381+ * @param classname Class to match. Defaults to '', meaning any class
 382+ * @param strict If true, the element the selection starts in cannot match (default: false)
 383+ * @return jQuery object or null if unknown
 384+ */
 385+ 'beforeSelection': function( classname, strict ) {
 386+ // used by TOC
 387+ /*
 388+ if ( typeof classname == 'undefined' ) {
 389+ classname = '';
 390+ }
 391+ var e = null, offset = null;
 392+ if ( $.browser.msie && !context.$iframe[0].contentWindow.document.body ) {
 393+ return null;
 394+ }
 395+ if ( context.$iframe[0].contentWindow.getSelection ) {
 396+ // Firefox and Opera
 397+ var selection = context.$iframe[0].contentWindow.getSelection();
 398+ // On load, webkit seems to not have a valid selection
 399+ if ( selection.baseNode !== null ) {
 400+ // Start at the selection's start and traverse the DOM backwards
 401+ // This is done by traversing an element's children first, then the element itself, then its parent
 402+ e = selection.getRangeAt( 0 ).startContainer;
 403+ offset = selection.getRangeAt( 0 ).startOffset;
 404+ } else {
 405+ return null;
 406+ }
 407+
 408+ // When the cursor is on an empty line, Opera gives us a bogus range object with
 409+ // startContainer=endContainer=body and startOffset=endOffset=1
 410+ var body = context.$iframe[0].contentWindow.document.body;
 411+ if ( $.browser.opera && e == body && offset == 1 ) {
 412+ return null;
 413+ }
 414+ }
 415+ if ( !e && context.$iframe[0].contentWindow.document.selection ) {
 416+ // IE
 417+ // Because there's nothing like range.startContainer in IE, we need to do a DOM traversal
 418+ // to find the element the start of the selection is in
 419+ var range = context.$iframe[0].contentWindow.document.selection.createRange();
 420+ // Set range2 to the text before the selection
 421+ var range2 = context.$iframe[0].contentWindow.document.body.createTextRange();
 422+ // For some reason this call throws errors in certain cases, e.g. when the selection is
 423+ // not in the iframe
 424+ try {
 425+ range2.setEndPoint( 'EndToStart', range );
 426+ } catch ( ex ) {
 427+ return null;
 428+ }
 429+ var seekPos = context.fn.htmlToText( range2.htmlText ).length;
 430+ var offset = context.fn.getOffset( seekPos );
 431+ e = offset ? offset.node : null;
 432+ offset = offset ? offset.offset : null;
 433+ if ( !e ) {
 434+ return null;
 435+ }
 436+ }
 437+ if ( e.nodeName != '#text' ) {
 438+ // The selection is not in a textnode, but between two non-text nodes
 439+ // (usually inside the <body> between two <br>s). Go to the rightmost
 440+ // child of the node just before the selection
 441+ var newE = e.firstChild;
 442+ for ( var i = 0; i < offset - 1 && newE; i++ ) {
 443+ newE = newE.nextSibling;
 444+ }
 445+ while ( newE && newE.lastChild ) {
 446+ newE = newE.lastChild;
 447+ }
 448+ e = newE || e;
 449+ }
 450+
 451+ // We'd normally use if( $( e ).hasClass( class ) in the while loop, but running the jQuery
 452+ // constructor thousands of times is very inefficient
 453+ var classStr = ' ' + classname + ' ';
 454+ while ( e ) {
 455+ if ( !strict && ( !classname || ( ' ' + e.className + ' ' ).indexOf( classStr ) != -1 ) ) {
 456+ return $( e );
 457+ }
 458+ var next = e.previousSibling;
 459+ while ( next && next.lastChild ) {
 460+ next = next.lastChild;
 461+ }
 462+ e = next || e.parentNode;
 463+ strict = false;
 464+ }
 465+ return $( [] );
 466+ */
 467+ },
 468+ /**
 469+ * Object used by traverser(). Don't use this unless you know what you're doing
 470+ */
 471+ 'rawTraverser': function( node, inP, ancestor, skipNoinclude ) {
 472+ // used by highlight, and internally by various functions in wikiEditor
 473+ /*
 474+ this.node = node;
 475+ this.inP = inP;
 476+ this.ancestor = ancestor;
 477+ this.skipNoinclude = skipNoinclude;
 478+ this.next = function() {
 479+ var p = this.node;
 480+ var nextInP = this.inP;
 481+ while ( p && !p.nextSibling ) {
 482+ p = p.parentNode;
 483+ if ( p == this.ancestor ) {
 484+ // We're back at the ancestor, stop here
 485+ p = null;
 486+ }
 487+ if ( p && p.nodeName == "P" ) {
 488+ nextInP = null;
 489+ }
 490+ }
 491+ p = p ? p.nextSibling : null;
 492+ if ( p && p.nodeName == "P" ) {
 493+ nextInP = p;
 494+ }
 495+ do {
 496+ // Filter nodes with the wikiEditor-noinclude class
 497+ // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because
 498+ // $() is slow in a tight loop
 499+ if ( this.skipNoinclude ) {
 500+ while ( p && ( ' ' + p.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) {
 501+ p = p.nextSibling;
 502+ }
 503+ }
 504+ if ( p && p.firstChild ) {
 505+ p = p.firstChild;
 506+ if ( p.nodeName == "P" ) {
 507+ nextInP = p;
 508+ }
 509+ }
 510+ } while ( p && p.firstChild );
 511+ // Instead of calling the rawTraverser constructor, inline it. This avoids function call overhead
 512+ return p ? { 'node': p, 'inP': nextInP, 'ancestor': this.ancestor,
 513+ 'skipNoinclude': this.skipNoinclude, 'next': this.next, 'prev': this.prev } : null;
 514+ };
 515+ this.prev = function() {
 516+ var p = this.node;
 517+ var prevInP = this.inP;
 518+ while ( p && !p.previousSibling ) {
 519+ p = p.parentNode;
 520+ if ( p == this.ancestor ) {
 521+ // We're back at the ancestor, stop here
 522+ p = null;
 523+ }
 524+ if ( p && p.nodeName == "P" ) {
 525+ prevInP = null;
 526+ }
 527+ }
 528+ p = p ? p.previousSibling : null;
 529+ if ( p && p.nodeName == "P" ) {
 530+ prevInP = p;
 531+ }
 532+ do {
 533+ // Filter nodes with the wikiEditor-noinclude class
 534+ // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because
 535+ // $() is slow in a tight loop
 536+ if ( this.skipNoinclude ) {
 537+ while ( p && ( ' ' + p.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) {
 538+ p = p.previousSibling;
 539+ }
 540+ }
 541+ if ( p && p.lastChild ) {
 542+ p = p.lastChild;
 543+ if ( p.nodeName == "P" ) {
 544+ prevInP = p;
 545+ }
 546+ }
 547+ } while ( p && p.lastChild );
 548+ // Instead of calling the rawTraverser constructor, inline it. This avoids function call overhead
 549+ return p ? { 'node': p, 'inP': prevInP, 'ancestor': this.ancestor,
 550+ 'skipNoinclude': this.skipNoinclude, 'next': this.next, 'prev': this.prev } : null;
 551+ };
 552+ */
 553+ },
 554+ /**
 555+ * Get an object used to traverse the leaf nodes in the iframe DOM. This traversal skips leaf nodes
 556+ * inside an element with the wikiEditor-noinclude class. This basically wraps rawTraverser
 557+ *
 558+ * @param start Node to start at
 559+ * @return Traverser object, use .next() or .prev() to get a traverser object referring to the
 560+ * previous/next node
 561+ */
 562+ 'traverser': function( start ) {
 563+ /*
 564+ // Find the leftmost leaf node in the tree
 565+ var startNode = start.jquery ? start.get( 0 ) : start;
 566+ var node = startNode;
 567+ var inP = node.nodeName == "P" ? node : null;
 568+ do {
 569+ // Filter nodes with the wikiEditor-noinclude class
 570+ // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because
 571+ // $() is slow in a tight loop
 572+ while ( node && ( ' ' + node.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) {
 573+ node = node.nextSibling;
 574+ }
 575+ if ( node && node.firstChild ) {
 576+ node = node.firstChild;
 577+ if ( node.nodeName == "P" ) {
 578+ inP = node;
 579+ }
 580+ }
 581+ } while ( node && node.firstChild );
 582+ return new context.fn.rawTraverser( node, inP, startNode, true );
 583+ */
 584+ },
 585+ 'getOffset': function( offset ) {
 586+ // used by highlight
 587+ /*
 588+ if ( !context.offsets ) {
 589+ context.fn.refreshOffsets();
 590+ }
 591+ if ( offset in context.offsets ) {
 592+ return context.offsets[offset];
 593+ }
 594+ // Our offset is not pre-cached. Find the highest offset below it and interpolate
 595+ // We need to traverse the entire object because for() doesn't traverse in order
 596+ // We don't do in-order traversal because the object is sparse
 597+ var lowerBound = -1;
 598+ for ( var o in context.offsets ) {
 599+ var realO = parseInt( o );
 600+ if ( realO < offset && realO > lowerBound) {
 601+ lowerBound = realO;
 602+ }
 603+ }
 604+ if ( !( lowerBound in context.offsets ) ) {
 605+ // Weird edge case: either offset is too large or the document is empty
 606+ return null;
 607+ }
 608+ var base = context.offsets[lowerBound];
 609+ return context.offsets[offset] = {
 610+ 'node': base.node,
 611+ 'offset': base.offset + offset - lowerBound,
 612+ 'length': base.length,
 613+ 'lastTextNode': base.lastTextNode
 614+ };
 615+ */
 616+ },
 617+ 'purgeOffsets': function() {
 618+ // used by highlight, templateEditor
 619+ // context.offsets = null;
 620+ },
 621+ 'refreshOffsets': function() {
 622+ // internal
 623+ /*
 624+ context.offsets = [ ];
 625+ var t = context.fn.traverser( context.$content );
 626+ var pos = 0, lastTextNode = null;
 627+ while ( t ) {
 628+ if ( t.node.nodeName != '#text' && t.node.nodeName != 'BR' ) {
 629+ t = t.next();
 630+ continue;
 631+ }
 632+ var nextPos = t.node.nodeName == '#text' ? pos + t.node.nodeValue.length : pos + 1;
 633+ var nextT = t.next();
 634+ var leavingP = t.node.nodeName == '#text' && t.inP && nextT && ( !nextT.inP || nextT.inP != t.inP );
 635+ context.offsets[pos] = {
 636+ 'node': t.node,
 637+ 'offset': 0,
 638+ 'length': nextPos - pos + ( leavingP ? 1 : 0 ),
 639+ 'lastTextNode': lastTextNode
 640+ };
 641+ if ( leavingP ) {
 642+ // <p>Foo</p> looks like "Foo\n", make it quack like it too
 643+ // Basically we're faking the \n character much like we're treating <br>s
 644+ context.offsets[nextPos] = {
 645+ 'node': t.node,
 646+ 'offset': nextPos - pos,
 647+ 'length': nextPos - pos + 1,
 648+ 'lastTextNode': lastTextNode
 649+ };
 650+ }
 651+ pos = nextPos + ( leavingP ? 1 : 0 );
 652+ if ( t.node.nodeName == '#text' ) {
 653+ lastTextNode = t.node;
 654+ }
 655+ t = nextT;
 656+ }
 657+ */
 658+ },
 659+ 'saveCursorAndScrollTop': function() {
 660+ // Stub out textarea behavior
 661+ return;
 662+ },
 663+ 'restoreCursorAndScrollTop': function() {
 664+ // Stub out textarea behavior
 665+ return;
 666+ },
 667+ 'saveSelection': function() {
 668+ /*
 669+ if ( $.client.profile().name === 'msie' ) {
 670+ context.$iframe[0].contentWindow.focus();
 671+ context.savedSelection = context.$iframe[0].contentWindow.document.selection.createRange();
 672+ }
 673+ */
 674+ },
 675+ 'restoreSelection': function() {
 676+ /*
 677+ if ( $.client.profile().name === 'msie' && context.savedSelection !== null ) {
 678+ context.$iframe[0].contentWindow.focus();
 679+ context.savedSelection.select();
 680+ context.savedSelection = null;
 681+ }
 682+ */
 683+ },
 684+ /**
 685+ * Update the history queue
 686+ *
 687+ * @param htmlChange pass true or false to inidicate if there was a text change that should potentially
 688+ * be given a new history state.
 689+ */
 690+ 'updateHistory': function( htmlChange ) {
 691+ /*
 692+ var newHTML = context.$content.html();
 693+ var newSel = context.fn.getCaretPosition();
 694+ // Was text changed? Was it because of a REDO or UNDO action?
 695+ if (
 696+ context.history.length == 0 ||
 697+ ( htmlChange && context.oldDelayedHistoryPosition == context.historyPosition )
 698+ ) {
 699+ context.oldDelayedSel = newSel;
 700+ // Do we need to trim extras from our history?
 701+ // FIXME: this should really be happing on change, not on the delay
 702+ if ( context.historyPosition < -1 ) {
 703+ //clear out the extras
 704+ context.history.splice( context.history.length + context.historyPosition + 1 );
 705+ context.historyPosition = -1;
 706+ }
 707+ context.history.push( { 'html': newHTML, 'sel': newSel } );
 708+ // If the history has grown longer than 10 items, remove the earliest one
 709+ while ( context.history.length > 10 ) {
 710+ context.history.shift();
 711+ }
 712+ } else if ( context.oldDelayedSel != newSel ) {
 713+ // If only the selection was changed, update it
 714+ context.oldDelayedSel = newSel;
 715+ context.history[context.history.length + context.historyPosition].sel = newSel;
 716+ }
 717+ // synch our old delayed history position until the next undo/redo action
 718+ context.oldDelayedHistoryPosition = context.historyPosition;
 719+ */
 720+ },
 721+
 722+ /**
 723+ * Sets up the iframe in place of the textarea to allow more advanced operations
 724+ */
 725+ 'setupIframe': function() {
 726+ var remoteEditor = new remoteEditor( context.$textArea );
 727+ remoteEditor.launchEditor();
 728+/*
 729+ context.$iframe = $( '<iframe></iframe>' )
 730+ .attr( {
 731+ 'frameBorder': 0,
 732+ 'border': 0,
 733+ 'tabindex': 1,
 734+ 'src': mw.config.get( 'wgExtensionAssetsPath' ) + '/WikiEditor/modules/jquery.wikiEditor.html?' +
 735+ 'instance=' + context.instance + '&ts=' + ( new Date() ).getTime() + '&is=content',
 736+ 'id': 'wikiEditor-iframe-' + context.instance
 737+ } )
 738+ .css( {
 739+ 'backgroundColor': 'white',
 740+ 'width': '100%',
 741+ 'height': context.$textarea.height(),
 742+ 'display': 'none',
 743+ 'overflow-y': 'scroll',
 744+ 'overflow-x': 'hidden'
 745+ } )
 746+ .insertAfter( context.$textarea )
 747+ .load( function() {
 748+ // Internet Explorer will reload the iframe once we turn on design mode, so we need to only turn it
 749+ // on during the first run, and then bail
 750+ if ( !this.isSecondRun ) {
 751+ // Turn the document's design mode on
 752+ context.$iframe[0].contentWindow.document.designMode = 'on';
 753+ // Let the rest of this function happen next time around
 754+ if ( $.browser.msie ) {
 755+ this.isSecondRun = true;
 756+ return;
 757+ }
 758+ }
 759+ // Get a reference to the content area of the iframe
 760+ context.$content = $( context.$iframe[0].contentWindow.document.body );
 761+ // Add classes to the body to influence the styles based on what's enabled
 762+ for ( module in context.modules ) {
 763+ context.$content.addClass( 'wikiEditor-' + module );
 764+ }
 765+ // If we just do "context.$content.text( context.$textarea.val() )", Internet Explorer will strip
 766+ // out the whitespace charcters, specifically "\n" - so we must manually encode text and append it
 767+ // TODO: Refactor this into a textToHtml() function
 768+ var html = context.$textarea.val()
 769+ // We're gonna use &esc; as an escape sequence
 770+ .replace( /&esc;/g, '&esc;esc;' )
 771+ // Escape existing uses of <p>, </p>, &nbsp; and <span class="wikiEditor-tab"></span>
 772+ .replace( /\<p\>/g, '&esc;&lt;p&gt;' )
 773+ .replace( /\<\/p\>/g, '&esc;&lt;/p&gt;' )
 774+ .replace(
 775+ /\<span class="wikiEditor-tab"\>\<\/span\>/g,
 776+ '&esc;&lt;span&nbsp;class=&quot;wikiEditor-tab&quot;&gt;&lt;/span&gt;'
 777+ )
 778+ .replace( /&nbsp;/g, '&esc;&amp;nbsp;' );
 779+ // We must do some extra processing on IE to avoid dirty diffs, specifically IE will collapse
 780+ // leading spaces - browser sniffing is not ideal, but executing this code on a non-broken browser
 781+ // doesn't cause harm
 782+ if ( $.browser.msie ) {
 783+ html = html.replace( /\t/g, '<span class="wikiEditor-tab"></span>' );
 784+ if ( $.browser.versionNumber <= 7 ) {
 785+ // Replace all spaces matching &nbsp; - IE <= 7 needs this because of its overzealous
 786+ // whitespace collapsing
 787+ html = html.replace( / /g, "&nbsp;" );
 788+ } else {
 789+ // IE8 is happy if we just convert the first leading space to &nbsp;
 790+ html = html.replace( /(^|\n) /g, "$1&nbsp;" );
 791+ }
 792+ }
 793+ // Use a dummy div to escape all entities
 794+ // This'll also escape <br>, <span> and &nbsp; , so we unescape those after
 795+ // We also need to unescape the doubly-escaped things mentioned above
 796+ html = $( '<div />' ).text( '<p>' + html.replace( /\r?\n/g, '</p><p>' ) + '</p>' ).html()
 797+ .replace( /&amp;nbsp;/g, '&nbsp;' )
 798+ // Allow <p> tags to survive encoding
 799+ .replace( /&lt;p&gt;/g, '<p>' )
 800+ .replace( /&lt;\/p&gt;/g, '</p>' )
 801+ // And <span class="wikiEditor-tab"></span> too
 802+ .replace(
 803+ /&lt;span( |&nbsp;)class=("|&quot;)wikiEditor-tab("|&quot;)&gt;&lt;\/span&gt;/g,
 804+ '<span class="wikiEditor-tab"></span>'
 805+ )
 806+ // Empty <p> tags need <br> tags in them
 807+ .replace( /<p><\/p>/g, '<p><br></p>' )
 808+ // Unescape &esc; stuff
 809+ .replace( /&amp;esc;&amp;amp;nbsp;/g, '&amp;nbsp;' )
 810+ .replace( /&amp;esc;&amp;lt;p&amp;gt;/g, '&lt;p&gt;' )
 811+ .replace( /&amp;esc;&amp;lt;\/p&amp;gt;/g, '&lt;/p&gt;' )
 812+ .replace(
 813+ /&amp;esc;&amp;lt;span&amp;nbsp;class=&amp;quot;wikiEditor-tab&amp;quot;&amp;gt;&amp;lt;\/span&amp;gt;/g,
 814+ '&lt;span class="wikiEditor-tab"&gt;&lt;\/span&gt;'
 815+ )
 816+ .replace( /&amp;esc;esc;/g, '&amp;esc;' );
 817+ context.$content.html( html );
 818+
 819+ // Reflect direction of parent frame into child
 820+ if ( $( 'body' ).is( '.rtl' ) ) {
 821+ context.$content.addClass( 'rtl' ).attr( 'dir', 'rtl' );
 822+ }
 823+ // Activate the iframe, encoding the content of the textarea and copying it to the content of iframe
 824+ context.$textarea.attr( 'disabled', true );
 825+ context.$textarea.hide();
 826+ context.$iframe.show();
 827+ // Let modules know we're ready to start working with the content
 828+ context.fn.trigger( 'ready' );
 829+ // Only save HTML now: ready handlers may have modified it
 830+ context.oldHTML = context.oldDelayedHTML = context.$content.html();
 831+ //remove our temporary loading
 832+ /* Disaling our loading div for now
 833+ $( '.wikiEditor-ui-loading' ).fadeOut( 'fast', function() {
 834+ $( this ).remove();
 835+ } );
 836+ /
 837+ // Setup event handling on the iframe
 838+ $( context.$iframe[0].contentWindow.document )
 839+ .bind( 'keydown', function( event ) {
 840+ event.jQueryNode = context.fn.getElementAtCursor();
 841+ return context.fn.trigger( 'keydown', event );
 842+
 843+ } )
 844+ .bind( 'keyup', function( event ) {
 845+ event.jQueryNode = context.fn.getElementAtCursor();
 846+ return context.fn.trigger( 'keyup', event );
 847+ } )
 848+ .bind( 'keypress', function( event ) {
 849+ event.jQueryNode = context.fn.getElementAtCursor();
 850+ return context.fn.trigger( 'keypress', event );
 851+ } )
 852+ .bind( 'paste', function( event ) {
 853+ return context.fn.trigger( 'paste', event );
 854+ } )
 855+ .bind( 'cut', function( event ) {
 856+ return context.fn.trigger( 'cut', event );
 857+ } )
 858+ .bind( 'keyup paste mouseup cut encapsulateSelection', function( event ) {
 859+ return context.fn.trigger( 'change', event );
 860+ } )
 861+ .delayedBind( 250, 'keyup paste mouseup cut encapsulateSelection', function( event ) {
 862+ context.fn.trigger( 'delayedChange', event );
 863+ } );
 864+ } );
 865+ // Attach a submit handler to the form so that when the form is submitted the content of the iframe gets
 866+ // decoded and copied over to the textarea
 867+ context.$textarea.closest( 'form' ).submit( function() {
 868+ context.$textarea.attr( 'disabled', false );
 869+ context.$textarea.val( context.$textarea.textSelection( 'getContents' ) );
 870+ } );
 871+ // Attach our own handler for onbeforeunload which respects the current one
 872+ context.fallbackWindowOnBeforeUnload = window.onbeforeunload;
 873+ window.onbeforeunload = function() {
 874+ context.$textarea.val( context.$textarea.textSelection( 'getContents' ) );
 875+ if ( context.fallbackWindowOnBeforeUnload ) {
 876+ return context.fallbackWindowOnBeforeUnload();
 877+ }
 878+ };
 879+ */
 880+ // TODO add edit warning (steal it from UploadWizard...)
 881+ },
 882+
 883+ /*
 884+ * Compatibility with the $.textSelection jQuery plug-in. When the iframe is in use, these functions provide
 885+ * equivilant functionality to the otherwise textarea-based functionality.
 886+ */
 887+
 888+ 'getElementAtCursor': function() {
 889+ /*
 890+ if ( context.$iframe[0].contentWindow.getSelection ) {
 891+ // Firefox and Opera
 892+ var selection = context.$iframe[0].contentWindow.getSelection();
 893+ if ( selection.rangeCount == 0 ) {
 894+ // We don't know where the cursor is
 895+ return $( [] );
 896+ }
 897+ var sc = selection.getRangeAt( 0 ).startContainer;
 898+ if ( sc.nodeName == "#text" ) sc = sc.parentNode;
 899+ return $( sc );
 900+ } else if ( context.$iframe[0].contentWindow.document.selection ) { // should come last; Opera!
 901+ // IE
 902+ var selection = context.$iframe[0].contentWindow.document.selection.createRange();
 903+ return $( selection.parentElement() );
 904+ }
 905+ */
 906+ },
 907+
 908+ /**
 909+ * Gets the complete contents of the iframe (in plain text, not HTML)
 910+ */
 911+ 'getContents': function() {
 912+ // For <p></p>, .html() returns <p>&nbsp;</p> in IE
 913+ // This seems to convince IE while not affecting display
 914+ /*
 915+ if ( !context.$content ) {
 916+ return '';
 917+ }
 918+ var html;
 919+ if ( $.browser.msie ) {
 920+ // Don't manipulate the iframe DOM itself, causes cursor jumping issues
 921+ var $c = $( context.$content.get( 0 ).cloneNode( true ) );
 922+ $c.find( 'p' ).each( function() {
 923+ if ( $(this).html() == '' ) {
 924+ $(this).replaceWith( '<p></p>' );
 925+ }
 926+ } );
 927+ html = $c.html();
 928+ } else {
 929+ html = context.$content.html();
 930+ }
 931+ return context.fn.htmlToText( html );
 932+ */
 933+ },
 934+ /**
 935+ * Gets the currently selected text in the content
 936+ * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
 937+ */
 938+ 'getSelection': function() {
 939+ /*
 940+ var retval;
 941+ if ( context.$iframe[0].contentWindow.getSelection ) {
 942+ // Firefox and Opera
 943+ retval = context.$iframe[0].contentWindow.getSelection();
 944+ if ( $.browser.opera ) {
 945+ // Opera strips newlines in getSelection(), so we need something more sophisticated
 946+ if ( retval.rangeCount > 0 ) {
 947+ retval = context.fn.htmlToText( $( '<pre />' )
 948+ .append( retval.getRangeAt( 0 ).cloneContents() )
 949+ .html()
 950+ );
 951+ } else {
 952+ retval = '';
 953+ }
 954+ }
 955+ } else if ( context.$iframe[0].contentWindow.document.selection ) { // should come last; Opera!
 956+ // IE
 957+ retval = context.$iframe[0].contentWindow.document.selection.createRange();
 958+ }
 959+ if ( typeof retval.text != 'undefined' ) {
 960+ // In IE8, retval.text is stripped of newlines, so we need to process retval.htmlText
 961+ // to get a reliable answer. IE7 does get this right though
 962+ // Run this fix for all IE versions anyway, it doesn't hurt
 963+ retval = context.fn.htmlToText( retval.htmlText );
 964+ } else if ( typeof retval.toString != 'undefined' ) {
 965+ retval = retval.toString();
 966+ }
 967+ */
 968+ return retval;
 969+ },
 970+ /**
 971+ * Inserts text at the begining and end of a text selection, optionally inserting text at the caret when
 972+ * selection is empty.
 973+ * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
 974+ */
 975+ 'encapsulateSelection': function( options ) {
 976+ /*
 977+ var selText = $(this).textSelection( 'getSelection' );
 978+ var selTextArr;
 979+ var collapseToEnd = false;
 980+ var selectAfter = false;
 981+ var setSelectionTo = null;
 982+ var pre = options.pre, post = options.post;
 983+ if ( !selText ) {
 984+ selText = options.peri;
 985+ selectAfter = true;
 986+ } else if ( options.peri == selText.replace( /\s+$/, '' ) ) {
 987+ // Probably a successive button press
 988+ // strip any extra white space from selText
 989+ selText = selText.replace( /\s+$/, '' );
 990+ // set the collapseToEnd flag to ensure our selection is collapsed to the end before any insertion is done
 991+ collapseToEnd = true;
 992+ // set selectAfter to true since we know we'll be populating with our default text
 993+ selectAfter = true;
 994+ } else if ( options.replace ) {
 995+ selText = options.peri;
 996+ } else if ( selText.charAt( selText.length - 1 ) == ' ' ) {
 997+ // Exclude ending space char
 998+ // FIXME: Why?
 999+ selText = selText.substring( 0, selText.length - 1 );
 1000+ post += ' ';
 1001+ }
 1002+ if ( options.splitlines ) {
 1003+ selTextArr = selText.split( /\n/ );
 1004+ }
 1005+
 1006+ if ( context.$iframe[0].contentWindow.getSelection ) {
 1007+ // Firefox and Opera
 1008+ var range = context.$iframe[0].contentWindow.getSelection().getRangeAt( 0 );
 1009+ // if our test above indicated that this was a sucessive button press, we need to collapse the
 1010+ // selection to the end to avoid replacing text
 1011+ if ( collapseToEnd ) {
 1012+ // Make sure we're not collapsing ourselves into a BR tag
 1013+ if ( range.endContainer.nodeName == 'BR' ) {
 1014+ range.setEndBefore( range.endContainer );
 1015+ }
 1016+ range.collapse( false );
 1017+ }
 1018+ if ( options.ownline ) {
 1019+ // We need to figure out if the cursor is at the start or end of a line
 1020+ var atStart = false, atEnd = false;
 1021+ var body = context.$content.get( 0 );
 1022+ if ( range.startOffset == 0 ) {
 1023+ // Start of a line
 1024+ // FIXME: Not necessarily the case with syntax highlighting or
 1025+ // template collapsing
 1026+ atStart = true;
 1027+ } else if ( range.startContainer == body ) {
 1028+ // Look up the node just before the start of the selection
 1029+ // If it's a <BR>, we're at the start of a line that starts with a
 1030+ // block element; if not, we're at the end of a line
 1031+ var n = body.firstChild;
 1032+ for ( var i = 0; i < range.startOffset - 1 && n; i++ ) {
 1033+ n = n.nextSibling;
 1034+ }
 1035+ if ( n && n.nodeName == 'BR' ) {
 1036+ atStart = true;
 1037+ } else {
 1038+ atEnd = true;
 1039+ }
 1040+ }
 1041+ if ( ( range.endOffset == 0 && range.endContainer.nodeValue == null ) ||
 1042+ ( range.endContainer.nodeName == '#text' &&
 1043+ range.endOffset == range.endContainer.nodeValue.length ) ||
 1044+ ( range.endContainer.nodeName == 'P' && range.endContainer.nodeValue == null ) ) {
 1045+ atEnd = true;
 1046+ }
 1047+ if ( !atStart ) {
 1048+ pre = "\n" + options.pre;
 1049+ }
 1050+ if ( !atEnd ) {
 1051+ post += "\n";
 1052+ }
 1053+ }
 1054+ var insertText = "";
 1055+ if ( options.splitlines ) {
 1056+ for( var j = 0; j < selTextArr.length; j++ ) {
 1057+ insertText = insertText + pre + selTextArr[j] + post;
 1058+ if( j != selTextArr.length - 1 ) {
 1059+ insertText += "\n";
 1060+ }
 1061+ }
 1062+ } else {
 1063+ insertText = pre + selText + post;
 1064+ }
 1065+ var insertLines = insertText.split( "\n" );
 1066+ range.extractContents();
 1067+ // Insert the contents one line at a time - insertNode() inserts at the beginning, so this has to happen
 1068+ // in reverse order
 1069+ // Track the first and last inserted node, and if we need to also track where the text we need to select
 1070+ // afterwards starts and ends
 1071+ var firstNode = null, lastNode = null;
 1072+ var selSC = null, selEC = null, selSO = null, selEO = null, offset = 0;
 1073+ for ( var i = insertLines.length - 1; i >= 0; i-- ) {
 1074+ firstNode = context.$iframe[0].contentWindow.document.createTextNode( insertLines[i] );
 1075+ range.insertNode( firstNode );
 1076+ lastNode = lastNode || firstNode;
 1077+ var newOffset = offset + insertLines[i].length;
 1078+ if ( !selEC && post.length <= newOffset ) {
 1079+ selEC = firstNode;
 1080+ selEO = selEC.nodeValue.length - ( post.length - offset );
 1081+ }
 1082+ if ( selEC && !selSC && pre.length >= insertText.length - newOffset ) {
 1083+ selSC = firstNode;
 1084+ selSO = pre.length - ( insertText.length - newOffset );
 1085+ }
 1086+ offset = newOffset;
 1087+ if ( i > 0 ) {
 1088+ firstNode = context.$iframe[0].contentWindow.document.createElement( 'br' );
 1089+ range.insertNode( firstNode );
 1090+ newOffset = offset + 1;
 1091+ if ( !selEC && post.length <= newOffset ) {
 1092+ selEC = firstNode;
 1093+ selEO = 1 - ( post.length - offset );
 1094+ }
 1095+ if ( selEC && !selSC && pre.length >= insertText.length - newOffset ) {
 1096+ selSC = firstNode;
 1097+ selSO = pre.length - ( insertText.length - newOffset );
 1098+ }
 1099+ offset = newOffset;
 1100+ }
 1101+ }
 1102+ if ( firstNode ) {
 1103+ context.fn.scrollToTop( $( firstNode.parentNode ) );
 1104+ }
 1105+ if ( selectAfter ) {
 1106+ setSelectionTo = {
 1107+ startContainer: selSC,
 1108+ endContainer: selEC,
 1109+ start: selSO,
 1110+ end: selEO
 1111+ };
 1112+ } else if ( lastNode ) {
 1113+ setSelectionTo = {
 1114+ startContainer: lastNode,
 1115+ endContainer: lastNode,
 1116+ start: lastNode.nodeValue.length,
 1117+ end: lastNode.nodeValue.length
 1118+ };
 1119+ }
 1120+ } else if ( context.$iframe[0].contentWindow.document.selection ) {
 1121+ // IE
 1122+ context.$iframe[0].contentWindow.focus();
 1123+ var range = context.$iframe[0].contentWindow.document.selection.createRange();
 1124+ if ( options.ownline && range.moveStart ) {
 1125+ // Check if we're at the start of a line
 1126+ // If not, prepend a newline
 1127+ var range2 = context.$iframe[0].contentWindow.document.selection.createRange();
 1128+ range2.collapse();
 1129+ range2.moveStart( 'character', -1 );
 1130+ // FIXME: Which check is correct?
 1131+ if ( range2.text != "\r" && range2.text != "\n" && range2.text != "" ) {
 1132+ pre = "\n" + pre;
 1133+ }
 1134+
 1135+ // Check if we're at the end of a line
 1136+ // If not, append a newline
 1137+ var range3 = context.$iframe[0].contentWindow.document.selection.createRange();
 1138+ range3.collapse( false );
 1139+ range3.moveEnd( 'character', 1 );
 1140+ if ( range3.text != "\r" && range3.text != "\n" && range3.text != "" ) {
 1141+ post += "\n";
 1142+ }
 1143+ }
 1144+ // if our test above indicated that this was a sucessive button press, we need to collapse the
 1145+ // selection to the end to avoid replacing text
 1146+ if ( collapseToEnd ) {
 1147+ range.collapse( false );
 1148+ }
 1149+ // TODO: Clean this up. Duplicate code due to the pre-existing browser specific structure of this
 1150+ // function
 1151+ var insertText = "";
 1152+ if ( options.splitlines ) {
 1153+ for( var j = 0; j < selTextArr.length; j++ ) {
 1154+ insertText = insertText + pre + selTextArr[j] + post;
 1155+ if( j != selTextArr.length - 1 ) {
 1156+ insertText += "\n";
 1157+ }
 1158+ }
 1159+ } else {
 1160+ insertText = pre + selText + post;
 1161+ }
 1162+ // TODO: Maybe find a more elegant way of doing this like the Firefox code above?
 1163+ range.pasteHTML( insertText
 1164+ .replace( /\</g, '&lt;' )
 1165+ .replace( />/g, '&gt;' )
 1166+ .replace( /\r?\n/g, '<br />' )
 1167+ );
 1168+ if ( selectAfter ) {
 1169+ range.moveStart( 'character', -post.length - selText.length );
 1170+ range.moveEnd( 'character', -post.length );
 1171+ range.select();
 1172+ }
 1173+ }
 1174+
 1175+ if ( setSelectionTo ) {
 1176+ context.fn.setSelection( setSelectionTo );
 1177+ }
 1178+ // Trigger the encapsulateSelection event (this might need to get named something else/done differently)
 1179+ $( context.$iframe[0].contentWindow.document ).trigger(
 1180+ 'encapsulateSelection', [ pre, options.peri, post, options.ownline, options.replace ]
 1181+ );
 1182+ return context.$textarea;
 1183+ */
 1184+ },
 1185+ /**
 1186+ * Gets the position (in resolution of bytes not nessecarily characters) in a textarea
 1187+ * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
 1188+ */
 1189+ 'getCaretPosition': function( options ) {
 1190+ /*
 1191+ var startPos = null, endPos = null;
 1192+ if ( context.$iframe[0].contentWindow.getSelection ) {
 1193+ var selection = context.$iframe[0].contentWindow.getSelection();
 1194+ if ( selection.rangeCount == 0 ) {
 1195+ // We don't know where the cursor is
 1196+ return [ 0, 0 ];
 1197+ }
 1198+ var sc = selection.getRangeAt( 0 ).startContainer, ec = selection.getRangeAt( 0 ).endContainer;
 1199+ var so = selection.getRangeAt( 0 ).startOffset, eo = selection.getRangeAt( 0 ).endOffset;
 1200+ if ( sc.nodeName == 'BODY' ) {
 1201+ // Grab the node just before the start of the selection
 1202+ var n = sc.firstChild;
 1203+ for ( var i = 0; i < so - 1 && n; i++ ) {
 1204+ n = n.nextSibling;
 1205+ }
 1206+ sc = n;
 1207+ so = 0;
 1208+ }
 1209+ if ( ec.nodeName == 'BODY' ) {
 1210+ var n = ec.firstChild;
 1211+ for ( var i = 0; i < eo - 1 && n; i++ ) {
 1212+ n = n.nextSibling;
 1213+ }
 1214+ ec = n;
 1215+ eo = 0;
 1216+ }
 1217+
 1218+ // Make sure sc and ec are leaf nodes
 1219+ while ( sc.firstChild ) {
 1220+ sc = sc.firstChild;
 1221+ }
 1222+ while ( ec.firstChild ) {
 1223+ ec = ec.firstChild;
 1224+ }
 1225+ // Make sure the offsets are regenerated if necessary
 1226+ context.fn.getOffset( 0 );
 1227+ var o;
 1228+ for ( o in context.offsets ) {
 1229+ if ( startPos === null && context.offsets[o].node == sc ) {
 1230+ // For some wicked reason o is a string, even though
 1231+ // we put it in as an integer. Use ~~ to coerce it too an int
 1232+ startPos = ~~o + so - context.offsets[o].offset;
 1233+ }
 1234+ if ( startPos !== null && context.offsets[o].node == ec ) {
 1235+ endPos = ~~o + eo - context.offsets[o].offset;
 1236+ break;
 1237+ }
 1238+ }
 1239+ } else if ( context.$iframe[0].contentWindow.document.selection ) {
 1240+ // IE
 1241+ // FIXME: This is mostly copypasted from the textSelection plugin
 1242+ var d = context.$iframe[0].contentWindow.document;
 1243+ var postFinished = false;
 1244+ var periFinished = false;
 1245+ var postFinished = false;
 1246+ var preText, rawPreText, periText;
 1247+ var rawPeriText, postText, rawPostText;
 1248+ // Depending on the document state, and if the cursor has ever been manually placed within the document
 1249+ // the following call such as setEndPoint can result in nasty errors. These cases are always cases
 1250+ // in which the start and end points can safely be assumed to be 0, so we will just try our best to do
 1251+ // the full process but fall back to 0.
 1252+ try {
 1253+ // Create range containing text in the selection
 1254+ var periRange = d.selection.createRange().duplicate();
 1255+ // Create range containing text before the selection
 1256+ var preRange = d.body.createTextRange();
 1257+ // Move the end where we need it
 1258+ preRange.setEndPoint( "EndToStart", periRange );
 1259+ // Create range containing text after the selection
 1260+ var postRange = d.body.createTextRange();
 1261+ // Move the start where we need it
 1262+ postRange.setEndPoint( "StartToEnd", periRange );
 1263+ // Load the text values we need to compare
 1264+ preText = rawPreText = preRange.text;
 1265+ periText = rawPeriText = periRange.text;
 1266+ postText = rawPostText = postRange.text;
 1267+ // Check each range for trimmed newlines by shrinking the range by 1
 1268+ // character and seeing if the text property has changed. If it has
 1269+ // not changed then we know that IE has trimmed a \r\n from the end.
 1270+ do {
 1271+ if ( !postFinished ) {
 1272+ if ( preRange.compareEndPoints( "StartToEnd", preRange ) == 0 ) {
 1273+ postFinished = true;
 1274+ } else {
 1275+ preRange.moveEnd( "character", -1 );
 1276+ if ( preRange.text == preText ) {
 1277+ rawPreText += "\r\n";
 1278+ } else {
 1279+ postFinished = true;
 1280+ }
 1281+ }
 1282+ }
 1283+ if ( !periFinished ) {
 1284+ if ( periRange.compareEndPoints( "StartToEnd", periRange ) == 0 ) {
 1285+ periFinished = true;
 1286+ } else {
 1287+ periRange.moveEnd( "character", -1 );
 1288+ if ( periRange.text == periText ) {
 1289+ rawPeriText += "\r\n";
 1290+ } else {
 1291+ periFinished = true;
 1292+ }
 1293+ }
 1294+ }
 1295+ if ( !postFinished ) {
 1296+ if ( postRange.compareEndPoints("StartToEnd", postRange) == 0 ) {
 1297+ postFinished = true;
 1298+ } else {
 1299+ postRange.moveEnd( "character", -1 );
 1300+ if ( postRange.text == postText ) {
 1301+ rawPostText += "\r\n";
 1302+ } else {
 1303+ postFinished = true;
 1304+ }
 1305+ }
 1306+ }
 1307+ } while ( ( !postFinished || !periFinished || !postFinished ) );
 1308+ startPos = rawPreText.replace( /\r\n/g, "\n" ).length;
 1309+ endPos = startPos + rawPeriText.replace( /\r\n/g, "\n" ).length;
 1310+ } catch( e ) {
 1311+ startPos = endPos = 0;
 1312+ }
 1313+ }
 1314+ return [ startPos, endPos ];
 1315+ */
 1316+ },
 1317+ /**
 1318+ * Sets the selection of the content
 1319+ * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
 1320+ *
 1321+ * @param start Character offset of selection start
 1322+ * @param end Character offset of selection end
 1323+ * @param startContainer Element in iframe to start selection in. If not set, start is a character offset
 1324+ * @param endContainer Element in iframe to end selection in. If not set, end is a character offset
 1325+ */
 1326+ 'setSelection': function( options ) {
 1327+ /*
 1328+ var sc = options.startContainer, ec = options.endContainer;
 1329+ sc = sc && sc.jquery ? sc[0] : sc;
 1330+ ec = ec && ec.jquery ? ec[0] : ec;
 1331+ if ( context.$iframe[0].contentWindow.getSelection ) {
 1332+ // Firefox and Opera
 1333+ var start = options.start, end = options.end;
 1334+ if ( !sc || !ec ) {
 1335+ var s = context.fn.getOffset( start );
 1336+ var e = context.fn.getOffset( end );
 1337+ sc = s ? s.node : null;
 1338+ ec = e ? e.node : null;
 1339+ start = s ? s.offset : null;
 1340+ end = e ? e.offset : null;
 1341+ // Don't try to set the selection past the end of a node, causes errors
 1342+ // Just put the selection at the end of the node in this case
 1343+ if ( sc != null && sc.nodeName == '#text' && start > sc.nodeValue.length ) {
 1344+ start = sc.nodeValue.length - 1;
 1345+ }
 1346+ if ( ec != null && ec.nodeName == '#text' && end > ec.nodeValue.length ) {
 1347+ end = ec.nodeValue.length - 1;
 1348+ }
 1349+ }
 1350+ if ( !sc || !ec ) {
 1351+ // The requested offset isn't in the offsets array
 1352+ // Give up
 1353+ return context.$textarea;
 1354+ }
 1355+
 1356+ var sel = context.$iframe[0].contentWindow.getSelection();
 1357+ while ( sc.firstChild && sc.nodeName != '#text' ) {
 1358+ sc = sc.firstChild;
 1359+ }
 1360+ while ( ec.firstChild && ec.nodeName != '#text' ) {
 1361+ ec = ec.firstChild;
 1362+ }
 1363+ var range = context.$iframe[0].contentWindow.document.createRange();
 1364+ range.setStart( sc, start );
 1365+ range.setEnd( ec, end );
 1366+ sel.removeAllRanges();
 1367+ sel.addRange( range );
 1368+ context.$iframe[0].contentWindow.focus();
 1369+ } else if ( context.$iframe[0].contentWindow.document.body.createTextRange ) {
 1370+ // IE
 1371+ var range = context.$iframe[0].contentWindow.document.body.createTextRange();
 1372+ if ( sc ) {
 1373+ range.moveToElementText( sc );
 1374+ }
 1375+ range.collapse();
 1376+ range.moveEnd( 'character', options.start );
 1377+
 1378+ var range2 = context.$iframe[0].contentWindow.document.body.createTextRange();
 1379+ if ( ec ) {
 1380+ range2.moveToElementText( ec );
 1381+ }
 1382+ range2.collapse();
 1383+ range2.moveEnd( 'character', options.end );
 1384+
 1385+ // IE does newline emulation for <p>s: <p>foo</p><p>bar</p> becomes foo\nbar just fine
 1386+ // but <p>foo</p><br><br><p>bar</p> becomes foo\n\n\n\nbar , one \n too many
 1387+ // Correct for this
 1388+ var matches, counted = 0;
 1389+ // while ( matches = range.htmlText.match( regex ) && matches.length <= counted ) doesn't work
 1390+ // because the assignment side effect hasn't happened yet when the second term is evaluated
 1391+ while ( matches = range.htmlText.match( /\<\/p\>(\<br[^\>]*\>)+\<p\>/gi ) ) {
 1392+ if ( matches.length <= counted )
 1393+ break;
 1394+ range.moveEnd( 'character', matches.length );
 1395+ counted += matches.length;
 1396+ }
 1397+ range2.moveEnd( 'character', counted );
 1398+ while ( matches = range2.htmlText.match( /\<\/p\>(\<br[^\>]*\>)+\<p\>/gi ) ) {
 1399+ if ( matches.length <= counted )
 1400+ break;
 1401+ range2.moveEnd( 'character', matches.length );
 1402+ counted += matches.length;
 1403+ }
 1404+
 1405+ range2.setEndPoint( 'StartToEnd', range );
 1406+ range2.select();
 1407+ }
 1408+ return context.$textarea;
 1409+ },
 1410+ /**
 1411+ * Scroll a textarea to the current cursor position. You can set the cursor position with setSelection()
 1412+ * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
 1413+ */
 1414+ 'scrollToCaretPosition': function( options ) {
 1415+ // context.fn.scrollToTop( context.fn.getElementAtCursor(), true );
 1416+ },
 1417+ /**
 1418+ * Scroll an element to the top of the iframe
 1419+ * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
 1420+ *
 1421+ * @param $element jQuery object containing an element in the iframe
 1422+ * @param force If true, scroll the element even if it's already visible
 1423+ */
 1424+ 'scrollToTop': function( $element, force ) {
 1425+ /*
 1426+ var html = context.$content.closest( 'html' ),
 1427+ body = context.$content.closest( 'body' ),
 1428+ parentHtml = $( 'html' ),
 1429+ parentBody = $( 'body' );
 1430+ var y = $element.offset().top;
 1431+ if ( !$.browser.msie && ! $element.is( 'body' ) ) {
 1432+ y = parentHtml.scrollTop() > 0 ? y + html.scrollTop() - parentHtml.scrollTop() : y;
 1433+ y = parentBody.scrollTop() > 0 ? y + body.scrollTop() - parentBody.scrollTop() : y;
 1434+ }
 1435+ var topBound = html.scrollTop() > body.scrollTop() ? html.scrollTop() : body.scrollTop(),
 1436+ bottomBound = topBound + context.$iframe.height();
 1437+ if ( force || y < topBound || y > bottomBound ) {
 1438+ html.scrollTop( y );
 1439+ body.scrollTop( y );
 1440+ }
 1441+ $element.trigger( 'scrollToTop' );
 1442+ */
 1443+ }
 1444+} );
 1445+
 1446+/* Setup the IFrame */
 1447+context.fn.setupIframe();
 1448+
 1449+} } )( jQuery );
Property changes on: branches/extensions-realtime/WikiEditor/modules/jquery.wikiEditor.remoteIframe.js
___________________________________________________________________
Added: svn:eol-style
11450 + native
Index: branches/extensions-realtime/WikiEditor/modules/ext.wikiEditor.remote.js
@@ -0,0 +1,143 @@
 2+/**
 3+ * Library to create a remote editor (particularly an instance of the modified etherpad editor pad-mediawiki)
 4+ *
 5+ * dependencies: mediawiki.uri
 6+ *
 7+ * @author Neil Kandalgaonkar
 8+ * @license same terms as MediaWiki itself
 9+ *
 10+ */
 11+
 12+( function( $, mw ) {
 13+
 14+ /**
 15+ * Creates a new remote editor object
 16+ * Upgrades the form on the page to have a remote editor. See launchEditor() for most of the details on how.
 17+ *
 18+ * @param {HTMLTextAreaElement} (can be jQuery-wrapped)
 19+ */
 20+ function remoteEditor( textarea ) {
 21+ if ( typeof textarea === 'undefined' ) {
 22+ throw new Error( "need a textarea as argument to remoteEditor" );
 23+ }
 24+
 25+ if ( typeof wgRemoteEditorConfig === 'undefined' ) {
 26+ console.log( "don't have config for the remote editor" );
 27+ return;
 28+ }
 29+
 30+ // todo obtain config from...?
 31+ this.config = wgRemoteEditorConfig;
 32+
 33+ // we will listen for messages from the remote editor, located at this origin
 34+ var originUri = new mw.uri( this.config.url );
 35+ this.authorizedMessageOrigin = originUri.protocol + '://' + originUri.getAuthority();
 36+
 37+ this.$textarea = $( textarea );
 38+ this.$form = $( this.$textarea.get( 0 ).form );
 39+ this.launchEditor();
 40+ };
 41+
 42+ remoteEditor.prototype = {
 43+
 44+ /**
 45+ * Does the work of converting the form to use the remote editor.
 46+ * Creates the iframe with appropriate parameters to authenticate us to the remoteEditor.
 47+ * Hides the current textarea.
 48+ * Modify the form so that, on submit, we
 49+ * obtain wikitext from the remote editor's iframe, paste it into the textarea
 50+ * fix up the commit message with names of participants
 51+ * submit the form
 52+ */
 53+ launchEditor: function() {
 54+
 55+ console.log( "launch the editor: " + this.config.name );
 56+
 57+ // set up the iframe
 58+ var iframeName = 'remoteEditor';
 59+
 60+ this.$textarea.hide();
 61+
 62+ var form = this.$form.get( 0 );
 63+
 64+ // parameters will authenticate us to the remoteEditor.
 65+ var apiUrl = mw.config.get( 'wgServer' ) + mw.config.get( 'wgScript' ) + '/api.php';
 66+ var pageTitle = mw.config.get( 'wgPageName' );
 67+ var pageVersion = mw.config.get( 'wgCurRevisionId' );
 68+ var userName = mw.config.get( 'wgUserName' );
 69+ // TODO sending editToken is a bit of a security problem -- now the remote editor can impersonate the user.
 70+ // pass a hash instead, of userName + editToken? That can be easily calculated again... perhaps need to pass a random salt instead.
 71+ var verifyToken = mw.user.tokens.get( 'editToken' );
 72+
 73+ // create the iframe
 74+ this.$iframe = $( '<iframe></iframe>' ).attr( { 'width': '100%', 'height': '600px', 'name': iframeName } );
 75+ this.$textarea.before( this.$iframe );
 76+
 77+ // tell the iframe about the current content by posting some form data to it
 78+ // TODO these should be query params in the URL of the iframe(?)
 79+ var $postForm = $( '<form style="visibility:hidden; height:0px;"></form>' )
 80+ .attr( { 'method': 'post', 'action': this.config.url, 'target': iframeName } )
 81+ .append(
 82+ $( '<input name="userName"></input>' ).val( userName ),
 83+ $( '<input name="pageTitle"></input>' ).val( pageTitle ),
 84+ $( '<input name="pageVersion"></input>' ).val( pageVersion ),
 85+ $( '<input name="apiUrl"></input>' ).val( apiUrl ),
 86+ $( '<input name="token"></input>' ).val( verifyToken ),
 87+ $( '<textarea name="text"></textarea>' ).val( form.wpTextbox1.value )
 88+ );
 89+ this.$form.before( $postForm );
 90+ $postForm.submit();
 91+
 92+ // listen for the iframe's messages to us ( with text & a summary )
 93+ this.listenForEdit();
 94+
 95+ // when the save button here is clicked, post a message to the remote editor. We assume it will tell us what to do in listenForEdit
 96+ var _this = this;
 97+ $( "#wpSave" ).click( function() {
 98+ var message = JSON.stringify( {type: "save"} );
 99+ _this.$iframe.get( 0 ).contentWindow.postMessage( message, "*" );
 100+ return false;
 101+ } );
 102+
 103+ },
 104+
 105+ /**
 106+ * Listen for HTML5 postmessages from the remote editor
 107+ * At the moment, we are only listening for the message that we're done editing, please post data to the wiki.
 108+ */
 109+ listenForEdit: function() {
 110+ var _this = this;
 111+ /**
 112+ * Receive HTML5 postmessage from the iframe.
 113+ * (https://developer.mozilla.org/en/DOM/window.postMessage)
 114+ * The message should be the usual HTML5 postmessage, having an origin, data, and window property.
 115+ * We expect message.data to be a serialized JSON string, having a "content" and "comment" property.
 116+ * @param {postmessage}
 117+ */
 118+ function receiveMessage( message ) {
 119+ if ( message.origin !== _this.authorizedMessageOrigin ) {
 120+ console.log( 'unauthorized postmessage -- ' + message.origin );
 121+ return;
 122+ }
 123+ var data = JSON.parse( message.data );
 124+ _this.$form.get( 0 ).wpTextbox1.value = data.content;
 125+ var summary = _this.$form.find( "#wpSummary" ).val() + " - " + data.comment;
 126+ _this.$form.find( "#wpSummary" ).val( summary );
 127+ _this.$iframe.hide();
 128+ _this.$form.submit();
 129+ }
 130+ window.addEventListener( "message", receiveMessage, false );
 131+ },
 132+
 133+ };
 134+
 135+ $.fn.remoteEditor = function() {
 136+ var $elements = this;
 137+ $.each( $elements, function( i, textarea ) {
 138+ var editor = new mw.remoteEditor( textarea );
 139+ } );
 140+ };
 141+
 142+} )( jQuery, mediaWiki );
 143+
 144+
Property changes on: branches/extensions-realtime/WikiEditor/modules/ext.wikiEditor.remote.js
___________________________________________________________________
Added: svn:eol-style
1145 + native
Index: branches/extensions-realtime/WikiEditor/WikiEditor.i18n.php
@@ -36,6 +36,8 @@
3737 'wikieditor-publish-dialog-watch' => 'Watch this page',
3838 'wikieditor-publish-dialog-publish' => 'Publish',
3939 'wikieditor-publish-dialog-goback' => 'Go back',
 40+ /* Remote Editor */
 41+ 'wikieditor-remote-preference' => 'Use real-time collaboration editor',
4042 /* Template Editor */
4143 'wikieditor-template-editor-preference' => 'Enable form-based editing of wiki templates',
4244 'wikieditor-template-editor-dialog-title' => 'Edit template',

Status & tagging log