From 0a71513db40b0a516d17f8e136f60bb0df5b7dba Mon Sep 17 00:00:00 2001 From: Trevor Parscal Date: Mon, 4 Oct 2010 20:12:30 +0000 Subject: [PATCH] * Introduced the concept of context extensions * Abstracted the requirements concept being used to hack in the iframe as a context extension requirement * Removed some hardcoding for iframe stuff * Moved about 60k of JavaScript code out of the main wikiEditor script, and into an iframe only context extension. --- WikiEditor.hooks.php | 19 +- modules/jquery.wikiEditor.iframe.js | 1317 ++++++++++++++++++++++++++ modules/jquery.wikiEditor.js | 1327 +-------------------------- 3 files changed, 1355 insertions(+), 1308 deletions(-) create mode 100644 modules/jquery.wikiEditor.iframe.js diff --git a/WikiEditor.hooks.php b/WikiEditor.hooks.php index a279c653..94b59998 100644 --- a/WikiEditor.hooks.php +++ b/WikiEditor.hooks.php @@ -36,6 +36,11 @@ class WikiEditorHooks { ), 'group' => 'ext.wikiEditor', ), + 'jquery.wikiEditor.iframe' => array( + 'scripts' => 'extensions/WikiEditor/modules/jquery.wikiEditor.iframe.js', + 'dependencies' => 'jquery.wikiEditor', + 'group' => 'ext.wikiEditor', + ), 'jquery.wikiEditor.dialogs' => array( 'scripts' => 'extensions/WikiEditor/modules/jquery.wikiEditor.dialogs.js', 'styles' => 'extensions/WikiEditor/modules/jquery.wikiEditor.dialogs.css', @@ -52,7 +57,10 @@ class WikiEditorHooks { ), 'jquery.wikiEditor.highlight' => array( 'scripts' => 'extensions/WikiEditor/modules/jquery.wikiEditor.highlight.js', - 'dependencies' => 'jquery.wikiEditor', + 'dependencies' => array( + 'jquery.wikiEditor', + 'jquery.wikiEditor.iframe', + ), 'group' => 'ext.wikiEditor', ), 'jquery.wikiEditor.preview' => array( @@ -82,13 +90,17 @@ class WikiEditorHooks { 'scripts' => 'extensions/WikiEditor/modules/jquery.wikiEditor.templateEditor.js', 'dependencies' => array( 'jquery.wikiEditor', - 'jquery.wikiEditor.dialogs', + 'jquery.wikiEditor.iframe', + 'jquery.wikiEditor.dialogs', ), 'group' => 'ext.wikiEditor', ), 'jquery.wikiEditor.templates' => array( 'scripts' => 'extensions/WikiEditor/modules/jquery.wikiEditor.templates.js', - 'dependencies' => 'jquery.wikiEditor', + 'dependencies' => array( + 'jquery.wikiEditor', + 'jquery.wikiEditor.iframe', + ), 'group' => 'ext.wikiEditor', ), 'jquery.wikiEditor.toc' => array( @@ -96,6 +108,7 @@ class WikiEditorHooks { 'styles' => 'extensions/WikiEditor/modules/jquery.wikiEditor.toc.css', 'dependencies' => array( 'jquery.wikiEditor', + 'jquery.wikiEditor.iframe', 'jquery.ui.draggable', 'jquery.ui.resizable', 'jquery.autoEllipsis', diff --git a/modules/jquery.wikiEditor.iframe.js b/modules/jquery.wikiEditor.iframe.js new file mode 100644 index 00000000..3e4c547d --- /dev/null +++ b/modules/jquery.wikiEditor.iframe.js @@ -0,0 +1,1317 @@ +/* IFrame extension for wikiEditor */ + +( function( $ ) { $.wikiEditor.extensions.iframe = 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, { + 'change': function( event ) { + event.data.scope = 'division'; + var newHTML = context.$content.html(); + if ( context.oldHTML != newHTML ) { + context.fn.purgeOffsets(); + context.oldHTML = newHTML; + event.data.scope = 'realchange'; + } + // Never let the body be totally empty + if ( context.$content.children().length == 0 ) { + context.$content.append( '

' ); + } + return true; + }, + 'delayedChange': function( event ) { + event.data.scope = 'division'; + var newHTML = context.$content.html(); + if ( context.oldDelayedHTML != newHTML ) { + context.oldDelayedHTML = newHTML; + event.data.scope = 'realchange'; + // Surround by

if it does not already have it + var cursorPos = context.fn.getCaretPosition(); + var t = context.fn.getOffset( cursorPos[0] ); + if ( ! $.browser.msie && t && t.node.nodeName == '#text' && t.node.parentNode.nodeName.toLowerCase() == 'body' ) { + $( t.node ).wrap( "

" ); + context.fn.purgeOffsets(); + context.fn.setSelection( { start: cursorPos[0], end: cursorPos[1] } ); + } + } + context.fn.updateHistory( event.data.scope == 'realchange' ); + return true; + }, + 'cut': function( event ) { + setTimeout( function() { + context.$content.find( 'br' ).each( function() { + if ( $(this).parent().is( 'body' ) ) { + $(this).wrap( $( '

' ) ); + } + } ); + }, 100 ); + return true; + }, + 'paste': function( event ) { + // Save the cursor position to restore it after all this voodoo + var cursorPos = context.fn.getCaretPosition(); + var oldLength = context.fn.getContents().length; + var positionFromEnd = oldLength - cursorPos[1]; + + //give everything the wikiEditor class so that we can easily pick out things without that class as pasted + context.$content.find( '*' ).addClass( 'wikiEditor' ); + if ( $.layout.name !== 'webkit' ) { + context.$content.addClass( 'pasting' ); + } + + setTimeout( function() { + // Kill stuff we know we don't want + context.$content.find( 'script,style,img,input,select,textarea,hr,button,link,meta' ).remove(); + var nodeToDelete = []; + var pastedContent = []; + var firstDirtyNode; + var $lastDirtyNode; + var elementAtCursor; + if ( $.browser.msie && !context.offsets ) { + elementAtCursor = null; + } else { + elementAtCursor = context.fn.getOffset( cursorPos[0] ); + } + if ( elementAtCursor == null || elementAtCursor.node == null ) { + context.$content.prepend( '

' ); + firstDirtyNode = context.$content.children()[0]; + } else { + firstDirtyNode = elementAtCursor.node; + } + + //this is ugly but seems like the best way to handle the case where we select and replace all editor contents + try { + firstDirtyNode.parentNode; + } catch ( err ) { + context.$content.prepend( '

' ); + firstDirtyNode = context.$content.children()[0]; + } + + while ( firstDirtyNode != null ) { + //we're going to replace the contents of the entire parent node. + while ( firstDirtyNode.parentNode && firstDirtyNode.parentNode.nodeName != 'BODY' + && ! $( firstDirtyNode ).hasClass( 'wikiEditor' ) + ) { + firstDirtyNode = firstDirtyNode.parentNode; + } + //go back till we find the first pasted node + while ( firstDirtyNode.previousSibling != null + && ! $( firstDirtyNode.previousSibling ).hasClass( 'wikiEditor' ) + ) { + + if ( $( firstDirtyNode.previousSibling ).hasClass( '#comment' ) ) { + $( firstDirtyNode ).remove(); + } else { + firstDirtyNode = firstDirtyNode.previousSibling; + } + } + + if ( firstDirtyNode.previousSibling != null ) { + $lastDirtyNode = $( firstDirtyNode.previousSibling ); + } else { + $lastDirtyNode = $( firstDirtyNode ); + } + + var cc = makeContentCollector( $.browser, null ); + while ( firstDirtyNode != null ) { + cc.collectContent(firstDirtyNode); + cc.notifyNextNode(firstDirtyNode.nextSibling); + + nodeToDelete.push( firstDirtyNode ); + + firstDirtyNode = firstDirtyNode.nextSibling; + if ( $( firstDirtyNode ).hasClass( 'wikiEditor' ) ) { + break; + } + } + + var ccData = cc.finish(); + pastedContent = ccData.lines; + var pastedPretty = ''; + for ( var i = 0; i < pastedContent.length; i++ ) { + //escape html + pastedPretty = pastedContent[i].replace(/&/g, '&').replace(//g, '>').replace(/\r?\n/g, '\\n'); + //replace leading white spaces with   + match = pastedContent[i].match(/^[\s]+[^\s]/); + if ( match != null && match.length > 0 ) { + index = match[0].length; + leadingSpace = match[0].replace(/[\s]/g, ' '); + pastedPretty = leadingSpace + pastedPretty.substring(index, pastedPretty.length); + } + + + if( !pastedPretty && $.browser.msie && i == 0 ) { + continue; + } + $newElement = $( '

' ); + if ( pastedPretty ) { + $newElement.html( pastedPretty ); + } else { + $newElement.html( '
' ); + } + $newElement.insertAfter( $lastDirtyNode ); + + $lastDirtyNode = $newElement; + + } + + //now delete all the original nodes that we prettified already + while ( nodeToDelete.length > 0 ) { + $deleteNode = $( nodeToDelete.pop() ); + $deleteNode.remove(); + } + + //anything without wikiEditor class was pasted. + $selection = context.$content.find( ':not(.wikiEditor)' ); + if ( $selection.length == 0 ) { + break; + } else { + firstDirtyNode = $selection.eq( 0 )[0]; + } + } + context.$content.find( '.wikiEditor' ).removeClass( 'wikiEditor' ); + + //now place the cursor at the end of pasted content + var newLength = context.fn.getContents().length; + var newPos = newLength - positionFromEnd; + + context.fn.purgeOffsets(); + context.fn.setSelection( { start: newPos, end: newPos } ); + + context.fn.scrollToCaretPosition(); + }, 0 ); + return true; + }, + 'ready': function( event ) { + // Initialize our history queue + if ( context.$content ) { + context.history.push( { 'html': context.$content.html(), 'sel': context.fn.getCaretPosition() } ); + } else { + context.history.push( { 'html': '', 'sel': context.fn.getCaretPosition() } ); + } + return true; + } +} ); + +/** + * Internally used functions + */ +context.fn = $.extend( context.fn, { + 'highlightLine': function( $element, mode ) { + if ( !$element.is( 'p' ) ) { + $element = $element.closest( 'p' ); + } + $element.css( 'backgroundColor', '#AACCFF' ); + setTimeout( function() { $element.animate( { 'backgroundColor': 'white' }, 'slow' ); }, 100 ); + setTimeout( function() { $element.css( 'backgroundColor', 'white' ); }, 1000 ); + }, + 'htmlToText': function( html ) { + // This function is slow for large inputs, so aggressively cache input/output pairs + if ( html in context.htmlToTextMap ) { + return context.htmlToTextMap[html]; + } + var origHTML = html; + + // We use this elaborate trickery for cross-browser compatibility + // IE does overzealous whitespace collapsing for $( '
' ).html( html );
+		// We also do 
and easy cases for

conversion here, complicated cases are handled later + html = html + .replace( /\r?\n/g, "" ) // IE7 inserts newlines before block elements + .replace( / /g, " " ) // We inserted these to prevent IE from collapsing spaces + .replace( /\]*\>\<\/p\>/gi, '

' ) // Remove trailing
from

+ .replace( /\<\/p\>\s*\]*\>/gi, "\n" ) // Easy case for

conversion + .replace( /\]*\>/gi, "\n" ) //
conversion + .replace( /\<\/p\>(\n*)\]*\>/gi, "$1\n" ) + // Un-nest

tags + .replace( /\]*\>]*\>/gi, '

' ) + .replace( /\<\/p\><\/p\>/gi, '

' ); + // Save leading and trailing whitespace now and restore it later. IE eats it all, and even Firefox + // won't leave everything alone + var leading = html.match( /^\s*/ )[0]; + var trailing = html.match( /\s*$/ )[0]; + html = html.substr( leading.length, html.length - leading.length - trailing.length ); + var $pre = $( '
' + html + '
' ); + $pre.find( '.wikiEditor-noinclude' ).each( function() { $( this ).remove(); } ); + // Convert tabs,

s and
s back + $pre.find( '.wikiEditor-tab' ).each( function() { $( this ).text( "\t" ); } ); + $pre.find( 'br' ).each( function() { $( this ).replaceWith( "\n" ); } ); + // Converting

s is wrong if there's nothing before them, so check that. + // .find( '* + p' ) isn't good enough because textnodes aren't considered + $pre.find( 'p' ).each( function() { + var text = $( this ).text(); + // If this

is preceded by some text, add a \n at the beginning, and if + // it's followed by a textnode, add a \n at the end + // We need the traverser because there can be other weird stuff in between + + // Check for preceding text + var t = new context.fn.rawTraverser( this.firstChild, this, $pre.get( 0 ), true ).prev(); + while ( t && t.node.nodeName != '#text' && t.node.nodeName != 'BR' && t.node.nodeName != 'P' ) { + t = t.prev(); + } + if ( t ) { + text = "\n" + text; + } + + // Check for following text + t = new context.fn.rawTraverser( this.lastChild, this, $pre.get( 0 ), true ).next(); + while ( t && t.node.nodeName != '#text' && t.node.nodeName != 'BR' && t.node.nodeName != 'P' ) { + t = t.next(); + } + if ( t && !t.inP && t.node.nodeName == '#text' && t.node.nodeValue.charAt( 0 ) != '\n' + && t.node.nodeValue.charAt( 0 ) != '\r' ) { + text += "\n"; + } + $( this ).text( text ); + } ); + var retval; + if ( $.browser.msie ) { + // IE aggressively collapses whitespace in .text() after having done DOM manipulation, + // but for some crazy reason this does work. Also convert \r back to \n + retval = $( '

' + $pre.html() + '
' ).text().replace( /\r/g, '\n' ); + } else { + retval = $pre.text(); + } + return context.htmlToTextMap[origHTML] = leading + retval + trailing; + }, + /** + * Get the first element before the selection that's in a certain class + * @param classname Class to match. Defaults to '', meaning any class + * @param strict If true, the element the selection starts in cannot match (default: false) + * @return jQuery object or null if unknown + */ + 'beforeSelection': function( classname, strict ) { + if ( typeof classname == 'undefined' ) { + classname = ''; + } + var e = null, offset = null; + if ( $.browser.msie && !context.$iframe[0].contentWindow.document.body ) { + return null; + } + if ( context.$iframe[0].contentWindow.getSelection ) { + // Firefox and Opera + var selection = context.$iframe[0].contentWindow.getSelection(); + // On load, webkit seems to not have a valid selection + if ( selection.baseNode !== null ) { + // Start at the selection's start and traverse the DOM backwards + // This is done by traversing an element's children first, then the element itself, then its parent + e = selection.getRangeAt( 0 ).startContainer; + offset = selection.getRangeAt( 0 ).startOffset; + } else { + return null; + } + + // When the cursor is on an empty line, Opera gives us a bogus range object with + // startContainer=endContainer=body and startOffset=endOffset=1 + var body = context.$iframe[0].contentWindow.document.body; + if ( $.browser.opera && e == body && offset == 1 ) { + return null; + } + } + if ( !e && context.$iframe[0].contentWindow.document.selection ) { + // IE + // Because there's nothing like range.startContainer in IE, we need to do a DOM traversal + // to find the element the start of the selection is in + var range = context.$iframe[0].contentWindow.document.selection.createRange(); + // Set range2 to the text before the selection + var range2 = context.$iframe[0].contentWindow.document.body.createTextRange(); + // For some reason this call throws errors in certain cases, e.g. when the selection is + // not in the iframe + try { + range2.setEndPoint( 'EndToStart', range ); + } catch ( ex ) { + return null; + } + var seekPos = context.fn.htmlToText( range2.htmlText ).length; + var offset = context.fn.getOffset( seekPos ); + e = offset ? offset.node : null; + offset = offset ? offset.offset : null; + if ( !e ) { + return null; + } + } + if ( e.nodeName != '#text' ) { + // The selection is not in a textnode, but between two non-text nodes + // (usually inside the between two
s). Go to the rightmost + // child of the node just before the selection + var newE = e.firstChild; + for ( var i = 0; i < offset - 1 && newE; i++ ) { + newE = newE.nextSibling; + } + while ( newE && newE.lastChild ) { + newE = newE.lastChild; + } + e = newE || e; + } + + // We'd normally use if( $( e ).hasClass( class ) in the while loop, but running the jQuery + // constructor thousands of times is very inefficient + var classStr = ' ' + classname + ' '; + while ( e ) { + if ( !strict && ( !classname || ( ' ' + e.className + ' ' ).indexOf( classStr ) != -1 ) ) { + return $( e ); + } + var next = e.previousSibling; + while ( next && next.lastChild ) { + next = next.lastChild; + } + e = next || e.parentNode; + strict = false; + } + return $( [] ); + }, + /** + * Object used by traverser(). Don't use this unless you know what you're doing + */ + 'rawTraverser': function( node, inP, ancestor, skipNoinclude ) { + this.node = node; + this.inP = inP; + this.ancestor = ancestor; + this.skipNoinclude = skipNoinclude; + this.next = function() { + var p = this.node; + var nextInP = this.inP; + while ( p && !p.nextSibling ) { + p = p.parentNode; + if ( p == this.ancestor ) { + // We're back at the ancestor, stop here + p = null; + } + if ( p && p.nodeName == "P" ) { + nextInP = null; + } + } + p = p ? p.nextSibling : null; + if ( p && p.nodeName == "P" ) { + nextInP = p; + } + do { + // Filter nodes with the wikiEditor-noinclude class + // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because + // $() is slow in a tight loop + if ( this.skipNoinclude ) { + while ( p && ( ' ' + p.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) { + p = p.nextSibling; + } + } + if ( p && p.firstChild ) { + p = p.firstChild; + if ( p.nodeName == "P" ) { + nextInP = p; + } + } + } while ( p && p.firstChild ); + // Instead of calling the rawTraverser constructor, inline it. This avoids function call overhead + return p ? { 'node': p, 'inP': nextInP, 'ancestor': this.ancestor, + 'skipNoinclude': this.skipNoinclude, 'next': this.next, 'prev': this.prev } : null; + }; + this.prev = function() { + var p = this.node; + var prevInP = this.inP; + while ( p && !p.previousSibling ) { + p = p.parentNode; + if ( p == this.ancestor ) { + // We're back at the ancestor, stop here + p = null; + } + if ( p && p.nodeName == "P" ) { + prevInP = null; + } + } + p = p ? p.previousSibling : null; + if ( p && p.nodeName == "P" ) { + prevInP = p; + } + do { + // Filter nodes with the wikiEditor-noinclude class + // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because + // $() is slow in a tight loop + if ( this.skipNoinclude ) { + while ( p && ( ' ' + p.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) { + p = p.previousSibling; + } + } + if ( p && p.lastChild ) { + p = p.lastChild; + if ( p.nodeName == "P" ) { + prevInP = p; + } + } + } while ( p && p.lastChild ); + // Instead of calling the rawTraverser constructor, inline it. This avoids function call overhead + return p ? { 'node': p, 'inP': prevInP, 'ancestor': this.ancestor, + 'skipNoinclude': this.skipNoinclude, 'next': this.next, 'prev': this.prev } : null; + }; + }, + /** + * Get an object used to traverse the leaf nodes in the iframe DOM. This traversal skips leaf nodes + * inside an element with the wikiEditor-noinclude class. This basically wraps rawTraverser + * + * @param start Node to start at + * @return Traverser object, use .next() or .prev() to get a traverser object referring to the + * previous/next node + */ + 'traverser': function( start ) { + // Find the leftmost leaf node in the tree + var startNode = start.jquery ? start.get( 0 ) : start; + var node = startNode; + var inP = node.nodeName == "P" ? node : null; + do { + // Filter nodes with the wikiEditor-noinclude class + // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because + // $() is slow in a tight loop + while ( node && ( ' ' + node.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) { + node = node.nextSibling; + } + if ( node && node.firstChild ) { + node = node.firstChild; + if ( node.nodeName == "P" ) { + inP = node; + } + } + } while ( node && node.firstChild ); + return new context.fn.rawTraverser( node, inP, startNode, true ); + }, + 'getOffset': function( offset ) { + if ( !context.offsets ) { + context.fn.refreshOffsets(); + } + if ( offset in context.offsets ) { + return context.offsets[offset]; + } + // Our offset is not pre-cached. Find the highest offset below it and interpolate + // We need to traverse the entire object because for() doesn't traverse in order + // We don't do in-order traversal because the object is sparse + var lowerBound = -1; + for ( var o in context.offsets ) { + var realO = parseInt( o ); + if ( realO < offset && realO > lowerBound) { + lowerBound = realO; + } + } + if ( !( lowerBound in context.offsets ) ) { + // Weird edge case: either offset is too large or the document is empty + return null; + } + var base = context.offsets[lowerBound]; + return context.offsets[offset] = { + 'node': base.node, + 'offset': base.offset + offset - lowerBound, + 'length': base.length, + 'lastTextNode': base.lastTextNode + }; + }, + 'purgeOffsets': function() { + context.offsets = null; + }, + 'refreshOffsets': function() { + context.offsets = [ ]; + var t = context.fn.traverser( context.$content ); + var pos = 0, lastTextNode = null; + while ( t ) { + if ( t.node.nodeName != '#text' && t.node.nodeName != 'BR' ) { + t = t.next(); + continue; + } + var nextPos = t.node.nodeName == '#text' ? pos + t.node.nodeValue.length : pos + 1; + var nextT = t.next(); + var leavingP = t.node.nodeName == '#text' && t.inP && nextT && ( !nextT.inP || nextT.inP != t.inP ); + context.offsets[pos] = { + 'node': t.node, + 'offset': 0, + 'length': nextPos - pos + ( leavingP ? 1 : 0 ), + 'lastTextNode': lastTextNode + }; + if ( leavingP ) { + //

Foo

looks like "Foo\n", make it quack like it too + // Basically we're faking the \n character much like we're treating
s + context.offsets[nextPos] = { + 'node': t.node, + 'offset': nextPos - pos, + 'length': nextPos - pos + 1, + 'lastTextNode': lastTextNode + }; + } + pos = nextPos + ( leavingP ? 1 : 0 ); + if ( t.node.nodeName == '#text' ) { + lastTextNode = t.node; + } + t = nextT; + } + }, + 'saveSelection': function() { + if ( !$.browser.msie ) { + // Only IE needs this + return; + } + if ( typeof context.$iframe != 'undefined' ) { + context.$iframe[0].contentWindow.focus(); + context.savedSelection = context.$iframe[0].contentWindow.document.selection.createRange(); + } else { + context.$textarea.focus(); + context.savedSelection = document.selection.createRange(); + } + }, + 'restoreSelection': function() { + if ( !$.browser.msie || context.savedSelection === null ) { + return; + } + if ( typeof context.$iframe != 'undefined' ) { + context.$iframe[0].contentWindow.focus(); + } else { + context.$textarea.focus(); + } + context.savedSelection.select(); + context.savedSelection = null; + }, + /** + * Update the history queue + * + * @param htmlChange pass true or false to inidicate if there was a text change that should potentially + * be given a new history state. + */ + 'updateHistory': function( htmlChange ) { + var newHTML = context.$content.html(); + var newSel = context.fn.getCaretPosition(); + // Was text changed? Was it because of a REDO or UNDO action? + if ( + context.history.length == 0 || + ( htmlChange && context.oldDelayedHistoryPosition == context.historyPosition ) + ) { + context.oldDelayedSel = newSel; + // Do we need to trim extras from our history? + // FIXME: this should really be happing on change, not on the delay + if ( context.historyPosition < -1 ) { + //clear out the extras + context.history.splice( context.history.length + context.historyPosition + 1 ); + context.historyPosition = -1; + } + context.history.push( { 'html': newHTML, 'sel': newSel } ); + // If the history has grown longer than 10 items, remove the earliest one + while ( context.history.length > 10 ) { + context.history.shift(); + } + } else if ( context.oldDelayedSel != newSel ) { + // If only the selection was changed, update it + context.oldDelayedSel = newSel; + context.history[context.history.length + context.historyPosition].sel = newSel; + } + // synch our old delayed history position until the next undo/redo action + context.oldDelayedHistoryPosition = context.historyPosition; + }, + /** + * Sets up the iframe in place of the textarea to allow more advanced operations + */ + 'setupIframe': function() { + context.$iframe = $( '' ) + .attr( { + 'frameBorder': 0, + 'border': 0, + 'tabindex': 1, + 'src': wgScriptPath + '/extensions/WikiEditor/modules/jquery.wikiEditor.html?' + + 'instance=' + context.instance + '&ts=' + ( new Date() ).getTime() + '&is=content', + 'id': 'wikiEditor-iframe-' + context.instance + } ) + .css( { + 'backgroundColor': 'white', + 'width': '100%', + 'height': context.$textarea.height(), + 'display': 'none', + 'overflow-y': 'scroll', + 'overflow-x': 'hidden' + } ) + .insertAfter( context.$textarea ) + .load( function() { + // Internet Explorer will reload the iframe once we turn on design mode, so we need to only turn it + // on during the first run, and then bail + if ( !this.isSecondRun ) { + // Turn the document's design mode on + context.$iframe[0].contentWindow.document.designMode = 'on'; + // Let the rest of this function happen next time around + if ( $.browser.msie ) { + this.isSecondRun = true; + return; + } + } + // Get a reference to the content area of the iframe + context.$content = $( context.$iframe[0].contentWindow.document.body ); + // Add classes to the body to influence the styles based on what's enabled + for ( module in context.modules ) { + context.$content.addClass( 'wikiEditor-' + module ); + } + // If we just do "context.$content.text( context.$textarea.val() )", Internet Explorer will strip + // out the whitespace charcters, specifically "\n" - so we must manually encode text and append it + // TODO: Refactor this into a textToHtml() function + var html = context.$textarea.val() + // We're gonna use &esc; as an escape sequence + .replace( /&esc;/g, '&esc;esc;' ) + // Escape existing uses of

,

,   and + .replace( /\/g, '&esc;<p>' ) + .replace( /\<\/p\>/g, '&esc;</p>' ) + .replace( + /\\<\/span\>/g, + '&esc;<span class="wikiEditor-tab"></span>' + ) + .replace( / /g, '&esc;&nbsp;' ); + // We must do some extra processing on IE to avoid dirty diffs, specifically IE will collapse + // leading spaces - browser sniffing is not ideal, but executing this code on a non-broken browser + // doesn't cause harm + if ( $.browser.msie ) { + html = html.replace( /\t/g, '' ); + if ( $.browser.versionNumber <= 7 ) { + // Replace all spaces matching   - IE <= 7 needs this because of its overzealous + // whitespace collapsing + html = html.replace( / /g, " " ); + } else { + // IE8 is happy if we just convert the first leading space to   + html = html.replace( /(^|\n) /g, "$1 " ); + } + } + // Use a dummy div to escape all entities + // This'll also escape
, and   , so we unescape those after + // We also need to unescape the doubly-escaped things mentioned above + html = $( '
' ).text( '

' + html.replace( /\r?\n/g, '

' ) + '

' ).html() + .replace( /&nbsp;/g, ' ' ) + // Allow

tags to survive encoding + .replace( /<p>/g, '

' ) + .replace( /<\/p>/g, '

' ) + // And too + .replace( + /<span( | )class=("|")wikiEditor-tab("|")><\/span>/g, + '' + ) + // Empty

tags need
tags in them + .replace( /

<\/p>/g, '


' ) + // Unescape &esc; stuff + .replace( /&esc;&amp;nbsp;/g, '&nbsp;' ) + .replace( /&esc;&lt;p&gt;/g, '<p>' ) + .replace( /&esc;&lt;\/p&gt;/g, '</p>' ) + .replace( + /&esc;&lt;span&nbsp;class=&quot;wikiEditor-tab&quot;&gt;&lt;\/span&gt;/g, + '<span class="wikiEditor-tab"><\/span>' + ) + .replace( /&esc;esc;/g, '&esc;' ); + context.$content.html( html ); + + // Reflect direction of parent frame into child + if ( $( 'body' ).is( '.rtl' ) ) { + context.$content.addClass( 'rtl' ).attr( 'dir', 'rtl' ); + } + // Activate the iframe, encoding the content of the textarea and copying it to the content of iframe + context.$textarea.attr( 'disabled', true ); + context.$textarea.hide(); + context.$iframe.show(); + // Let modules know we're ready to start working with the content + context.fn.trigger( 'ready' ); + // Only save HTML now: ready handlers may have modified it + context.oldHTML = context.oldDelayedHTML = context.$content.html(); + //remove our temporary loading + /* Disaling our loading div for now + $( '.wikiEditor-ui-loading' ).fadeOut( 'fast', function() { + $( this ).remove(); + } ); + */ + // Setup event handling on the iframe + $( context.$iframe[0].contentWindow.document ) + .bind( 'keydown', function( event ) { + event.jQueryNode = context.fn.getElementAtCursor(); + return context.fn.trigger( 'keydown', event ); + + } ) + .bind( 'keyup', function( event ) { + event.jQueryNode = context.fn.getElementAtCursor(); + return context.fn.trigger( 'keyup', event ); + } ) + .bind( 'keypress', function( event ) { + event.jQueryNode = context.fn.getElementAtCursor(); + return context.fn.trigger( 'keypress', event ); + } ) + .bind( 'paste', function( event ) { + return context.fn.trigger( 'paste', event ); + } ) + .bind( 'cut', function( event ) { + return context.fn.trigger( 'cut', event ); + } ) + .bind( 'keyup paste mouseup cut encapsulateSelection', function( event ) { + return context.fn.trigger( 'change', event ); + } ) + .delayedBind( 250, 'keyup paste mouseup cut encapsulateSelection', function( event ) { + context.fn.trigger( 'delayedChange', event ); + } ); + } ); + // Attach a submit handler to the form so that when the form is submitted the content of the iframe gets + // decoded and copied over to the textarea + context.$textarea.closest( 'form' ).submit( function() { + context.$textarea.attr( 'disabled', false ); + context.$textarea.val( context.$textarea.textSelection( 'getContents' ) ); + } ); + /* FIXME: This was taken from EditWarning.js - maybe we could do a jquery plugin for this? */ + // Attach our own handler for onbeforeunload which respects the current one + context.fallbackWindowOnBeforeUnload = window.onbeforeunload; + window.onbeforeunload = function() { + context.$textarea.val( context.$textarea.textSelection( 'getContents' ) ); + if ( context.fallbackWindowOnBeforeUnload ) { + return context.fallbackWindowOnBeforeUnload(); + } + }; + }, + + /* + * 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() { + if ( context.$iframe[0].contentWindow.getSelection ) { + // Firefox and Opera + var selection = context.$iframe[0].contentWindow.getSelection(); + if ( selection.rangeCount == 0 ) { + // We don't know where the cursor is + return $( [] ); + } + var sc = selection.getRangeAt( 0 ).startContainer; + if ( sc.nodeName == "#text" ) sc = sc.parentNode; + return $( sc ); + } else if ( context.$iframe[0].contentWindow.document.selection ) { // should come last; Opera! + // IE + var selection = context.$iframe[0].contentWindow.document.selection.createRange(); + return $( selection.parentElement() ); + } + }, + + /** + * Gets the complete contents of the iframe (in plain text, not HTML) + */ + 'getContents': function() { + // For

, .html() returns

 

in IE + // This seems to convince IE while not affecting display + if ( !context.$content ) { + return ''; + } + var html; + if ( $.browser.msie ) { + // Don't manipulate the iframe DOM itself, causes cursor jumping issues + var $c = $( context.$content.get( 0 ).cloneNode( true ) ); + $c.find( 'p' ).each( function() { + if ( $(this).html() == '' ) { + $(this).replaceWith( '

' ); + } + } ); + html = $c.html(); + } else { + html = context.$content.html(); + } + return context.fn.htmlToText( html ); + }, + /** + * Gets the currently selected text in the content + * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead + */ + 'getSelection': function() { + var retval; + if ( context.$iframe[0].contentWindow.getSelection ) { + // Firefox and Opera + retval = context.$iframe[0].contentWindow.getSelection(); + if ( $.browser.opera ) { + // Opera strips newlines in getSelection(), so we need something more sophisticated + if ( retval.rangeCount > 0 ) { + retval = context.fn.htmlToText( $( '
' )
+							.append( retval.getRangeAt( 0 ).cloneContents() )
+							.html()
+					);
+				} else {
+					retval = '';
+				}
+			}
+		} else if ( context.$iframe[0].contentWindow.document.selection ) { // should come last; Opera!
+			// IE
+			retval = context.$iframe[0].contentWindow.document.selection.createRange();
+		}
+		if ( typeof retval.text != 'undefined' ) {
+			// In IE8, retval.text is stripped of newlines, so we need to process retval.htmlText
+			// to get a reliable answer. IE7 does get this right though
+			// Run this fix for all IE versions anyway, it doesn't hurt
+			retval = context.fn.htmlToText( retval.htmlText );
+		} else if ( typeof retval.toString != 'undefined' ) {
+			retval = retval.toString();
+		}
+		return retval;
+	},
+	/**
+	 * 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 selText = $(this).textSelection( 'getSelection' );
+		var selTextArr;
+		var collapseToEnd = false;
+		var selectAfter = false;
+		var setSelectionTo = null;
+		var pre = options.pre, post = options.post;
+		if ( !selText ) {
+			selText = options.peri;
+			selectAfter = true;
+		} else if ( options.peri == selText.replace( /\s+$/, '' ) ) {
+			// Probably a successive button press
+			// strip any extra white space from selText
+			selText = selText.replace( /\s+$/, '' );
+			// set the collapseToEnd flag to ensure our selection is collapsed to the end before any insertion is done
+			collapseToEnd = true;
+			// set selectAfter to true since we know we'll be populating with our default text
+			selectAfter = true;
+		} else if ( options.replace ) {
+			selText = options.peri;
+		} else if ( selText.charAt( selText.length - 1 ) == ' ' ) {
+			// Exclude ending space char
+			// FIXME: Why?
+			selText = selText.substring( 0, selText.length - 1 );
+			post += ' ';
+		}
+		if ( options.splitlines ) {
+			selTextArr = selText.split( /\n/ );
+		}
+
+		if ( context.$iframe[0].contentWindow.getSelection ) {
+			// Firefox and Opera
+			var range = context.$iframe[0].contentWindow.getSelection().getRangeAt( 0 );
+			// if our test above indicated that this was a sucessive button press, we need to collapse the 
+			// selection to the end to avoid replacing text 
+			if ( collapseToEnd ) {
+				// Make sure we're not collapsing ourselves into a BR tag
+				if ( range.endContainer.nodeName == 'BR' ) {
+					range.setEndBefore( range.endContainer );
+				}
+				range.collapse( false );
+			}
+			if ( options.ownline ) {
+				// We need to figure out if the cursor is at the start or end of a line
+				var atStart = false, atEnd = false;
+				var body = context.$content.get( 0 );
+				if ( range.startOffset == 0 ) {
+					// Start of a line
+					// FIXME: Not necessarily the case with syntax highlighting or
+					// template collapsing
+					atStart = true;
+				} else if ( range.startContainer == body ) {
+					// Look up the node just before the start of the selection
+					// If it's a 
, we're at the start of a line that starts with a + // block element; if not, we're at the end of a line + var n = body.firstChild; + for ( var i = 0; i < range.startOffset - 1 && n; i++ ) { + n = n.nextSibling; + } + if ( n && n.nodeName == 'BR' ) { + atStart = true; + } else { + atEnd = true; + } + } + if ( ( range.endOffset == 0 && range.endContainer.nodeValue == null ) || + ( range.endContainer.nodeName == '#text' && + range.endOffset == range.endContainer.nodeValue.length ) || + ( range.endContainer.nodeName == 'P' && range.endContainer.nodeValue == null ) ) { + atEnd = true; + } + if ( !atStart ) { + pre = "\n" + options.pre; + } + if ( !atEnd ) { + post += "\n"; + } + } + var insertText = ""; + if ( options.splitlines ) { + for( var j = 0; j < selTextArr.length; j++ ) { + insertText = insertText + pre + selTextArr[j] + post; + if( j != selTextArr.length - 1 ) { + insertText += "\n"; + } + } + } else { + insertText = pre + selText + post; + } + var insertLines = insertText.split( "\n" ); + range.extractContents(); + // Insert the contents one line at a time - insertNode() inserts at the beginning, so this has to happen + // in reverse order + // Track the first and last inserted node, and if we need to also track where the text we need to select + // afterwards starts and ends + var firstNode = null, lastNode = null; + var selSC = null, selEC = null, selSO = null, selEO = null, offset = 0; + for ( var i = insertLines.length - 1; i >= 0; i-- ) { + firstNode = context.$iframe[0].contentWindow.document.createTextNode( insertLines[i] ); + range.insertNode( firstNode ); + lastNode = lastNode || firstNode; + var newOffset = offset + insertLines[i].length; + if ( !selEC && post.length <= newOffset ) { + selEC = firstNode; + selEO = selEC.nodeValue.length - ( post.length - offset ); + } + if ( selEC && !selSC && pre.length >= insertText.length - newOffset ) { + selSC = firstNode; + selSO = pre.length - ( insertText.length - newOffset ); + } + offset = newOffset; + if ( i > 0 ) { + firstNode = context.$iframe[0].contentWindow.document.createElement( 'br' ); + range.insertNode( firstNode ); + newOffset = offset + 1; + if ( !selEC && post.length <= newOffset ) { + selEC = firstNode; + selEO = 1 - ( post.length - offset ); + } + if ( selEC && !selSC && pre.length >= insertText.length - newOffset ) { + selSC = firstNode; + selSO = pre.length - ( insertText.length - newOffset ); + } + offset = newOffset; + } + } + if ( firstNode ) { + context.fn.scrollToTop( $( firstNode.parentNode ) ); + } + if ( selectAfter ) { + setSelectionTo = { + startContainer: selSC, + endContainer: selEC, + start: selSO, + end: selEO + }; + } else if ( lastNode ) { + setSelectionTo = { + startContainer: lastNode, + endContainer: lastNode, + start: lastNode.nodeValue.length, + end: lastNode.nodeValue.length + }; + } + } else if ( context.$iframe[0].contentWindow.document.selection ) { + // IE + context.$iframe[0].contentWindow.focus(); + var range = context.$iframe[0].contentWindow.document.selection.createRange(); + if ( options.ownline && range.moveStart ) { + // Check if we're at the start of a line + // If not, prepend a newline + var range2 = context.$iframe[0].contentWindow.document.selection.createRange(); + range2.collapse(); + range2.moveStart( 'character', -1 ); + // FIXME: Which check is correct? + if ( range2.text != "\r" && range2.text != "\n" && range2.text != "" ) { + pre = "\n" + pre; + } + + // Check if we're at the end of a line + // If not, append a newline + var range3 = context.$iframe[0].contentWindow.document.selection.createRange(); + range3.collapse( false ); + range3.moveEnd( 'character', 1 ); + if ( range3.text != "\r" && range3.text != "\n" && range3.text != "" ) { + post += "\n"; + } + } + // if our test above indicated that this was a sucessive button press, we need to collapse the + // selection to the end to avoid replacing text + if ( collapseToEnd ) { + range.collapse( false ); + } + // TODO: Clean this up. Duplicate code due to the pre-existing browser specific structure of this + // function + var insertText = ""; + if ( options.splitlines ) { + for( var j = 0; j < selTextArr.length; j++ ) { + insertText = insertText + pre + selTextArr[j] + post; + if( j != selTextArr.length - 1 ) { + insertText += "\n"; + } + } + } else { + insertText = pre + selText + post; + } + // TODO: Maybe find a more elegant way of doing this like the Firefox code above? + range.pasteHTML( insertText + .replace( /\/g, '>' ) + .replace( /\r?\n/g, '
' ) + ); + if ( selectAfter ) { + range.moveStart( 'character', -post.length - selText.length ); + range.moveEnd( 'character', -post.length ); + range.select(); + } + } + + if ( setSelectionTo ) { + context.fn.setSelection( setSelectionTo ); + } + // Trigger the encapsulateSelection event (this might need to get named something else/done differently) + $( context.$iframe[0].contentWindow.document ).trigger( + 'encapsulateSelection', [ pre, options.peri, post, options.ownline, options.replace ] + ); + 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 ) { + var startPos = null, endPos = null; + if ( context.$iframe[0].contentWindow.getSelection ) { + var selection = context.$iframe[0].contentWindow.getSelection(); + if ( selection.rangeCount == 0 ) { + // We don't know where the cursor is + return [ 0, 0 ]; + } + var sc = selection.getRangeAt( 0 ).startContainer, ec = selection.getRangeAt( 0 ).endContainer; + var so = selection.getRangeAt( 0 ).startOffset, eo = selection.getRangeAt( 0 ).endOffset; + if ( sc.nodeName == 'BODY' ) { + // Grab the node just before the start of the selection + var n = sc.firstChild; + for ( var i = 0; i < so - 1 && n; i++ ) { + n = n.nextSibling; + } + sc = n; + so = 0; + } + if ( ec.nodeName == 'BODY' ) { + var n = ec.firstChild; + for ( var i = 0; i < eo - 1 && n; i++ ) { + n = n.nextSibling; + } + ec = n; + eo = 0; + } + + // Make sure sc and ec are leaf nodes + while ( sc.firstChild ) { + sc = sc.firstChild; + } + while ( ec.firstChild ) { + ec = ec.firstChild; + } + // Make sure the offsets are regenerated if necessary + context.fn.getOffset( 0 ); + var o; + for ( o in context.offsets ) { + if ( startPos === null && context.offsets[o].node == sc ) { + // For some wicked reason o is a string, even though + // we put it in as an integer. Use ~~ to coerce it too an int + startPos = ~~o + so - context.offsets[o].offset; + } + if ( startPos !== null && context.offsets[o].node == ec ) { + endPos = ~~o + eo - context.offsets[o].offset; + break; + } + } + } else if ( context.$iframe[0].contentWindow.document.selection ) { + // IE + // FIXME: This is mostly copypasted from the textSelection plugin + var d = context.$iframe[0].contentWindow.document; + var postFinished = false; + var periFinished = false; + var postFinished = false; + var preText, rawPreText, periText; + var rawPeriText, postText, rawPostText; + // Depending on the document state, and if the cursor has ever been manually placed within the document + // the following call such as setEndPoint can result in nasty errors. These cases are always cases + // in which the start and end points can safely be assumed to be 0, so we will just try our best to do + // the full process but fall back to 0. + try { + // Create range containing text in the selection + var periRange = d.selection.createRange().duplicate(); + // Create range containing text before the selection + var preRange = d.body.createTextRange(); + // Move the end where we need it + preRange.setEndPoint( "EndToStart", periRange ); + // Create range containing text after the selection + var postRange = d.body.createTextRange(); + // Move the start where we need it + postRange.setEndPoint( "StartToEnd", periRange ); + // Load the text values we need to compare + preText = rawPreText = preRange.text; + periText = rawPeriText = periRange.text; + postText = rawPostText = postRange.text; + /* + * Check each range for trimmed newlines by shrinking the range by 1 + * character and seeing if the text property has changed. If it has + * not changed then we know that IE has trimmed a \r\n from the end. + */ + do { + if ( !postFinished ) { + if ( preRange.compareEndPoints( "StartToEnd", preRange ) == 0 ) { + postFinished = true; + } else { + preRange.moveEnd( "character", -1 ) + if ( preRange.text == preText ) { + rawPreText += "\r\n"; + } else { + postFinished = true; + } + } + } + if ( !periFinished ) { + if ( periRange.compareEndPoints( "StartToEnd", periRange ) == 0 ) { + periFinished = true; + } else { + periRange.moveEnd( "character", -1 ) + if ( periRange.text == periText ) { + rawPeriText += "\r\n"; + } else { + periFinished = true; + } + } + } + if ( !postFinished ) { + if ( postRange.compareEndPoints("StartToEnd", postRange) == 0 ) { + postFinished = true; + } else { + postRange.moveEnd( "character", -1 ) + if ( postRange.text == postText ) { + rawPostText += "\r\n"; + } else { + postFinished = true; + } + } + } + } while ( ( !postFinished || !periFinished || !postFinished ) ); + startPos = rawPreText.replace( /\r\n/g, "\n" ).length; + endPos = startPos + rawPeriText.replace( /\r\n/g, "\n" ).length; + } catch( e ) { + startPos = endPos = 0; + } + } + return [ startPos, endPos ]; + }, + /** + * 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 sc = options.startContainer, ec = options.endContainer; + sc = sc && sc.jquery ? sc[0] : sc; + ec = ec && ec.jquery ? ec[0] : ec; + if ( context.$iframe[0].contentWindow.getSelection ) { + // Firefox and Opera + var start = options.start, end = options.end; + if ( !sc || !ec ) { + var s = context.fn.getOffset( start ); + var e = context.fn.getOffset( end ); + sc = s ? s.node : null; + ec = e ? e.node : null; + start = s ? s.offset : null; + end = e ? e.offset : null; + // Don't try to set the selection past the end of a node, causes errors + // Just put the selection at the end of the node in this case + if ( sc != null && sc.nodeName == '#text' && start > sc.nodeValue.length ) { + start = sc.nodeValue.length - 1; + } + if ( ec != null && ec.nodeName == '#text' && end > ec.nodeValue.length ) { + end = ec.nodeValue.length - 1; + } + } + if ( !sc || !ec ) { + // The requested offset isn't in the offsets array + // Give up + return context.$textarea; + } + + var sel = context.$iframe[0].contentWindow.getSelection(); + while ( sc.firstChild && sc.nodeName != '#text' ) { + sc = sc.firstChild; + } + while ( ec.firstChild && ec.nodeName != '#text' ) { + ec = ec.firstChild; + } + var range = context.$iframe[0].contentWindow.document.createRange(); + range.setStart( sc, start ); + range.setEnd( ec, end ); + sel.removeAllRanges(); + sel.addRange( range ); + context.$iframe[0].contentWindow.focus(); + } else if ( context.$iframe[0].contentWindow.document.body.createTextRange ) { + // IE + var range = context.$iframe[0].contentWindow.document.body.createTextRange(); + if ( sc ) { + range.moveToElementText( sc ); + } + range.collapse(); + range.moveEnd( 'character', options.start ); + + var range2 = context.$iframe[0].contentWindow.document.body.createTextRange(); + if ( ec ) { + range2.moveToElementText( ec ); + } + range2.collapse(); + range2.moveEnd( 'character', options.end ); + + // IE does newline emulation for

s:

foo

bar

becomes foo\nbar just fine + // but

foo



bar

becomes foo\n\n\n\nbar , one \n too many + // Correct for this + var matches, counted = 0; + // while ( matches = range.htmlText.match( regex ) && matches.length <= counted ) doesn't work + // because the assignment side effect hasn't happened yet when the second term is evaluated + while ( matches = range.htmlText.match( /\<\/p\>(\]*\>)+\/gi ) ) { + if ( matches.length <= counted ) + break; + range.moveEnd( 'character', matches.length ); + counted += matches.length; + } + range2.moveEnd( 'character', counted ); + while ( matches = range2.htmlText.match( /\<\/p\>(\]*\>)+\/gi ) ) { + if ( matches.length <= counted ) + break; + range2.moveEnd( 'character', matches.length ); + counted += matches.length; + } + + range2.setEndPoint( 'StartToEnd', range ); + range2.select(); + } + 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 ) { + context.fn.scrollToTop( context.fn.getElementAtCursor(), true ); + }, + /** + * 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 ) { + var html = context.$content.closest( 'html' ), + body = context.$content.closest( 'body' ), + parentHtml = $( 'html' ), + parentBody = $( 'body' ); + var y = $element.offset().top; + if ( !$.browser.msie && ! $element.is( 'body' ) ) { + y = parentHtml.scrollTop() > 0 ? y + html.scrollTop() - parentHtml.scrollTop() : y; + y = parentBody.scrollTop() > 0 ? y + body.scrollTop() - parentBody.scrollTop() : y; + } + var topBound = html.scrollTop() > body.scrollTop() ? html.scrollTop() : body.scrollTop(), + bottomBound = topBound + context.$iframe.height(); + if ( force || y < topBound || y > bottomBound ) { + html.scrollTop( y ); + body.scrollTop( y ); + } + $element.trigger( 'scrollToTop' ); + } +} ); + +/* Setup the IFrame */ +context.fn.setupIframe(); + +} } )( jQuery ); diff --git a/modules/jquery.wikiEditor.js b/modules/jquery.wikiEditor.js index e9ddba00..2a40fbc9 100644 --- a/modules/jquery.wikiEditor.js +++ b/modules/jquery.wikiEditor.js @@ -20,6 +20,10 @@ $.wikiEditor = { * module is in use by a specific context check the context.modules object. */ 'modules': {}, + /** + * A context can be extended, such as adding iframe support, on a per-wikiEditor instance basis. + */ + 'extensions': {}, /** * In some cases like with the iframe's HTML file, it's convienent to have a lookup table of all instances of the * WikiEditor. Each context contains an instance field which contains a key that corrosponds to a reference to the @@ -239,7 +243,9 @@ if ( !context || typeof context == 'undefined' ) { // Current history state position - this is number of steps backwards, so it's always -1 or less 'historyPosition': -1, /// The previous historyPosition, stored to detect if change events were due to an undo or redo action - 'oldDelayedHistoryPosition': -1 + 'oldDelayedHistoryPosition': -1, + // List of extensions active on this context + 'extensions': [], }; /* @@ -361,201 +367,13 @@ if ( !context || typeof context == 'undefined' ) { } break; case 86: //v - if ( event.ctrlKey && $.browser.msie ) { + if ( event.ctrlKey && $.browser.msie && 'paste' in context.evt ) { //paste, intercepted for IE context.evt.paste( event ); } break; } return true; - }, - 'change': function( event ) { - event.data.scope = 'division'; - var newHTML = context.$content.html(); - if ( context.oldHTML != newHTML ) { - context.fn.purgeOffsets(); - context.oldHTML = newHTML; - event.data.scope = 'realchange'; - } - // Never let the body be totally empty - if ( context.$content.children().length == 0 ) { - context.$content.append( '

' ); - } - return true; - }, - 'delayedChange': function( event ) { - event.data.scope = 'division'; - var newHTML = context.$content.html(); - if ( context.oldDelayedHTML != newHTML ) { - context.oldDelayedHTML = newHTML; - event.data.scope = 'realchange'; - // Surround by

if it does not already have it - var cursorPos = context.fn.getCaretPosition(); - var t = context.fn.getOffset( cursorPos[0] ); - if ( ! $.browser.msie && t && t.node.nodeName == '#text' && t.node.parentNode.nodeName.toLowerCase() == 'body' ) { - $( t.node ).wrap( "

" ); - context.fn.purgeOffsets(); - context.fn.setSelection( { start: cursorPos[0], end: cursorPos[1] } ); - } - } - context.fn.updateHistory( event.data.scope == 'realchange' ); - return true; - }, - 'cut': function( event ) { - setTimeout( function() { - context.$content.find( 'br' ).each( function() { - if ( $(this).parent().is( 'body' ) ) { - $(this).wrap( $( '

' ) ); - } - } ); - }, 100 ); - return true; - }, - 'paste': function( event ) { - // Save the cursor position to restore it after all this voodoo - var cursorPos = context.fn.getCaretPosition(); - var oldLength = context.fn.getContents().length; - var positionFromEnd = oldLength - cursorPos[1]; - - //give everything the wikiEditor class so that we can easily pick out things without that class as pasted - context.$content.find( '*' ).addClass( 'wikiEditor' ); - if ( $.layout.name !== 'webkit' ) { - context.$content.addClass( 'pasting' ); - } - - setTimeout( function() { - // Kill stuff we know we don't want - context.$content.find( 'script,style,img,input,select,textarea,hr,button,link,meta' ).remove(); - var nodeToDelete = []; - var pastedContent = []; - var firstDirtyNode; - var $lastDirtyNode; - var elementAtCursor; - if ( $.browser.msie && !context.offsets ) { - elementAtCursor = null; - } else { - elementAtCursor = context.fn.getOffset( cursorPos[0] ); - } - if ( elementAtCursor == null || elementAtCursor.node == null ) { - context.$content.prepend( '

' ); - firstDirtyNode = context.$content.children()[0]; - } else { - firstDirtyNode = elementAtCursor.node; - } - - //this is ugly but seems like the best way to handle the case where we select and replace all editor contents - try { - firstDirtyNode.parentNode; - } catch ( err ) { - context.$content.prepend( '

' ); - firstDirtyNode = context.$content.children()[0]; - } - - while ( firstDirtyNode != null ) { - //we're going to replace the contents of the entire parent node. - while ( firstDirtyNode.parentNode && firstDirtyNode.parentNode.nodeName != 'BODY' - && ! $( firstDirtyNode ).hasClass( 'wikiEditor' ) - ) { - firstDirtyNode = firstDirtyNode.parentNode; - } - //go back till we find the first pasted node - while ( firstDirtyNode.previousSibling != null - && ! $( firstDirtyNode.previousSibling ).hasClass( 'wikiEditor' ) - ) { - - if ( $( firstDirtyNode.previousSibling ).hasClass( '#comment' ) ) { - $( firstDirtyNode ).remove(); - } else { - firstDirtyNode = firstDirtyNode.previousSibling; - } - } - - if ( firstDirtyNode.previousSibling != null ) { - $lastDirtyNode = $( firstDirtyNode.previousSibling ); - } else { - $lastDirtyNode = $( firstDirtyNode ); - } - - var cc = makeContentCollector( $.browser, null ); - while ( firstDirtyNode != null ) { - cc.collectContent(firstDirtyNode); - cc.notifyNextNode(firstDirtyNode.nextSibling); - - nodeToDelete.push( firstDirtyNode ); - - firstDirtyNode = firstDirtyNode.nextSibling; - if ( $( firstDirtyNode ).hasClass( 'wikiEditor' ) ) { - break; - } - } - - var ccData = cc.finish(); - pastedContent = ccData.lines; - var pastedPretty = ''; - for ( var i = 0; i < pastedContent.length; i++ ) { - //escape html - pastedPretty = pastedContent[i].replace(/&/g, '&').replace(//g, '>').replace(/\r?\n/g, '\\n'); - //replace leading white spaces with   - match = pastedContent[i].match(/^[\s]+[^\s]/); - if ( match != null && match.length > 0 ) { - index = match[0].length; - leadingSpace = match[0].replace(/[\s]/g, ' '); - pastedPretty = leadingSpace + pastedPretty.substring(index, pastedPretty.length); - } - - - if( !pastedPretty && $.browser.msie && i == 0 ) { - continue; - } - $newElement = $( '

' ); - if ( pastedPretty ) { - $newElement.html( pastedPretty ); - } else { - $newElement.html( '
' ); - } - $newElement.insertAfter( $lastDirtyNode ); - - $lastDirtyNode = $newElement; - - } - - //now delete all the original nodes that we prettified already - while ( nodeToDelete.length > 0 ) { - $deleteNode = $( nodeToDelete.pop() ); - $deleteNode.remove(); - } - - //anything without wikiEditor class was pasted. - $selection = context.$content.find( ':not(.wikiEditor)' ); - if ( $selection.length == 0 ) { - break; - } else { - firstDirtyNode = $selection.eq( 0 )[0]; - } - } - context.$content.find( '.wikiEditor' ).removeClass( 'wikiEditor' ); - - //now place the cursor at the end of pasted content - var newLength = context.fn.getContents().length; - var newPos = newLength - positionFromEnd; - - context.fn.purgeOffsets(); - context.fn.setSelection( { start: newPos, end: newPos } ); - - context.fn.scrollToCaretPosition(); - - - }, 0 ); - return true; - }, - 'ready': function( event ) { - // Initialize our history queue - if ( context.$content ) { - context.history.push( { 'html': context.$content.html(), 'sel': context.fn.getCaretPosition() } ); - } else { - context.history.push( { 'html': '', 'sel': context.fn.getCaretPosition() } ); - } - return true; } }; @@ -667,1114 +485,6 @@ if ( !context || typeof context == 'undefined' ) { .hide() .appendTo( context.$ui ); }, - 'highlightLine': function( $element, mode ) { - if ( !$element.is( 'p' ) ) { - $element = $element.closest( 'p' ); - } - $element.css( 'backgroundColor', '#AACCFF' ); - setTimeout( function() { $element.animate( { 'backgroundColor': 'white' }, 'slow' ); }, 100 ); - setTimeout( function() { $element.css( 'backgroundColor', 'white' ); }, 1000 ); - }, - 'htmlToText': function( html ) { - // This function is slow for large inputs, so aggressively cache input/output pairs - if ( html in context.htmlToTextMap ) { - return context.htmlToTextMap[html]; - } - var origHTML = html; - - // We use this elaborate trickery for cross-browser compatibility - // IE does overzealous whitespace collapsing for $( '
' ).html( html );
-			// We also do 
and easy cases for

conversion here, complicated cases are handled later - html = html - .replace( /\r?\n/g, "" ) // IE7 inserts newlines before block elements - .replace( / /g, " " ) // We inserted these to prevent IE from collapsing spaces - .replace( /\]*\>\<\/p\>/gi, '

' ) // Remove trailing
from

- .replace( /\<\/p\>\s*\]*\>/gi, "\n" ) // Easy case for

conversion - .replace( /\]*\>/gi, "\n" ) //
conversion - .replace( /\<\/p\>(\n*)\]*\>/gi, "$1\n" ) - // Un-nest

tags - .replace( /\]*\>]*\>/gi, '

' ) - .replace( /\<\/p\><\/p\>/gi, '

' ); - // Save leading and trailing whitespace now and restore it later. IE eats it all, and even Firefox - // won't leave everything alone - var leading = html.match( /^\s*/ )[0]; - var trailing = html.match( /\s*$/ )[0]; - html = html.substr( leading.length, html.length - leading.length - trailing.length ); - var $pre = $( '
' + html + '
' ); - $pre.find( '.wikiEditor-noinclude' ).each( function() { $( this ).remove(); } ); - // Convert tabs,

s and
s back - $pre.find( '.wikiEditor-tab' ).each( function() { $( this ).text( "\t" ); } ); - $pre.find( 'br' ).each( function() { $( this ).replaceWith( "\n" ); } ); - // Converting

s is wrong if there's nothing before them, so check that. - // .find( '* + p' ) isn't good enough because textnodes aren't considered - $pre.find( 'p' ).each( function() { - var text = $( this ).text(); - // If this

is preceded by some text, add a \n at the beginning, and if - // it's followed by a textnode, add a \n at the end - // We need the traverser because there can be other weird stuff in between - - // Check for preceding text - var t = new context.fn.rawTraverser( this.firstChild, this, $pre.get( 0 ), true ).prev(); - while ( t && t.node.nodeName != '#text' && t.node.nodeName != 'BR' && t.node.nodeName != 'P' ) { - t = t.prev(); - } - if ( t ) { - text = "\n" + text; - } - - // Check for following text - t = new context.fn.rawTraverser( this.lastChild, this, $pre.get( 0 ), true ).next(); - while ( t && t.node.nodeName != '#text' && t.node.nodeName != 'BR' && t.node.nodeName != 'P' ) { - t = t.next(); - } - if ( t && !t.inP && t.node.nodeName == '#text' && t.node.nodeValue.charAt( 0 ) != '\n' - && t.node.nodeValue.charAt( 0 ) != '\r' ) { - text += "\n"; - } - $( this ).text( text ); - } ); - var retval; - if ( $.browser.msie ) { - // IE aggressively collapses whitespace in .text() after having done DOM manipulation, - // but for some crazy reason this does work. Also convert \r back to \n - retval = $( '

' + $pre.html() + '
' ).text().replace( /\r/g, '\n' ); - } else { - retval = $pre.text(); - } - return context.htmlToTextMap[origHTML] = leading + retval + trailing; - }, - /** - * Get the first element before the selection that's in a certain class - * @param classname Class to match. Defaults to '', meaning any class - * @param strict If true, the element the selection starts in cannot match (default: false) - * @return jQuery object or null if unknown - */ - 'beforeSelection': function( classname, strict ) { - if ( typeof classname == 'undefined' ) { - classname = ''; - } - var e = null, offset = null; - if ( $.browser.msie && !context.$iframe[0].contentWindow.document.body ) { - return null; - } - if ( context.$iframe[0].contentWindow.getSelection ) { - // Firefox and Opera - var selection = context.$iframe[0].contentWindow.getSelection(); - // On load, webkit seems to not have a valid selection - if ( selection.baseNode !== null ) { - // Start at the selection's start and traverse the DOM backwards - // This is done by traversing an element's children first, then the element itself, then its parent - e = selection.getRangeAt( 0 ).startContainer; - offset = selection.getRangeAt( 0 ).startOffset; - } else { - return null; - } - - // When the cursor is on an empty line, Opera gives us a bogus range object with - // startContainer=endContainer=body and startOffset=endOffset=1 - var body = context.$iframe[0].contentWindow.document.body; - if ( $.browser.opera && e == body && offset == 1 ) { - return null; - } - } - if ( !e && context.$iframe[0].contentWindow.document.selection ) { - // IE - // Because there's nothing like range.startContainer in IE, we need to do a DOM traversal - // to find the element the start of the selection is in - var range = context.$iframe[0].contentWindow.document.selection.createRange(); - // Set range2 to the text before the selection - var range2 = context.$iframe[0].contentWindow.document.body.createTextRange(); - // For some reason this call throws errors in certain cases, e.g. when the selection is - // not in the iframe - try { - range2.setEndPoint( 'EndToStart', range ); - } catch ( ex ) { - return null; - } - var seekPos = context.fn.htmlToText( range2.htmlText ).length; - var offset = context.fn.getOffset( seekPos ); - e = offset ? offset.node : null; - offset = offset ? offset.offset : null; - if ( !e ) { - return null; - } - } - if ( e.nodeName != '#text' ) { - // The selection is not in a textnode, but between two non-text nodes - // (usually inside the between two
s). Go to the rightmost - // child of the node just before the selection - var newE = e.firstChild; - for ( var i = 0; i < offset - 1 && newE; i++ ) { - newE = newE.nextSibling; - } - while ( newE && newE.lastChild ) { - newE = newE.lastChild; - } - e = newE || e; - } - - // We'd normally use if( $( e ).hasClass( class ) in the while loop, but running the jQuery - // constructor thousands of times is very inefficient - var classStr = ' ' + classname + ' '; - while ( e ) { - if ( !strict && ( !classname || ( ' ' + e.className + ' ' ).indexOf( classStr ) != -1 ) ) { - return $( e ); - } - var next = e.previousSibling; - while ( next && next.lastChild ) { - next = next.lastChild; - } - e = next || e.parentNode; - strict = false; - } - return $( [] ); - }, - /** - * Object used by traverser(). Don't use this unless you know what you're doing - */ - 'rawTraverser': function( node, inP, ancestor, skipNoinclude ) { - this.node = node; - this.inP = inP; - this.ancestor = ancestor; - this.skipNoinclude = skipNoinclude; - this.next = function() { - var p = this.node; - var nextInP = this.inP; - while ( p && !p.nextSibling ) { - p = p.parentNode; - if ( p == this.ancestor ) { - // We're back at the ancestor, stop here - p = null; - } - if ( p && p.nodeName == "P" ) { - nextInP = null; - } - } - p = p ? p.nextSibling : null; - if ( p && p.nodeName == "P" ) { - nextInP = p; - } - do { - // Filter nodes with the wikiEditor-noinclude class - // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because - // $() is slow in a tight loop - if ( this.skipNoinclude ) { - while ( p && ( ' ' + p.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) { - p = p.nextSibling; - } - } - if ( p && p.firstChild ) { - p = p.firstChild; - if ( p.nodeName == "P" ) { - nextInP = p; - } - } - } while ( p && p.firstChild ); - // Instead of calling the rawTraverser constructor, inline it. This avoids function call overhead - return p ? { 'node': p, 'inP': nextInP, 'ancestor': this.ancestor, - 'skipNoinclude': this.skipNoinclude, 'next': this.next, 'prev': this.prev } : null; - }; - this.prev = function() { - var p = this.node; - var prevInP = this.inP; - while ( p && !p.previousSibling ) { - p = p.parentNode; - if ( p == this.ancestor ) { - // We're back at the ancestor, stop here - p = null; - } - if ( p && p.nodeName == "P" ) { - prevInP = null; - } - } - p = p ? p.previousSibling : null; - if ( p && p.nodeName == "P" ) { - prevInP = p; - } - do { - // Filter nodes with the wikiEditor-noinclude class - // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because - // $() is slow in a tight loop - if ( this.skipNoinclude ) { - while ( p && ( ' ' + p.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) { - p = p.previousSibling; - } - } - if ( p && p.lastChild ) { - p = p.lastChild; - if ( p.nodeName == "P" ) { - prevInP = p; - } - } - } while ( p && p.lastChild ); - // Instead of calling the rawTraverser constructor, inline it. This avoids function call overhead - return p ? { 'node': p, 'inP': prevInP, 'ancestor': this.ancestor, - 'skipNoinclude': this.skipNoinclude, 'next': this.next, 'prev': this.prev } : null; - }; - }, - /** - * Get an object used to traverse the leaf nodes in the iframe DOM. This traversal skips leaf nodes - * inside an element with the wikiEditor-noinclude class. This basically wraps rawTraverser - * - * @param start Node to start at - * @return Traverser object, use .next() or .prev() to get a traverser object referring to the - * previous/next node - */ - 'traverser': function( start ) { - // Find the leftmost leaf node in the tree - var startNode = start.jquery ? start.get( 0 ) : start; - var node = startNode; - var inP = node.nodeName == "P" ? node : null; - do { - // Filter nodes with the wikiEditor-noinclude class - // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because - // $() is slow in a tight loop - while ( node && ( ' ' + node.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) { - node = node.nextSibling; - } - if ( node && node.firstChild ) { - node = node.firstChild; - if ( node.nodeName == "P" ) { - inP = node; - } - } - } while ( node && node.firstChild ); - return new context.fn.rawTraverser( node, inP, startNode, true ); - }, - 'getOffset': function( offset ) { - if ( !context.offsets ) { - context.fn.refreshOffsets(); - } - if ( offset in context.offsets ) { - return context.offsets[offset]; - } - // Our offset is not pre-cached. Find the highest offset below it and interpolate - // We need to traverse the entire object because for() doesn't traverse in order - // We don't do in-order traversal because the object is sparse - var lowerBound = -1; - for ( var o in context.offsets ) { - var realO = parseInt( o ); - if ( realO < offset && realO > lowerBound) { - lowerBound = realO; - } - } - if ( !( lowerBound in context.offsets ) ) { - // Weird edge case: either offset is too large or the document is empty - return null; - } - var base = context.offsets[lowerBound]; - return context.offsets[offset] = { - 'node': base.node, - 'offset': base.offset + offset - lowerBound, - 'length': base.length, - 'lastTextNode': base.lastTextNode - }; - }, - 'purgeOffsets': function() { - context.offsets = null; - }, - 'refreshOffsets': function() { - context.offsets = [ ]; - var t = context.fn.traverser( context.$content ); - var pos = 0, lastTextNode = null; - while ( t ) { - if ( t.node.nodeName != '#text' && t.node.nodeName != 'BR' ) { - t = t.next(); - continue; - } - var nextPos = t.node.nodeName == '#text' ? pos + t.node.nodeValue.length : pos + 1; - var nextT = t.next(); - var leavingP = t.node.nodeName == '#text' && t.inP && nextT && ( !nextT.inP || nextT.inP != t.inP ); - context.offsets[pos] = { - 'node': t.node, - 'offset': 0, - 'length': nextPos - pos + ( leavingP ? 1 : 0 ), - 'lastTextNode': lastTextNode - }; - if ( leavingP ) { - //

Foo

looks like "Foo\n", make it quack like it too - // Basically we're faking the \n character much like we're treating
s - context.offsets[nextPos] = { - 'node': t.node, - 'offset': nextPos - pos, - 'length': nextPos - pos + 1, - 'lastTextNode': lastTextNode - }; - } - pos = nextPos + ( leavingP ? 1 : 0 ); - if ( t.node.nodeName == '#text' ) { - lastTextNode = t.node; - } - t = nextT; - } - }, - 'saveSelection': function() { - if ( !$.browser.msie ) { - // Only IE needs this - return; - } - if ( typeof context.$iframe != 'undefined' ) { - context.$iframe[0].contentWindow.focus(); - context.savedSelection = context.$iframe[0].contentWindow.document.selection.createRange(); - } else { - context.$textarea.focus(); - context.savedSelection = document.selection.createRange(); - } - }, - 'restoreSelection': function() { - if ( !$.browser.msie || context.savedSelection === null ) { - return; - } - if ( typeof context.$iframe != 'undefined' ) { - context.$iframe[0].contentWindow.focus(); - } else { - context.$textarea.focus(); - } - context.savedSelection.select(); - context.savedSelection = null; - }, - /** - * Update the history queue - * - * @param htmlChange pass true or false to inidicate if there was a text change that should potentially - * be given a new history state. - */ - 'updateHistory': function( htmlChange ) { - var newHTML = context.$content.html(); - var newSel = context.fn.getCaretPosition(); - // Was text changed? Was it because of a REDO or UNDO action? - if ( - context.history.length == 0 || - ( htmlChange && context.oldDelayedHistoryPosition == context.historyPosition ) - ) { - context.oldDelayedSel = newSel; - // Do we need to trim extras from our history? - // FIXME: this should really be happing on change, not on the delay - if ( context.historyPosition < -1 ) { - //clear out the extras - context.history.splice( context.history.length + context.historyPosition + 1 ); - context.historyPosition = -1; - } - context.history.push( { 'html': newHTML, 'sel': newSel } ); - // If the history has grown longer than 10 items, remove the earliest one - while ( context.history.length > 10 ) { - context.history.shift(); - } - } else if ( context.oldDelayedSel != newSel ) { - // If only the selection was changed, update it - context.oldDelayedSel = newSel; - context.history[context.history.length + context.historyPosition].sel = newSel; - } - // synch our old delayed history position until the next undo/redo action - context.oldDelayedHistoryPosition = context.historyPosition; - }, - /** - * Sets up the iframe in place of the textarea to allow more advanced operations - */ - 'setupIframe': function() { - context.$iframe = $( '' ) - .attr( { - 'frameBorder': 0, - 'border': 0, - 'tabindex': 1, - 'src': wgScriptPath + '/extensions/WikiEditor/modules/jquery.wikiEditor.html?' + - 'instance=' + context.instance + '&ts=' + ( new Date() ).getTime() + '&is=content', - 'id': 'wikiEditor-iframe-' + context.instance - } ) - .css( { - 'backgroundColor': 'white', - 'width': '100%', - 'height': context.$textarea.height(), - 'display': 'none', - 'overflow-y': 'scroll', - 'overflow-x': 'hidden' - } ) - .insertAfter( context.$textarea ) - .load( function() { - // Internet Explorer will reload the iframe once we turn on design mode, so we need to only turn it - // on during the first run, and then bail - if ( !this.isSecondRun ) { - // Turn the document's design mode on - context.$iframe[0].contentWindow.document.designMode = 'on'; - // Let the rest of this function happen next time around - if ( $.browser.msie ) { - this.isSecondRun = true; - return; - } - } - // Get a reference to the content area of the iframe - context.$content = $( context.$iframe[0].contentWindow.document.body ); - // Add classes to the body to influence the styles based on what's enabled - for ( module in context.modules ) { - context.$content.addClass( 'wikiEditor-' + module ); - } - // If we just do "context.$content.text( context.$textarea.val() )", Internet Explorer will strip - // out the whitespace charcters, specifically "\n" - so we must manually encode text and append it - // TODO: Refactor this into a textToHtml() function - var html = context.$textarea.val() - // We're gonna use &esc; as an escape sequence - .replace( /&esc;/g, '&esc;esc;' ) - // Escape existing uses of

,

,   and - .replace( /\/g, '&esc;<p>' ) - .replace( /\<\/p\>/g, '&esc;</p>' ) - .replace( - /\\<\/span\>/g, - '&esc;<span class="wikiEditor-tab"></span>' - ) - .replace( / /g, '&esc;&nbsp;' ); - // We must do some extra processing on IE to avoid dirty diffs, specifically IE will collapse - // leading spaces - browser sniffing is not ideal, but executing this code on a non-broken browser - // doesn't cause harm - if ( $.browser.msie ) { - html = html.replace( /\t/g, '' ); - if ( $.browser.versionNumber <= 7 ) { - // Replace all spaces matching   - IE <= 7 needs this because of its overzealous - // whitespace collapsing - html = html.replace( / /g, " " ); - } else { - // IE8 is happy if we just convert the first leading space to   - html = html.replace( /(^|\n) /g, "$1 " ); - } - } - // Use a dummy div to escape all entities - // This'll also escape
, and   , so we unescape those after - // We also need to unescape the doubly-escaped things mentioned above - html = $( '
' ).text( '

' + html.replace( /\r?\n/g, '

' ) + '

' ).html() - .replace( /&nbsp;/g, ' ' ) - // Allow

tags to survive encoding - .replace( /<p>/g, '

' ) - .replace( /<\/p>/g, '

' ) - // And too - .replace( - /<span( | )class=("|")wikiEditor-tab("|")><\/span>/g, - '' - ) - // Empty

tags need
tags in them - .replace( /

<\/p>/g, '


' ) - // Unescape &esc; stuff - .replace( /&esc;&amp;nbsp;/g, '&nbsp;' ) - .replace( /&esc;&lt;p&gt;/g, '<p>' ) - .replace( /&esc;&lt;\/p&gt;/g, '</p>' ) - .replace( - /&esc;&lt;span&nbsp;class=&quot;wikiEditor-tab&quot;&gt;&lt;\/span&gt;/g, - '<span class="wikiEditor-tab"><\/span>' - ) - .replace( /&esc;esc;/g, '&esc;' ); - context.$content.html( html ); - - // Reflect direction of parent frame into child - if ( $( 'body' ).is( '.rtl' ) ) { - context.$content.addClass( 'rtl' ).attr( 'dir', 'rtl' ); - } - // Activate the iframe, encoding the content of the textarea and copying it to the content of iframe - context.$textarea.attr( 'disabled', true ); - context.$textarea.hide(); - context.$iframe.show(); - // Let modules know we're ready to start working with the content - context.fn.trigger( 'ready' ); - // Only save HTML now: ready handlers may have modified it - context.oldHTML = context.oldDelayedHTML = context.$content.html(); - //remove our temporary loading - /* Disaling our loading div for now - $( '.wikiEditor-ui-loading' ).fadeOut( 'fast', function() { - $( this ).remove(); - } ); - */ - // Setup event handling on the iframe - $( context.$iframe[0].contentWindow.document ) - .bind( 'keydown', function( event ) { - event.jQueryNode = context.fn.getElementAtCursor(); - return context.fn.trigger( 'keydown', event ); - - } ) - .bind( 'keyup', function( event ) { - event.jQueryNode = context.fn.getElementAtCursor(); - return context.fn.trigger( 'keyup', event ); - } ) - .bind( 'keypress', function( event ) { - event.jQueryNode = context.fn.getElementAtCursor(); - return context.fn.trigger( 'keypress', event ); - } ) - .bind( 'paste', function( event ) { - return context.fn.trigger( 'paste', event ); - } ) - .bind( 'cut', function( event ) { - return context.fn.trigger( 'cut', event ); - } ) - .bind( 'keyup paste mouseup cut encapsulateSelection', function( event ) { - return context.fn.trigger( 'change', event ); - } ) - .delayedBind( 250, 'keyup paste mouseup cut encapsulateSelection', function( event ) { - context.fn.trigger( 'delayedChange', event ); - } ); - } ); - // Attach a submit handler to the form so that when the form is submitted the content of the iframe gets - // decoded and copied over to the textarea - context.$textarea.closest( 'form' ).submit( function() { - context.$textarea.attr( 'disabled', false ); - context.$textarea.val( context.$textarea.textSelection( 'getContents' ) ); - } ); - /* FIXME: This was taken from EditWarning.js - maybe we could do a jquery plugin for this? */ - // Attach our own handler for onbeforeunload which respects the current one - context.fallbackWindowOnBeforeUnload = window.onbeforeunload; - window.onbeforeunload = function() { - context.$textarea.val( context.$textarea.textSelection( 'getContents' ) ); - if ( context.fallbackWindowOnBeforeUnload ) { - return context.fallbackWindowOnBeforeUnload(); - } - }; - }, - - /* - * 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() { - if ( context.$iframe[0].contentWindow.getSelection ) { - // Firefox and Opera - var selection = context.$iframe[0].contentWindow.getSelection(); - if ( selection.rangeCount == 0 ) { - // We don't know where the cursor is - return $( [] ); - } - var sc = selection.getRangeAt( 0 ).startContainer; - if ( sc.nodeName == "#text" ) sc = sc.parentNode; - return $( sc ); - } else if ( context.$iframe[0].contentWindow.document.selection ) { // should come last; Opera! - // IE - var selection = context.$iframe[0].contentWindow.document.selection.createRange(); - return $( selection.parentElement() ); - } - }, - - /** - * Gets the complete contents of the iframe (in plain text, not HTML) - */ - 'getContents': function() { - // For

, .html() returns

 

in IE - // This seems to convince IE while not affecting display - if ( !context.$content ) { - return ''; - } - var html; - if ( $.browser.msie ) { - // Don't manipulate the iframe DOM itself, causes cursor jumping issues - var $c = $( context.$content.get( 0 ).cloneNode( true ) ); - $c.find( 'p' ).each( function() { - if ( $(this).html() == '' ) { - $(this).replaceWith( '

' ); - } - } ); - html = $c.html(); - } else { - html = context.$content.html(); - } - return context.fn.htmlToText( html ); - }, - /** - * Gets the currently selected text in the content - * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead - */ - 'getSelection': function() { - var retval; - if ( context.$iframe[0].contentWindow.getSelection ) { - // Firefox and Opera - retval = context.$iframe[0].contentWindow.getSelection(); - if ( $.browser.opera ) { - // Opera strips newlines in getSelection(), so we need something more sophisticated - if ( retval.rangeCount > 0 ) { - retval = context.fn.htmlToText( $( '
' )
-								.append( retval.getRangeAt( 0 ).cloneContents() )
-								.html()
-						);
-					} else {
-						retval = '';
-					}
-				}
-			} else if ( context.$iframe[0].contentWindow.document.selection ) { // should come last; Opera!
-				// IE
-				retval = context.$iframe[0].contentWindow.document.selection.createRange();
-			}
-			if ( typeof retval.text != 'undefined' ) {
-				// In IE8, retval.text is stripped of newlines, so we need to process retval.htmlText
-				// to get a reliable answer. IE7 does get this right though
-				// Run this fix for all IE versions anyway, it doesn't hurt
-				retval = context.fn.htmlToText( retval.htmlText );
-			} else if ( typeof retval.toString != 'undefined' ) {
-				retval = retval.toString();
-			}
-			return retval;
-		},
-		/**
-		 * 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 selText = $(this).textSelection( 'getSelection' );
-			var selTextArr;
-			var collapseToEnd = false;
-			var selectAfter = false;
-			var setSelectionTo = null;
-			var pre = options.pre, post = options.post;
-			if ( !selText ) {
-				selText = options.peri;
-				selectAfter = true;
-			} else if ( options.peri == selText.replace( /\s+$/, '' ) ) {
-				// Probably a successive button press
-				// strip any extra white space from selText
-				selText = selText.replace( /\s+$/, '' );
-				// set the collapseToEnd flag to ensure our selection is collapsed to the end before any insertion is done
-				collapseToEnd = true;
-				// set selectAfter to true since we know we'll be populating with our default text
-				selectAfter = true;
-			} else if ( options.replace ) {
-				selText = options.peri;
-			} else if ( selText.charAt( selText.length - 1 ) == ' ' ) {
-				// Exclude ending space char
-				// FIXME: Why?
-				selText = selText.substring( 0, selText.length - 1 );
-				post += ' ';
-			}
-			if ( options.splitlines ) {
-				selTextArr = selText.split( /\n/ );
-			}
-
-			if ( context.$iframe[0].contentWindow.getSelection ) {
-				// Firefox and Opera
-				var range = context.$iframe[0].contentWindow.getSelection().getRangeAt( 0 );
-				// if our test above indicated that this was a sucessive button press, we need to collapse the 
-				// selection to the end to avoid replacing text 
-				if ( collapseToEnd ) {
-					// Make sure we're not collapsing ourselves into a BR tag
-					if ( range.endContainer.nodeName == 'BR' ) {
-						range.setEndBefore( range.endContainer );
-					}
-					range.collapse( false );
-				}
-				if ( options.ownline ) {
-					// We need to figure out if the cursor is at the start or end of a line
-					var atStart = false, atEnd = false;
-					var body = context.$content.get( 0 );
-					if ( range.startOffset == 0 ) {
-						// Start of a line
-						// FIXME: Not necessarily the case with syntax highlighting or
-						// template collapsing
-						atStart = true;
-					} else if ( range.startContainer == body ) {
-						// Look up the node just before the start of the selection
-						// If it's a 
, we're at the start of a line that starts with a - // block element; if not, we're at the end of a line - var n = body.firstChild; - for ( var i = 0; i < range.startOffset - 1 && n; i++ ) { - n = n.nextSibling; - } - if ( n && n.nodeName == 'BR' ) { - atStart = true; - } else { - atEnd = true; - } - } - if ( ( range.endOffset == 0 && range.endContainer.nodeValue == null ) || - ( range.endContainer.nodeName == '#text' && - range.endOffset == range.endContainer.nodeValue.length ) || - ( range.endContainer.nodeName == 'P' && range.endContainer.nodeValue == null ) ) { - atEnd = true; - } - if ( !atStart ) { - pre = "\n" + options.pre; - } - if ( !atEnd ) { - post += "\n"; - } - } - var insertText = ""; - if ( options.splitlines ) { - for( var j = 0; j < selTextArr.length; j++ ) { - insertText = insertText + pre + selTextArr[j] + post; - if( j != selTextArr.length - 1 ) { - insertText += "\n"; - } - } - } else { - insertText = pre + selText + post; - } - var insertLines = insertText.split( "\n" ); - range.extractContents(); - // Insert the contents one line at a time - insertNode() inserts at the beginning, so this has to happen - // in reverse order - // Track the first and last inserted node, and if we need to also track where the text we need to select - // afterwards starts and ends - var firstNode = null, lastNode = null; - var selSC = null, selEC = null, selSO = null, selEO = null, offset = 0; - for ( var i = insertLines.length - 1; i >= 0; i-- ) { - firstNode = context.$iframe[0].contentWindow.document.createTextNode( insertLines[i] ); - range.insertNode( firstNode ); - lastNode = lastNode || firstNode; - var newOffset = offset + insertLines[i].length; - if ( !selEC && post.length <= newOffset ) { - selEC = firstNode; - selEO = selEC.nodeValue.length - ( post.length - offset ); - } - if ( selEC && !selSC && pre.length >= insertText.length - newOffset ) { - selSC = firstNode; - selSO = pre.length - ( insertText.length - newOffset ); - } - offset = newOffset; - if ( i > 0 ) { - firstNode = context.$iframe[0].contentWindow.document.createElement( 'br' ); - range.insertNode( firstNode ); - newOffset = offset + 1; - if ( !selEC && post.length <= newOffset ) { - selEC = firstNode; - selEO = 1 - ( post.length - offset ); - } - if ( selEC && !selSC && pre.length >= insertText.length - newOffset ) { - selSC = firstNode; - selSO = pre.length - ( insertText.length - newOffset ); - } - offset = newOffset; - } - } - if ( firstNode ) { - context.fn.scrollToTop( $( firstNode.parentNode ) ); - } - if ( selectAfter ) { - setSelectionTo = { - startContainer: selSC, - endContainer: selEC, - start: selSO, - end: selEO - }; - } else if ( lastNode ) { - setSelectionTo = { - startContainer: lastNode, - endContainer: lastNode, - start: lastNode.nodeValue.length, - end: lastNode.nodeValue.length - }; - } - } else if ( context.$iframe[0].contentWindow.document.selection ) { - // IE - context.$iframe[0].contentWindow.focus(); - var range = context.$iframe[0].contentWindow.document.selection.createRange(); - if ( options.ownline && range.moveStart ) { - // Check if we're at the start of a line - // If not, prepend a newline - var range2 = context.$iframe[0].contentWindow.document.selection.createRange(); - range2.collapse(); - range2.moveStart( 'character', -1 ); - // FIXME: Which check is correct? - if ( range2.text != "\r" && range2.text != "\n" && range2.text != "" ) { - pre = "\n" + pre; - } - - // Check if we're at the end of a line - // If not, append a newline - var range3 = context.$iframe[0].contentWindow.document.selection.createRange(); - range3.collapse( false ); - range3.moveEnd( 'character', 1 ); - if ( range3.text != "\r" && range3.text != "\n" && range3.text != "" ) { - post += "\n"; - } - } - // if our test above indicated that this was a sucessive button press, we need to collapse the - // selection to the end to avoid replacing text - if ( collapseToEnd ) { - range.collapse( false ); - } - // TODO: Clean this up. Duplicate code due to the pre-existing browser specific structure of this - // function - var insertText = ""; - if ( options.splitlines ) { - for( var j = 0; j < selTextArr.length; j++ ) { - insertText = insertText + pre + selTextArr[j] + post; - if( j != selTextArr.length - 1 ) { - insertText += "\n"; - } - } - } else { - insertText = pre + selText + post; - } - // TODO: Maybe find a more elegant way of doing this like the Firefox code above? - range.pasteHTML( insertText - .replace( /\/g, '>' ) - .replace( /\r?\n/g, '
' ) - ); - if ( selectAfter ) { - range.moveStart( 'character', -post.length - selText.length ); - range.moveEnd( 'character', -post.length ); - range.select(); - } - } - - if ( setSelectionTo ) { - context.fn.setSelection( setSelectionTo ); - } - // Trigger the encapsulateSelection event (this might need to get named something else/done differently) - $( context.$iframe[0].contentWindow.document ).trigger( - 'encapsulateSelection', [ pre, options.peri, post, options.ownline, options.replace ] - ); - 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 ) { - var startPos = null, endPos = null; - if ( context.$iframe[0].contentWindow.getSelection ) { - var selection = context.$iframe[0].contentWindow.getSelection(); - if ( selection.rangeCount == 0 ) { - // We don't know where the cursor is - return [ 0, 0 ]; - } - var sc = selection.getRangeAt( 0 ).startContainer, ec = selection.getRangeAt( 0 ).endContainer; - var so = selection.getRangeAt( 0 ).startOffset, eo = selection.getRangeAt( 0 ).endOffset; - if ( sc.nodeName == 'BODY' ) { - // Grab the node just before the start of the selection - var n = sc.firstChild; - for ( var i = 0; i < so - 1 && n; i++ ) { - n = n.nextSibling; - } - sc = n; - so = 0; - } - if ( ec.nodeName == 'BODY' ) { - var n = ec.firstChild; - for ( var i = 0; i < eo - 1 && n; i++ ) { - n = n.nextSibling; - } - ec = n; - eo = 0; - } - - // Make sure sc and ec are leaf nodes - while ( sc.firstChild ) { - sc = sc.firstChild; - } - while ( ec.firstChild ) { - ec = ec.firstChild; - } - // Make sure the offsets are regenerated if necessary - context.fn.getOffset( 0 ); - var o; - for ( o in context.offsets ) { - if ( startPos === null && context.offsets[o].node == sc ) { - // For some wicked reason o is a string, even though - // we put it in as an integer. Use ~~ to coerce it too an int - startPos = ~~o + so - context.offsets[o].offset; - } - if ( startPos !== null && context.offsets[o].node == ec ) { - endPos = ~~o + eo - context.offsets[o].offset; - break; - } - } - } else if ( context.$iframe[0].contentWindow.document.selection ) { - // IE - // FIXME: This is mostly copypasted from the textSelection plugin - var d = context.$iframe[0].contentWindow.document; - var postFinished = false; - var periFinished = false; - var postFinished = false; - var preText, rawPreText, periText; - var rawPeriText, postText, rawPostText; - // Depending on the document state, and if the cursor has ever been manually placed within the document - // the following call such as setEndPoint can result in nasty errors. These cases are always cases - // in which the start and end points can safely be assumed to be 0, so we will just try our best to do - // the full process but fall back to 0. - try { - // Create range containing text in the selection - var periRange = d.selection.createRange().duplicate(); - // Create range containing text before the selection - var preRange = d.body.createTextRange(); - // Move the end where we need it - preRange.setEndPoint( "EndToStart", periRange ); - // Create range containing text after the selection - var postRange = d.body.createTextRange(); - // Move the start where we need it - postRange.setEndPoint( "StartToEnd", periRange ); - // Load the text values we need to compare - preText = rawPreText = preRange.text; - periText = rawPeriText = periRange.text; - postText = rawPostText = postRange.text; - /* - * Check each range for trimmed newlines by shrinking the range by 1 - * character and seeing if the text property has changed. If it has - * not changed then we know that IE has trimmed a \r\n from the end. - */ - do { - if ( !postFinished ) { - if ( preRange.compareEndPoints( "StartToEnd", preRange ) == 0 ) { - postFinished = true; - } else { - preRange.moveEnd( "character", -1 ) - if ( preRange.text == preText ) { - rawPreText += "\r\n"; - } else { - postFinished = true; - } - } - } - if ( !periFinished ) { - if ( periRange.compareEndPoints( "StartToEnd", periRange ) == 0 ) { - periFinished = true; - } else { - periRange.moveEnd( "character", -1 ) - if ( periRange.text == periText ) { - rawPeriText += "\r\n"; - } else { - periFinished = true; - } - } - } - if ( !postFinished ) { - if ( postRange.compareEndPoints("StartToEnd", postRange) == 0 ) { - postFinished = true; - } else { - postRange.moveEnd( "character", -1 ) - if ( postRange.text == postText ) { - rawPostText += "\r\n"; - } else { - postFinished = true; - } - } - } - } while ( ( !postFinished || !periFinished || !postFinished ) ); - startPos = rawPreText.replace( /\r\n/g, "\n" ).length; - endPos = startPos + rawPeriText.replace( /\r\n/g, "\n" ).length; - } catch( e ) { - startPos = endPos = 0; - } - } - return [ startPos, endPos ]; - }, - /** - * 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 sc = options.startContainer, ec = options.endContainer; - sc = sc && sc.jquery ? sc[0] : sc; - ec = ec && ec.jquery ? ec[0] : ec; - if ( context.$iframe[0].contentWindow.getSelection ) { - // Firefox and Opera - var start = options.start, end = options.end; - if ( !sc || !ec ) { - var s = context.fn.getOffset( start ); - var e = context.fn.getOffset( end ); - sc = s ? s.node : null; - ec = e ? e.node : null; - start = s ? s.offset : null; - end = e ? e.offset : null; - // Don't try to set the selection past the end of a node, causes errors - // Just put the selection at the end of the node in this case - if ( sc != null && sc.nodeName == '#text' && start > sc.nodeValue.length ) { - start = sc.nodeValue.length - 1; - } - if ( ec != null && ec.nodeName == '#text' && end > ec.nodeValue.length ) { - end = ec.nodeValue.length - 1; - } - } - if ( !sc || !ec ) { - // The requested offset isn't in the offsets array - // Give up - return context.$textarea; - } - - var sel = context.$iframe[0].contentWindow.getSelection(); - while ( sc.firstChild && sc.nodeName != '#text' ) { - sc = sc.firstChild; - } - while ( ec.firstChild && ec.nodeName != '#text' ) { - ec = ec.firstChild; - } - var range = context.$iframe[0].contentWindow.document.createRange(); - range.setStart( sc, start ); - range.setEnd( ec, end ); - sel.removeAllRanges(); - sel.addRange( range ); - context.$iframe[0].contentWindow.focus(); - } else if ( context.$iframe[0].contentWindow.document.body.createTextRange ) { - // IE - var range = context.$iframe[0].contentWindow.document.body.createTextRange(); - if ( sc ) { - range.moveToElementText( sc ); - } - range.collapse(); - range.moveEnd( 'character', options.start ); - - var range2 = context.$iframe[0].contentWindow.document.body.createTextRange(); - if ( ec ) { - range2.moveToElementText( ec ); - } - range2.collapse(); - range2.moveEnd( 'character', options.end ); - - // IE does newline emulation for

s:

foo

bar

becomes foo\nbar just fine - // but

foo



bar

becomes foo\n\n\n\nbar , one \n too many - // Correct for this - var matches, counted = 0; - // while ( matches = range.htmlText.match( regex ) && matches.length <= counted ) doesn't work - // because the assignment side effect hasn't happened yet when the second term is evaluated - while ( matches = range.htmlText.match( /\<\/p\>(\]*\>)+\/gi ) ) { - if ( matches.length <= counted ) - break; - range.moveEnd( 'character', matches.length ); - counted += matches.length; - } - range2.moveEnd( 'character', counted ); - while ( matches = range2.htmlText.match( /\<\/p\>(\]*\>)+\/gi ) ) { - if ( matches.length <= counted ) - break; - range2.moveEnd( 'character', matches.length ); - counted += matches.length; - } - - range2.setEndPoint( 'StartToEnd', range ); - range2.select(); - } - 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 ) { - context.fn.scrollToTop( context.fn.getElementAtCursor(), true ); - }, - /** - * 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 ) { - var html = context.$content.closest( 'html' ), - body = context.$content.closest( 'body' ), - parentHtml = $( 'html' ), - parentBody = $( 'body' ); - var y = $element.offset().top; - if ( !$.browser.msie && ! $element.is( 'body' ) ) { - y = parentHtml.scrollTop() > 0 ? y + html.scrollTop() - parentHtml.scrollTop() : y; - y = parentBody.scrollTop() > 0 ? y + body.scrollTop() - parentBody.scrollTop() : y; - } - var topBound = html.scrollTop() > body.scrollTop() ? html.scrollTop() : body.scrollTop(), - bottomBound = topBound + context.$iframe.height(); - if ( force || y < topBound || y > bottomBound ) { - html.scrollTop( y ); - body.scrollTop( y ); - } - $element.trigger( 'scrollToTop' ); - }, /** * Save scrollTop and cursor position for IE. */ @@ -1862,19 +572,26 @@ if ( !context || typeof context == 'undefined' ) { // Since javascript gives arguments as an object, we need to convert them so they can be used more easily var args = $.makeArray( arguments ); -// Dynamically setup the Iframe when needed when adding modules -if ( typeof context.$iframe === 'undefined' && args[0] == 'addModule' && typeof args[1] != 'undefined' ) { +// Dynamically setup core extensions for modules that are required +if ( args[0] == 'addModule' && typeof args[1] != 'undefined' ) { var modules = args[1]; if ( typeof modules != "object" ) { modules = {}; modules[args[1]] = ''; } for ( module in modules ) { - // Only allow modules which are supported (and thus actually being turned on) affect this decision - if ( module in $.wikiEditor.modules && $.wikiEditor.isSupported( $.wikiEditor.modules[module] ) && - $.wikiEditor.isRequired( $.wikiEditor.modules[module], 'iframe' ) ) { - - context.fn.setupIframe(); + // Only allow modules which are supported (and thus actually being turned on) affect the decision to extend + if ( module in $.wikiEditor.modules && $.wikiEditor.isSupported( $.wikiEditor.modules[module] ) ) { + // Activate all required core extensions on context + for ( e in $.wikiEditor.extensions ) { + if ( + $.wikiEditor.isRequired( $.wikiEditor.modules[module], e ) && + context.extensions.indexOf( e ) === -1 + ) { + context.extensions[context.extensions.length] = e; + $.wikiEditor.extensions[e]( context ); + } + } break; } }