r74271 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r74270‎ | r74271 | r74272 >
Date:20:12, 4 October 2010
Author:tparscal
Status:resolved (Comments)
Tags:
Comment:
* 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.
Modified paths:
  • /trunk/extensions/WikiEditor/WikiEditor.hooks.php (modified) (history)
  • /trunk/extensions/WikiEditor/modules/jquery.wikiEditor.iframe.js (added) (history)
  • /trunk/extensions/WikiEditor/modules/jquery.wikiEditor.js (modified) (history)

Diff [purge]

Index: trunk/extensions/WikiEditor/WikiEditor.hooks.php
@@ -36,6 +36,11 @@
3737 ),
3838 'group' => 'ext.wikiEditor',
3939 ),
 40+ 'jquery.wikiEditor.iframe' => array(
 41+ 'scripts' => 'extensions/WikiEditor/modules/jquery.wikiEditor.iframe.js',
 42+ 'dependencies' => 'jquery.wikiEditor',
 43+ 'group' => 'ext.wikiEditor',
 44+ ),
4045 'jquery.wikiEditor.dialogs' => array(
4146 'scripts' => 'extensions/WikiEditor/modules/jquery.wikiEditor.dialogs.js',
4247 'styles' => 'extensions/WikiEditor/modules/jquery.wikiEditor.dialogs.css',
@@ -52,7 +57,10 @@
5358 ),
5459 'jquery.wikiEditor.highlight' => array(
5560 'scripts' => 'extensions/WikiEditor/modules/jquery.wikiEditor.highlight.js',
56 - 'dependencies' => 'jquery.wikiEditor',
 61+ 'dependencies' => array(
 62+ 'jquery.wikiEditor',
 63+ 'jquery.wikiEditor.iframe',
 64+ ),
5765 'group' => 'ext.wikiEditor',
5866 ),
5967 'jquery.wikiEditor.preview' => array(
@@ -82,13 +90,17 @@
8391 'scripts' => 'extensions/WikiEditor/modules/jquery.wikiEditor.templateEditor.js',
8492 'dependencies' => array(
8593 'jquery.wikiEditor',
86 - 'jquery.wikiEditor.dialogs',
 94+ 'jquery.wikiEditor.iframe',
 95+ 'jquery.wikiEditor.dialogs',
8796 ),
8897 'group' => 'ext.wikiEditor',
8998 ),
9099 'jquery.wikiEditor.templates' => array(
91100 'scripts' => 'extensions/WikiEditor/modules/jquery.wikiEditor.templates.js',
92 - 'dependencies' => 'jquery.wikiEditor',
 101+ 'dependencies' => array(
 102+ 'jquery.wikiEditor',
 103+ 'jquery.wikiEditor.iframe',
 104+ ),
93105 'group' => 'ext.wikiEditor',
94106 ),
95107 'jquery.wikiEditor.toc' => array(
@@ -96,6 +108,7 @@
97109 'styles' => 'extensions/WikiEditor/modules/jquery.wikiEditor.toc.css',
98110 'dependencies' => array(
99111 'jquery.wikiEditor',
 112+ 'jquery.wikiEditor.iframe',
100113 'jquery.ui.draggable',
101114 'jquery.ui.resizable',
102115 'jquery.autoEllipsis',
Index: trunk/extensions/WikiEditor/modules/jquery.wikiEditor.js
@@ -21,6 +21,10 @@
2222 */
2323 'modules': {},
2424 /**
 25+ * A context can be extended, such as adding iframe support, on a per-wikiEditor instance basis.
 26+ */
 27+ 'extensions': {},
 28+ /**
2529 * In some cases like with the iframe's HTML file, it's convienent to have a lookup table of all instances of the
2630 * WikiEditor. Each context contains an instance field which contains a key that corrosponds to a reference to the
2731 * textarea which the WikiEditor was build around. This way, by passing a simple integer you can provide a way back
@@ -239,7 +243,9 @@
240244 // Current history state position - this is number of steps backwards, so it's always -1 or less
241245 'historyPosition': -1,
242246 /// The previous historyPosition, stored to detect if change events were due to an undo or redo action
243 - 'oldDelayedHistoryPosition': -1
 247+ 'oldDelayedHistoryPosition': -1,
 248+ // List of extensions active on this context
 249+ 'extensions': [],
244250 };
245251
246252 /*
@@ -361,201 +367,13 @@
362368 }
363369 break;
364370 case 86: //v
365 - if ( event.ctrlKey && $.browser.msie ) {
 371+ if ( event.ctrlKey && $.browser.msie && 'paste' in context.evt ) {
366372 //paste, intercepted for IE
367373 context.evt.paste( event );
368374 }
369375 break;
370376 }
371377 return true;
372 - },
373 - 'change': function( event ) {
374 - event.data.scope = 'division';
375 - var newHTML = context.$content.html();
376 - if ( context.oldHTML != newHTML ) {
377 - context.fn.purgeOffsets();
378 - context.oldHTML = newHTML;
379 - event.data.scope = 'realchange';
380 - }
381 - // Never let the body be totally empty
382 - if ( context.$content.children().length == 0 ) {
383 - context.$content.append( '<p></p>' );
384 - }
385 - return true;
386 - },
387 - 'delayedChange': function( event ) {
388 - event.data.scope = 'division';
389 - var newHTML = context.$content.html();
390 - if ( context.oldDelayedHTML != newHTML ) {
391 - context.oldDelayedHTML = newHTML;
392 - event.data.scope = 'realchange';
393 - // Surround by <p> if it does not already have it
394 - var cursorPos = context.fn.getCaretPosition();
395 - var t = context.fn.getOffset( cursorPos[0] );
396 - if ( ! $.browser.msie && t && t.node.nodeName == '#text' && t.node.parentNode.nodeName.toLowerCase() == 'body' ) {
397 - $( t.node ).wrap( "<p></p>" );
398 - context.fn.purgeOffsets();
399 - context.fn.setSelection( { start: cursorPos[0], end: cursorPos[1] } );
400 - }
401 - }
402 - context.fn.updateHistory( event.data.scope == 'realchange' );
403 - return true;
404 - },
405 - 'cut': function( event ) {
406 - setTimeout( function() {
407 - context.$content.find( 'br' ).each( function() {
408 - if ( $(this).parent().is( 'body' ) ) {
409 - $(this).wrap( $( '<p></p>' ) );
410 - }
411 - } );
412 - }, 100 );
413 - return true;
414 - },
415 - 'paste': function( event ) {
416 - // Save the cursor position to restore it after all this voodoo
417 - var cursorPos = context.fn.getCaretPosition();
418 - var oldLength = context.fn.getContents().length;
419 - var positionFromEnd = oldLength - cursorPos[1];
420 -
421 - //give everything the wikiEditor class so that we can easily pick out things without that class as pasted
422 - context.$content.find( '*' ).addClass( 'wikiEditor' );
423 - if ( $.layout.name !== 'webkit' ) {
424 - context.$content.addClass( 'pasting' );
425 - }
426 -
427 - setTimeout( function() {
428 - // Kill stuff we know we don't want
429 - context.$content.find( 'script,style,img,input,select,textarea,hr,button,link,meta' ).remove();
430 - var nodeToDelete = [];
431 - var pastedContent = [];
432 - var firstDirtyNode;
433 - var $lastDirtyNode;
434 - var elementAtCursor;
435 - if ( $.browser.msie && !context.offsets ) {
436 - elementAtCursor = null;
437 - } else {
438 - elementAtCursor = context.fn.getOffset( cursorPos[0] );
439 - }
440 - if ( elementAtCursor == null || elementAtCursor.node == null ) {
441 - context.$content.prepend( '<p class = wikiEditor></p>' );
442 - firstDirtyNode = context.$content.children()[0];
443 - } else {
444 - firstDirtyNode = elementAtCursor.node;
445 - }
446 -
447 - //this is ugly but seems like the best way to handle the case where we select and replace all editor contents
448 - try {
449 - firstDirtyNode.parentNode;
450 - } catch ( err ) {
451 - context.$content.prepend( '<p class = wikiEditor></p>' );
452 - firstDirtyNode = context.$content.children()[0];
453 - }
454 -
455 - while ( firstDirtyNode != null ) {
456 - //we're going to replace the contents of the entire parent node.
457 - while ( firstDirtyNode.parentNode && firstDirtyNode.parentNode.nodeName != 'BODY'
458 - && ! $( firstDirtyNode ).hasClass( 'wikiEditor' )
459 - ) {
460 - firstDirtyNode = firstDirtyNode.parentNode;
461 - }
462 - //go back till we find the first pasted node
463 - while ( firstDirtyNode.previousSibling != null
464 - && ! $( firstDirtyNode.previousSibling ).hasClass( 'wikiEditor' )
465 - ) {
466 -
467 - if ( $( firstDirtyNode.previousSibling ).hasClass( '#comment' ) ) {
468 - $( firstDirtyNode ).remove();
469 - } else {
470 - firstDirtyNode = firstDirtyNode.previousSibling;
471 - }
472 - }
473 -
474 - if ( firstDirtyNode.previousSibling != null ) {
475 - $lastDirtyNode = $( firstDirtyNode.previousSibling );
476 - } else {
477 - $lastDirtyNode = $( firstDirtyNode );
478 - }
479 -
480 - var cc = makeContentCollector( $.browser, null );
481 - while ( firstDirtyNode != null ) {
482 - cc.collectContent(firstDirtyNode);
483 - cc.notifyNextNode(firstDirtyNode.nextSibling);
484 -
485 - nodeToDelete.push( firstDirtyNode );
486 -
487 - firstDirtyNode = firstDirtyNode.nextSibling;
488 - if ( $( firstDirtyNode ).hasClass( 'wikiEditor' ) ) {
489 - break;
490 - }
491 - }
492 -
493 - var ccData = cc.finish();
494 - pastedContent = ccData.lines;
495 - var pastedPretty = '';
496 - for ( var i = 0; i < pastedContent.length; i++ ) {
497 - //escape html
498 - pastedPretty = pastedContent[i].replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\r?\n/g, '\\n');
499 - //replace leading white spaces with &nbsp;
500 - match = pastedContent[i].match(/^[\s]+[^\s]/);
501 - if ( match != null && match.length > 0 ) {
502 - index = match[0].length;
503 - leadingSpace = match[0].replace(/[\s]/g, '&nbsp;');
504 - pastedPretty = leadingSpace + pastedPretty.substring(index, pastedPretty.length);
505 - }
506 -
507 -
508 - if( !pastedPretty && $.browser.msie && i == 0 ) {
509 - continue;
510 - }
511 - $newElement = $( '<p class="wikiEditor pasted" ></p>' );
512 - if ( pastedPretty ) {
513 - $newElement.html( pastedPretty );
514 - } else {
515 - $newElement.html( '<br class="wikiEditor">' );
516 - }
517 - $newElement.insertAfter( $lastDirtyNode );
518 -
519 - $lastDirtyNode = $newElement;
520 -
521 - }
522 -
523 - //now delete all the original nodes that we prettified already
524 - while ( nodeToDelete.length > 0 ) {
525 - $deleteNode = $( nodeToDelete.pop() );
526 - $deleteNode.remove();
527 - }
528 -
529 - //anything without wikiEditor class was pasted.
530 - $selection = context.$content.find( ':not(.wikiEditor)' );
531 - if ( $selection.length == 0 ) {
532 - break;
533 - } else {
534 - firstDirtyNode = $selection.eq( 0 )[0];
535 - }
536 - }
537 - context.$content.find( '.wikiEditor' ).removeClass( 'wikiEditor' );
538 -
539 - //now place the cursor at the end of pasted content
540 - var newLength = context.fn.getContents().length;
541 - var newPos = newLength - positionFromEnd;
542 -
543 - context.fn.purgeOffsets();
544 - context.fn.setSelection( { start: newPos, end: newPos } );
545 -
546 - context.fn.scrollToCaretPosition();
547 -
548 -
549 - }, 0 );
550 - return true;
551 - },
552 - 'ready': function( event ) {
553 - // Initialize our history queue
554 - if ( context.$content ) {
555 - context.history.push( { 'html': context.$content.html(), 'sel': context.fn.getCaretPosition() } );
556 - } else {
557 - context.history.push( { 'html': '', 'sel': context.fn.getCaretPosition() } );
558 - }
559 - return true;
560378 }
561379 };
562380
@@ -667,1115 +485,7 @@
668486 .hide()
669487 .appendTo( context.$ui );
670488 },
671 - 'highlightLine': function( $element, mode ) {
672 - if ( !$element.is( 'p' ) ) {
673 - $element = $element.closest( 'p' );
674 - }
675 - $element.css( 'backgroundColor', '#AACCFF' );
676 - setTimeout( function() { $element.animate( { 'backgroundColor': 'white' }, 'slow' ); }, 100 );
677 - setTimeout( function() { $element.css( 'backgroundColor', 'white' ); }, 1000 );
678 - },
679 - 'htmlToText': function( html ) {
680 - // This function is slow for large inputs, so aggressively cache input/output pairs
681 - if ( html in context.htmlToTextMap ) {
682 - return context.htmlToTextMap[html];
683 - }
684 - var origHTML = html;
685 -
686 - // We use this elaborate trickery for cross-browser compatibility
687 - // IE does overzealous whitespace collapsing for $( '<pre />' ).html( html );
688 - // We also do <br> and easy cases for <p> conversion here, complicated cases are handled later
689 - html = html
690 - .replace( /\r?\n/g, "" ) // IE7 inserts newlines before block elements
691 - .replace( /&nbsp;/g, " " ) // We inserted these to prevent IE from collapsing spaces
692 - .replace( /\<br[^\>]*\>\<\/p\>/gi, '</p>' ) // Remove trailing <br> from <p>
693 - .replace( /\<\/p\>\s*\<p[^\>]*\>/gi, "\n" ) // Easy case for <p> conversion
694 - .replace( /\<br[^\>]*\>/gi, "\n" ) // <br> conversion
695 - .replace( /\<\/p\>(\n*)\<p[^\>]*\>/gi, "$1\n" )
696 - // Un-nest <p> tags
697 - .replace( /\<p[^\>]*\><p[^\>]*\>/gi, '<p>' )
698 - .replace( /\<\/p\><\/p\>/gi, '</p>' );
699 - // Save leading and trailing whitespace now and restore it later. IE eats it all, and even Firefox
700 - // won't leave everything alone
701 - var leading = html.match( /^\s*/ )[0];
702 - var trailing = html.match( /\s*$/ )[0];
703 - html = html.substr( leading.length, html.length - leading.length - trailing.length );
704 - var $pre = $( '<pre>' + html + '</pre>' );
705 - $pre.find( '.wikiEditor-noinclude' ).each( function() { $( this ).remove(); } );
706 - // Convert tabs, <p>s and <br>s back
707 - $pre.find( '.wikiEditor-tab' ).each( function() { $( this ).text( "\t" ); } );
708 - $pre.find( 'br' ).each( function() { $( this ).replaceWith( "\n" ); } );
709 - // Converting <p>s is wrong if there's nothing before them, so check that.
710 - // .find( '* + p' ) isn't good enough because textnodes aren't considered
711 - $pre.find( 'p' ).each( function() {
712 - var text = $( this ).text();
713 - // If this <p> is preceded by some text, add a \n at the beginning, and if
714 - // it's followed by a textnode, add a \n at the end
715 - // We need the traverser because there can be other weird stuff in between
716 -
717 - // Check for preceding text
718 - var t = new context.fn.rawTraverser( this.firstChild, this, $pre.get( 0 ), true ).prev();
719 - while ( t && t.node.nodeName != '#text' && t.node.nodeName != 'BR' && t.node.nodeName != 'P' ) {
720 - t = t.prev();
721 - }
722 - if ( t ) {
723 - text = "\n" + text;
724 - }
725 -
726 - // Check for following text
727 - t = new context.fn.rawTraverser( this.lastChild, this, $pre.get( 0 ), true ).next();
728 - while ( t && t.node.nodeName != '#text' && t.node.nodeName != 'BR' && t.node.nodeName != 'P' ) {
729 - t = t.next();
730 - }
731 - if ( t && !t.inP && t.node.nodeName == '#text' && t.node.nodeValue.charAt( 0 ) != '\n'
732 - && t.node.nodeValue.charAt( 0 ) != '\r' ) {
733 - text += "\n";
734 - }
735 - $( this ).text( text );
736 - } );
737 - var retval;
738 - if ( $.browser.msie ) {
739 - // IE aggressively collapses whitespace in .text() after having done DOM manipulation,
740 - // but for some crazy reason this does work. Also convert \r back to \n
741 - retval = $( '<pre>' + $pre.html() + '</pre>' ).text().replace( /\r/g, '\n' );
742 - } else {
743 - retval = $pre.text();
744 - }
745 - return context.htmlToTextMap[origHTML] = leading + retval + trailing;
746 - },
747489 /**
748 - * Get the first element before the selection that's in a certain class
749 - * @param classname Class to match. Defaults to '', meaning any class
750 - * @param strict If true, the element the selection starts in cannot match (default: false)
751 - * @return jQuery object or null if unknown
752 - */
753 - 'beforeSelection': function( classname, strict ) {
754 - if ( typeof classname == 'undefined' ) {
755 - classname = '';
756 - }
757 - var e = null, offset = null;
758 - if ( $.browser.msie && !context.$iframe[0].contentWindow.document.body ) {
759 - return null;
760 - }
761 - if ( context.$iframe[0].contentWindow.getSelection ) {
762 - // Firefox and Opera
763 - var selection = context.$iframe[0].contentWindow.getSelection();
764 - // On load, webkit seems to not have a valid selection
765 - if ( selection.baseNode !== null ) {
766 - // Start at the selection's start and traverse the DOM backwards
767 - // This is done by traversing an element's children first, then the element itself, then its parent
768 - e = selection.getRangeAt( 0 ).startContainer;
769 - offset = selection.getRangeAt( 0 ).startOffset;
770 - } else {
771 - return null;
772 - }
773 -
774 - // When the cursor is on an empty line, Opera gives us a bogus range object with
775 - // startContainer=endContainer=body and startOffset=endOffset=1
776 - var body = context.$iframe[0].contentWindow.document.body;
777 - if ( $.browser.opera && e == body && offset == 1 ) {
778 - return null;
779 - }
780 - }
781 - if ( !e && context.$iframe[0].contentWindow.document.selection ) {
782 - // IE
783 - // Because there's nothing like range.startContainer in IE, we need to do a DOM traversal
784 - // to find the element the start of the selection is in
785 - var range = context.$iframe[0].contentWindow.document.selection.createRange();
786 - // Set range2 to the text before the selection
787 - var range2 = context.$iframe[0].contentWindow.document.body.createTextRange();
788 - // For some reason this call throws errors in certain cases, e.g. when the selection is
789 - // not in the iframe
790 - try {
791 - range2.setEndPoint( 'EndToStart', range );
792 - } catch ( ex ) {
793 - return null;
794 - }
795 - var seekPos = context.fn.htmlToText( range2.htmlText ).length;
796 - var offset = context.fn.getOffset( seekPos );
797 - e = offset ? offset.node : null;
798 - offset = offset ? offset.offset : null;
799 - if ( !e ) {
800 - return null;
801 - }
802 - }
803 - if ( e.nodeName != '#text' ) {
804 - // The selection is not in a textnode, but between two non-text nodes
805 - // (usually inside the <body> between two <br>s). Go to the rightmost
806 - // child of the node just before the selection
807 - var newE = e.firstChild;
808 - for ( var i = 0; i < offset - 1 && newE; i++ ) {
809 - newE = newE.nextSibling;
810 - }
811 - while ( newE && newE.lastChild ) {
812 - newE = newE.lastChild;
813 - }
814 - e = newE || e;
815 - }
816 -
817 - // We'd normally use if( $( e ).hasClass( class ) in the while loop, but running the jQuery
818 - // constructor thousands of times is very inefficient
819 - var classStr = ' ' + classname + ' ';
820 - while ( e ) {
821 - if ( !strict && ( !classname || ( ' ' + e.className + ' ' ).indexOf( classStr ) != -1 ) ) {
822 - return $( e );
823 - }
824 - var next = e.previousSibling;
825 - while ( next && next.lastChild ) {
826 - next = next.lastChild;
827 - }
828 - e = next || e.parentNode;
829 - strict = false;
830 - }
831 - return $( [] );
832 - },
833 - /**
834 - * Object used by traverser(). Don't use this unless you know what you're doing
835 - */
836 - 'rawTraverser': function( node, inP, ancestor, skipNoinclude ) {
837 - this.node = node;
838 - this.inP = inP;
839 - this.ancestor = ancestor;
840 - this.skipNoinclude = skipNoinclude;
841 - this.next = function() {
842 - var p = this.node;
843 - var nextInP = this.inP;
844 - while ( p && !p.nextSibling ) {
845 - p = p.parentNode;
846 - if ( p == this.ancestor ) {
847 - // We're back at the ancestor, stop here
848 - p = null;
849 - }
850 - if ( p && p.nodeName == "P" ) {
851 - nextInP = null;
852 - }
853 - }
854 - p = p ? p.nextSibling : null;
855 - if ( p && p.nodeName == "P" ) {
856 - nextInP = p;
857 - }
858 - do {
859 - // Filter nodes with the wikiEditor-noinclude class
860 - // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because
861 - // $() is slow in a tight loop
862 - if ( this.skipNoinclude ) {
863 - while ( p && ( ' ' + p.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) {
864 - p = p.nextSibling;
865 - }
866 - }
867 - if ( p && p.firstChild ) {
868 - p = p.firstChild;
869 - if ( p.nodeName == "P" ) {
870 - nextInP = p;
871 - }
872 - }
873 - } while ( p && p.firstChild );
874 - // Instead of calling the rawTraverser constructor, inline it. This avoids function call overhead
875 - return p ? { 'node': p, 'inP': nextInP, 'ancestor': this.ancestor,
876 - 'skipNoinclude': this.skipNoinclude, 'next': this.next, 'prev': this.prev } : null;
877 - };
878 - this.prev = function() {
879 - var p = this.node;
880 - var prevInP = this.inP;
881 - while ( p && !p.previousSibling ) {
882 - p = p.parentNode;
883 - if ( p == this.ancestor ) {
884 - // We're back at the ancestor, stop here
885 - p = null;
886 - }
887 - if ( p && p.nodeName == "P" ) {
888 - prevInP = null;
889 - }
890 - }
891 - p = p ? p.previousSibling : null;
892 - if ( p && p.nodeName == "P" ) {
893 - prevInP = p;
894 - }
895 - do {
896 - // Filter nodes with the wikiEditor-noinclude class
897 - // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because
898 - // $() is slow in a tight loop
899 - if ( this.skipNoinclude ) {
900 - while ( p && ( ' ' + p.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) {
901 - p = p.previousSibling;
902 - }
903 - }
904 - if ( p && p.lastChild ) {
905 - p = p.lastChild;
906 - if ( p.nodeName == "P" ) {
907 - prevInP = p;
908 - }
909 - }
910 - } while ( p && p.lastChild );
911 - // Instead of calling the rawTraverser constructor, inline it. This avoids function call overhead
912 - return p ? { 'node': p, 'inP': prevInP, 'ancestor': this.ancestor,
913 - 'skipNoinclude': this.skipNoinclude, 'next': this.next, 'prev': this.prev } : null;
914 - };
915 - },
916 - /**
917 - * Get an object used to traverse the leaf nodes in the iframe DOM. This traversal skips leaf nodes
918 - * inside an element with the wikiEditor-noinclude class. This basically wraps rawTraverser
919 - *
920 - * @param start Node to start at
921 - * @return Traverser object, use .next() or .prev() to get a traverser object referring to the
922 - * previous/next node
923 - */
924 - 'traverser': function( start ) {
925 - // Find the leftmost leaf node in the tree
926 - var startNode = start.jquery ? start.get( 0 ) : start;
927 - var node = startNode;
928 - var inP = node.nodeName == "P" ? node : null;
929 - do {
930 - // Filter nodes with the wikiEditor-noinclude class
931 - // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because
932 - // $() is slow in a tight loop
933 - while ( node && ( ' ' + node.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) {
934 - node = node.nextSibling;
935 - }
936 - if ( node && node.firstChild ) {
937 - node = node.firstChild;
938 - if ( node.nodeName == "P" ) {
939 - inP = node;
940 - }
941 - }
942 - } while ( node && node.firstChild );
943 - return new context.fn.rawTraverser( node, inP, startNode, true );
944 - },
945 - 'getOffset': function( offset ) {
946 - if ( !context.offsets ) {
947 - context.fn.refreshOffsets();
948 - }
949 - if ( offset in context.offsets ) {
950 - return context.offsets[offset];
951 - }
952 - // Our offset is not pre-cached. Find the highest offset below it and interpolate
953 - // We need to traverse the entire object because for() doesn't traverse in order
954 - // We don't do in-order traversal because the object is sparse
955 - var lowerBound = -1;
956 - for ( var o in context.offsets ) {
957 - var realO = parseInt( o );
958 - if ( realO < offset && realO > lowerBound) {
959 - lowerBound = realO;
960 - }
961 - }
962 - if ( !( lowerBound in context.offsets ) ) {
963 - // Weird edge case: either offset is too large or the document is empty
964 - return null;
965 - }
966 - var base = context.offsets[lowerBound];
967 - return context.offsets[offset] = {
968 - 'node': base.node,
969 - 'offset': base.offset + offset - lowerBound,
970 - 'length': base.length,
971 - 'lastTextNode': base.lastTextNode
972 - };
973 - },
974 - 'purgeOffsets': function() {
975 - context.offsets = null;
976 - },
977 - 'refreshOffsets': function() {
978 - context.offsets = [ ];
979 - var t = context.fn.traverser( context.$content );
980 - var pos = 0, lastTextNode = null;
981 - while ( t ) {
982 - if ( t.node.nodeName != '#text' && t.node.nodeName != 'BR' ) {
983 - t = t.next();
984 - continue;
985 - }
986 - var nextPos = t.node.nodeName == '#text' ? pos + t.node.nodeValue.length : pos + 1;
987 - var nextT = t.next();
988 - var leavingP = t.node.nodeName == '#text' && t.inP && nextT && ( !nextT.inP || nextT.inP != t.inP );
989 - context.offsets[pos] = {
990 - 'node': t.node,
991 - 'offset': 0,
992 - 'length': nextPos - pos + ( leavingP ? 1 : 0 ),
993 - 'lastTextNode': lastTextNode
994 - };
995 - if ( leavingP ) {
996 - // <p>Foo</p> looks like "Foo\n", make it quack like it too
997 - // Basically we're faking the \n character much like we're treating <br>s
998 - context.offsets[nextPos] = {
999 - 'node': t.node,
1000 - 'offset': nextPos - pos,
1001 - 'length': nextPos - pos + 1,
1002 - 'lastTextNode': lastTextNode
1003 - };
1004 - }
1005 - pos = nextPos + ( leavingP ? 1 : 0 );
1006 - if ( t.node.nodeName == '#text' ) {
1007 - lastTextNode = t.node;
1008 - }
1009 - t = nextT;
1010 - }
1011 - },
1012 - 'saveSelection': function() {
1013 - if ( !$.browser.msie ) {
1014 - // Only IE needs this
1015 - return;
1016 - }
1017 - if ( typeof context.$iframe != 'undefined' ) {
1018 - context.$iframe[0].contentWindow.focus();
1019 - context.savedSelection = context.$iframe[0].contentWindow.document.selection.createRange();
1020 - } else {
1021 - context.$textarea.focus();
1022 - context.savedSelection = document.selection.createRange();
1023 - }
1024 - },
1025 - 'restoreSelection': function() {
1026 - if ( !$.browser.msie || context.savedSelection === null ) {
1027 - return;
1028 - }
1029 - if ( typeof context.$iframe != 'undefined' ) {
1030 - context.$iframe[0].contentWindow.focus();
1031 - } else {
1032 - context.$textarea.focus();
1033 - }
1034 - context.savedSelection.select();
1035 - context.savedSelection = null;
1036 - },
1037 - /**
1038 - * Update the history queue
1039 - *
1040 - * @param htmlChange pass true or false to inidicate if there was a text change that should potentially
1041 - * be given a new history state.
1042 - */
1043 - 'updateHistory': function( htmlChange ) {
1044 - var newHTML = context.$content.html();
1045 - var newSel = context.fn.getCaretPosition();
1046 - // Was text changed? Was it because of a REDO or UNDO action?
1047 - if (
1048 - context.history.length == 0 ||
1049 - ( htmlChange && context.oldDelayedHistoryPosition == context.historyPosition )
1050 - ) {
1051 - context.oldDelayedSel = newSel;
1052 - // Do we need to trim extras from our history?
1053 - // FIXME: this should really be happing on change, not on the delay
1054 - if ( context.historyPosition < -1 ) {
1055 - //clear out the extras
1056 - context.history.splice( context.history.length + context.historyPosition + 1 );
1057 - context.historyPosition = -1;
1058 - }
1059 - context.history.push( { 'html': newHTML, 'sel': newSel } );
1060 - // If the history has grown longer than 10 items, remove the earliest one
1061 - while ( context.history.length > 10 ) {
1062 - context.history.shift();
1063 - }
1064 - } else if ( context.oldDelayedSel != newSel ) {
1065 - // If only the selection was changed, update it
1066 - context.oldDelayedSel = newSel;
1067 - context.history[context.history.length + context.historyPosition].sel = newSel;
1068 - }
1069 - // synch our old delayed history position until the next undo/redo action
1070 - context.oldDelayedHistoryPosition = context.historyPosition;
1071 - },
1072 - /**
1073 - * Sets up the iframe in place of the textarea to allow more advanced operations
1074 - */
1075 - 'setupIframe': function() {
1076 - context.$iframe = $( '<iframe></iframe>' )
1077 - .attr( {
1078 - 'frameBorder': 0,
1079 - 'border': 0,
1080 - 'tabindex': 1,
1081 - 'src': wgScriptPath + '/extensions/WikiEditor/modules/jquery.wikiEditor.html?' +
1082 - 'instance=' + context.instance + '&ts=' + ( new Date() ).getTime() + '&is=content',
1083 - 'id': 'wikiEditor-iframe-' + context.instance
1084 - } )
1085 - .css( {
1086 - 'backgroundColor': 'white',
1087 - 'width': '100%',
1088 - 'height': context.$textarea.height(),
1089 - 'display': 'none',
1090 - 'overflow-y': 'scroll',
1091 - 'overflow-x': 'hidden'
1092 - } )
1093 - .insertAfter( context.$textarea )
1094 - .load( function() {
1095 - // Internet Explorer will reload the iframe once we turn on design mode, so we need to only turn it
1096 - // on during the first run, and then bail
1097 - if ( !this.isSecondRun ) {
1098 - // Turn the document's design mode on
1099 - context.$iframe[0].contentWindow.document.designMode = 'on';
1100 - // Let the rest of this function happen next time around
1101 - if ( $.browser.msie ) {
1102 - this.isSecondRun = true;
1103 - return;
1104 - }
1105 - }
1106 - // Get a reference to the content area of the iframe
1107 - context.$content = $( context.$iframe[0].contentWindow.document.body );
1108 - // Add classes to the body to influence the styles based on what's enabled
1109 - for ( module in context.modules ) {
1110 - context.$content.addClass( 'wikiEditor-' + module );
1111 - }
1112 - // If we just do "context.$content.text( context.$textarea.val() )", Internet Explorer will strip
1113 - // out the whitespace charcters, specifically "\n" - so we must manually encode text and append it
1114 - // TODO: Refactor this into a textToHtml() function
1115 - var html = context.$textarea.val()
1116 - // We're gonna use &esc; as an escape sequence
1117 - .replace( /&esc;/g, '&esc;esc;' )
1118 - // Escape existing uses of <p>, </p>, &nbsp; and <span class="wikiEditor-tab"></span>
1119 - .replace( /\<p\>/g, '&esc;&lt;p&gt;' )
1120 - .replace( /\<\/p\>/g, '&esc;&lt;/p&gt;' )
1121 - .replace(
1122 - /\<span class="wikiEditor-tab"\>\<\/span\>/g,
1123 - '&esc;&lt;span&nbsp;class=&quot;wikiEditor-tab&quot;&gt;&lt;/span&gt;'
1124 - )
1125 - .replace( /&nbsp;/g, '&esc;&amp;nbsp;' );
1126 - // We must do some extra processing on IE to avoid dirty diffs, specifically IE will collapse
1127 - // leading spaces - browser sniffing is not ideal, but executing this code on a non-broken browser
1128 - // doesn't cause harm
1129 - if ( $.browser.msie ) {
1130 - html = html.replace( /\t/g, '<span class="wikiEditor-tab"></span>' );
1131 - if ( $.browser.versionNumber <= 7 ) {
1132 - // Replace all spaces matching &nbsp; - IE <= 7 needs this because of its overzealous
1133 - // whitespace collapsing
1134 - html = html.replace( / /g, "&nbsp;" );
1135 - } else {
1136 - // IE8 is happy if we just convert the first leading space to &nbsp;
1137 - html = html.replace( /(^|\n) /g, "$1&nbsp;" );
1138 - }
1139 - }
1140 - // Use a dummy div to escape all entities
1141 - // This'll also escape <br>, <span> and &nbsp; , so we unescape those after
1142 - // We also need to unescape the doubly-escaped things mentioned above
1143 - html = $( '<div />' ).text( '<p>' + html.replace( /\r?\n/g, '</p><p>' ) + '</p>' ).html()
1144 - .replace( /&amp;nbsp;/g, '&nbsp;' )
1145 - // Allow <p> tags to survive encoding
1146 - .replace( /&lt;p&gt;/g, '<p>' )
1147 - .replace( /&lt;\/p&gt;/g, '</p>' )
1148 - // And <span class="wikiEditor-tab"></span> too
1149 - .replace(
1150 - /&lt;span( |&nbsp;)class=("|&quot;)wikiEditor-tab("|&quot;)&gt;&lt;\/span&gt;/g,
1151 - '<span class="wikiEditor-tab"></span>'
1152 - )
1153 - // Empty <p> tags need <br> tags in them
1154 - .replace( /<p><\/p>/g, '<p><br></p>' )
1155 - // Unescape &esc; stuff
1156 - .replace( /&amp;esc;&amp;amp;nbsp;/g, '&amp;nbsp;' )
1157 - .replace( /&amp;esc;&amp;lt;p&amp;gt;/g, '&lt;p&gt;' )
1158 - .replace( /&amp;esc;&amp;lt;\/p&amp;gt;/g, '&lt;/p&gt;' )
1159 - .replace(
1160 - /&amp;esc;&amp;lt;span&amp;nbsp;class=&amp;quot;wikiEditor-tab&amp;quot;&amp;gt;&amp;lt;\/span&amp;gt;/g,
1161 - '&lt;span class="wikiEditor-tab"&gt;&lt;\/span&gt;'
1162 - )
1163 - .replace( /&amp;esc;esc;/g, '&amp;esc;' );
1164 - context.$content.html( html );
1165 -
1166 - // Reflect direction of parent frame into child
1167 - if ( $( 'body' ).is( '.rtl' ) ) {
1168 - context.$content.addClass( 'rtl' ).attr( 'dir', 'rtl' );
1169 - }
1170 - // Activate the iframe, encoding the content of the textarea and copying it to the content of iframe
1171 - context.$textarea.attr( 'disabled', true );
1172 - context.$textarea.hide();
1173 - context.$iframe.show();
1174 - // Let modules know we're ready to start working with the content
1175 - context.fn.trigger( 'ready' );
1176 - // Only save HTML now: ready handlers may have modified it
1177 - context.oldHTML = context.oldDelayedHTML = context.$content.html();
1178 - //remove our temporary loading
1179 - /* Disaling our loading div for now
1180 - $( '.wikiEditor-ui-loading' ).fadeOut( 'fast', function() {
1181 - $( this ).remove();
1182 - } );
1183 - */
1184 - // Setup event handling on the iframe
1185 - $( context.$iframe[0].contentWindow.document )
1186 - .bind( 'keydown', function( event ) {
1187 - event.jQueryNode = context.fn.getElementAtCursor();
1188 - return context.fn.trigger( 'keydown', event );
1189 -
1190 - } )
1191 - .bind( 'keyup', function( event ) {
1192 - event.jQueryNode = context.fn.getElementAtCursor();
1193 - return context.fn.trigger( 'keyup', event );
1194 - } )
1195 - .bind( 'keypress', function( event ) {
1196 - event.jQueryNode = context.fn.getElementAtCursor();
1197 - return context.fn.trigger( 'keypress', event );
1198 - } )
1199 - .bind( 'paste', function( event ) {
1200 - return context.fn.trigger( 'paste', event );
1201 - } )
1202 - .bind( 'cut', function( event ) {
1203 - return context.fn.trigger( 'cut', event );
1204 - } )
1205 - .bind( 'keyup paste mouseup cut encapsulateSelection', function( event ) {
1206 - return context.fn.trigger( 'change', event );
1207 - } )
1208 - .delayedBind( 250, 'keyup paste mouseup cut encapsulateSelection', function( event ) {
1209 - context.fn.trigger( 'delayedChange', event );
1210 - } );
1211 - } );
1212 - // Attach a submit handler to the form so that when the form is submitted the content of the iframe gets
1213 - // decoded and copied over to the textarea
1214 - context.$textarea.closest( 'form' ).submit( function() {
1215 - context.$textarea.attr( 'disabled', false );
1216 - context.$textarea.val( context.$textarea.textSelection( 'getContents' ) );
1217 - } );
1218 - /* FIXME: This was taken from EditWarning.js - maybe we could do a jquery plugin for this? */
1219 - // Attach our own handler for onbeforeunload which respects the current one
1220 - context.fallbackWindowOnBeforeUnload = window.onbeforeunload;
1221 - window.onbeforeunload = function() {
1222 - context.$textarea.val( context.$textarea.textSelection( 'getContents' ) );
1223 - if ( context.fallbackWindowOnBeforeUnload ) {
1224 - return context.fallbackWindowOnBeforeUnload();
1225 - }
1226 - };
1227 - },
1228 -
1229 - /*
1230 - * Compatibility with the $.textSelection jQuery plug-in. When the iframe is in use, these functions provide
1231 - * equivilant functionality to the otherwise textarea-based functionality.
1232 - */
1233 -
1234 - 'getElementAtCursor': function() {
1235 - if ( context.$iframe[0].contentWindow.getSelection ) {
1236 - // Firefox and Opera
1237 - var selection = context.$iframe[0].contentWindow.getSelection();
1238 - if ( selection.rangeCount == 0 ) {
1239 - // We don't know where the cursor is
1240 - return $( [] );
1241 - }
1242 - var sc = selection.getRangeAt( 0 ).startContainer;
1243 - if ( sc.nodeName == "#text" ) sc = sc.parentNode;
1244 - return $( sc );
1245 - } else if ( context.$iframe[0].contentWindow.document.selection ) { // should come last; Opera!
1246 - // IE
1247 - var selection = context.$iframe[0].contentWindow.document.selection.createRange();
1248 - return $( selection.parentElement() );
1249 - }
1250 - },
1251 -
1252 - /**
1253 - * Gets the complete contents of the iframe (in plain text, not HTML)
1254 - */
1255 - 'getContents': function() {
1256 - // For <p></p>, .html() returns <p>&nbsp;</p> in IE
1257 - // This seems to convince IE while not affecting display
1258 - if ( !context.$content ) {
1259 - return '';
1260 - }
1261 - var html;
1262 - if ( $.browser.msie ) {
1263 - // Don't manipulate the iframe DOM itself, causes cursor jumping issues
1264 - var $c = $( context.$content.get( 0 ).cloneNode( true ) );
1265 - $c.find( 'p' ).each( function() {
1266 - if ( $(this).html() == '' ) {
1267 - $(this).replaceWith( '<p></p>' );
1268 - }
1269 - } );
1270 - html = $c.html();
1271 - } else {
1272 - html = context.$content.html();
1273 - }
1274 - return context.fn.htmlToText( html );
1275 - },
1276 - /**
1277 - * Gets the currently selected text in the content
1278 - * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
1279 - */
1280 - 'getSelection': function() {
1281 - var retval;
1282 - if ( context.$iframe[0].contentWindow.getSelection ) {
1283 - // Firefox and Opera
1284 - retval = context.$iframe[0].contentWindow.getSelection();
1285 - if ( $.browser.opera ) {
1286 - // Opera strips newlines in getSelection(), so we need something more sophisticated
1287 - if ( retval.rangeCount > 0 ) {
1288 - retval = context.fn.htmlToText( $( '<pre />' )
1289 - .append( retval.getRangeAt( 0 ).cloneContents() )
1290 - .html()
1291 - );
1292 - } else {
1293 - retval = '';
1294 - }
1295 - }
1296 - } else if ( context.$iframe[0].contentWindow.document.selection ) { // should come last; Opera!
1297 - // IE
1298 - retval = context.$iframe[0].contentWindow.document.selection.createRange();
1299 - }
1300 - if ( typeof retval.text != 'undefined' ) {
1301 - // In IE8, retval.text is stripped of newlines, so we need to process retval.htmlText
1302 - // to get a reliable answer. IE7 does get this right though
1303 - // Run this fix for all IE versions anyway, it doesn't hurt
1304 - retval = context.fn.htmlToText( retval.htmlText );
1305 - } else if ( typeof retval.toString != 'undefined' ) {
1306 - retval = retval.toString();
1307 - }
1308 - return retval;
1309 - },
1310 - /**
1311 - * Inserts text at the begining and end of a text selection, optionally inserting text at the caret when
1312 - * selection is empty.
1313 - * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
1314 - */
1315 - 'encapsulateSelection': function( options ) {
1316 - var selText = $(this).textSelection( 'getSelection' );
1317 - var selTextArr;
1318 - var collapseToEnd = false;
1319 - var selectAfter = false;
1320 - var setSelectionTo = null;
1321 - var pre = options.pre, post = options.post;
1322 - if ( !selText ) {
1323 - selText = options.peri;
1324 - selectAfter = true;
1325 - } else if ( options.peri == selText.replace( /\s+$/, '' ) ) {
1326 - // Probably a successive button press
1327 - // strip any extra white space from selText
1328 - selText = selText.replace( /\s+$/, '' );
1329 - // set the collapseToEnd flag to ensure our selection is collapsed to the end before any insertion is done
1330 - collapseToEnd = true;
1331 - // set selectAfter to true since we know we'll be populating with our default text
1332 - selectAfter = true;
1333 - } else if ( options.replace ) {
1334 - selText = options.peri;
1335 - } else if ( selText.charAt( selText.length - 1 ) == ' ' ) {
1336 - // Exclude ending space char
1337 - // FIXME: Why?
1338 - selText = selText.substring( 0, selText.length - 1 );
1339 - post += ' ';
1340 - }
1341 - if ( options.splitlines ) {
1342 - selTextArr = selText.split( /\n/ );
1343 - }
1344 -
1345 - if ( context.$iframe[0].contentWindow.getSelection ) {
1346 - // Firefox and Opera
1347 - var range = context.$iframe[0].contentWindow.getSelection().getRangeAt( 0 );
1348 - // if our test above indicated that this was a sucessive button press, we need to collapse the
1349 - // selection to the end to avoid replacing text
1350 - if ( collapseToEnd ) {
1351 - // Make sure we're not collapsing ourselves into a BR tag
1352 - if ( range.endContainer.nodeName == 'BR' ) {
1353 - range.setEndBefore( range.endContainer );
1354 - }
1355 - range.collapse( false );
1356 - }
1357 - if ( options.ownline ) {
1358 - // We need to figure out if the cursor is at the start or end of a line
1359 - var atStart = false, atEnd = false;
1360 - var body = context.$content.get( 0 );
1361 - if ( range.startOffset == 0 ) {
1362 - // Start of a line
1363 - // FIXME: Not necessarily the case with syntax highlighting or
1364 - // template collapsing
1365 - atStart = true;
1366 - } else if ( range.startContainer == body ) {
1367 - // Look up the node just before the start of the selection
1368 - // If it's a <BR>, we're at the start of a line that starts with a
1369 - // block element; if not, we're at the end of a line
1370 - var n = body.firstChild;
1371 - for ( var i = 0; i < range.startOffset - 1 && n; i++ ) {
1372 - n = n.nextSibling;
1373 - }
1374 - if ( n && n.nodeName == 'BR' ) {
1375 - atStart = true;
1376 - } else {
1377 - atEnd = true;
1378 - }
1379 - }
1380 - if ( ( range.endOffset == 0 && range.endContainer.nodeValue == null ) ||
1381 - ( range.endContainer.nodeName == '#text' &&
1382 - range.endOffset == range.endContainer.nodeValue.length ) ||
1383 - ( range.endContainer.nodeName == 'P' && range.endContainer.nodeValue == null ) ) {
1384 - atEnd = true;
1385 - }
1386 - if ( !atStart ) {
1387 - pre = "\n" + options.pre;
1388 - }
1389 - if ( !atEnd ) {
1390 - post += "\n";
1391 - }
1392 - }
1393 - var insertText = "";
1394 - if ( options.splitlines ) {
1395 - for( var j = 0; j < selTextArr.length; j++ ) {
1396 - insertText = insertText + pre + selTextArr[j] + post;
1397 - if( j != selTextArr.length - 1 ) {
1398 - insertText += "\n";
1399 - }
1400 - }
1401 - } else {
1402 - insertText = pre + selText + post;
1403 - }
1404 - var insertLines = insertText.split( "\n" );
1405 - range.extractContents();
1406 - // Insert the contents one line at a time - insertNode() inserts at the beginning, so this has to happen
1407 - // in reverse order
1408 - // Track the first and last inserted node, and if we need to also track where the text we need to select
1409 - // afterwards starts and ends
1410 - var firstNode = null, lastNode = null;
1411 - var selSC = null, selEC = null, selSO = null, selEO = null, offset = 0;
1412 - for ( var i = insertLines.length - 1; i >= 0; i-- ) {
1413 - firstNode = context.$iframe[0].contentWindow.document.createTextNode( insertLines[i] );
1414 - range.insertNode( firstNode );
1415 - lastNode = lastNode || firstNode;
1416 - var newOffset = offset + insertLines[i].length;
1417 - if ( !selEC && post.length <= newOffset ) {
1418 - selEC = firstNode;
1419 - selEO = selEC.nodeValue.length - ( post.length - offset );
1420 - }
1421 - if ( selEC && !selSC && pre.length >= insertText.length - newOffset ) {
1422 - selSC = firstNode;
1423 - selSO = pre.length - ( insertText.length - newOffset );
1424 - }
1425 - offset = newOffset;
1426 - if ( i > 0 ) {
1427 - firstNode = context.$iframe[0].contentWindow.document.createElement( 'br' );
1428 - range.insertNode( firstNode );
1429 - newOffset = offset + 1;
1430 - if ( !selEC && post.length <= newOffset ) {
1431 - selEC = firstNode;
1432 - selEO = 1 - ( post.length - offset );
1433 - }
1434 - if ( selEC && !selSC && pre.length >= insertText.length - newOffset ) {
1435 - selSC = firstNode;
1436 - selSO = pre.length - ( insertText.length - newOffset );
1437 - }
1438 - offset = newOffset;
1439 - }
1440 - }
1441 - if ( firstNode ) {
1442 - context.fn.scrollToTop( $( firstNode.parentNode ) );
1443 - }
1444 - if ( selectAfter ) {
1445 - setSelectionTo = {
1446 - startContainer: selSC,
1447 - endContainer: selEC,
1448 - start: selSO,
1449 - end: selEO
1450 - };
1451 - } else if ( lastNode ) {
1452 - setSelectionTo = {
1453 - startContainer: lastNode,
1454 - endContainer: lastNode,
1455 - start: lastNode.nodeValue.length,
1456 - end: lastNode.nodeValue.length
1457 - };
1458 - }
1459 - } else if ( context.$iframe[0].contentWindow.document.selection ) {
1460 - // IE
1461 - context.$iframe[0].contentWindow.focus();
1462 - var range = context.$iframe[0].contentWindow.document.selection.createRange();
1463 - if ( options.ownline && range.moveStart ) {
1464 - // Check if we're at the start of a line
1465 - // If not, prepend a newline
1466 - var range2 = context.$iframe[0].contentWindow.document.selection.createRange();
1467 - range2.collapse();
1468 - range2.moveStart( 'character', -1 );
1469 - // FIXME: Which check is correct?
1470 - if ( range2.text != "\r" && range2.text != "\n" && range2.text != "" ) {
1471 - pre = "\n" + pre;
1472 - }
1473 -
1474 - // Check if we're at the end of a line
1475 - // If not, append a newline
1476 - var range3 = context.$iframe[0].contentWindow.document.selection.createRange();
1477 - range3.collapse( false );
1478 - range3.moveEnd( 'character', 1 );
1479 - if ( range3.text != "\r" && range3.text != "\n" && range3.text != "" ) {
1480 - post += "\n";
1481 - }
1482 - }
1483 - // if our test above indicated that this was a sucessive button press, we need to collapse the
1484 - // selection to the end to avoid replacing text
1485 - if ( collapseToEnd ) {
1486 - range.collapse( false );
1487 - }
1488 - // TODO: Clean this up. Duplicate code due to the pre-existing browser specific structure of this
1489 - // function
1490 - var insertText = "";
1491 - if ( options.splitlines ) {
1492 - for( var j = 0; j < selTextArr.length; j++ ) {
1493 - insertText = insertText + pre + selTextArr[j] + post;
1494 - if( j != selTextArr.length - 1 ) {
1495 - insertText += "\n";
1496 - }
1497 - }
1498 - } else {
1499 - insertText = pre + selText + post;
1500 - }
1501 - // TODO: Maybe find a more elegant way of doing this like the Firefox code above?
1502 - range.pasteHTML( insertText
1503 - .replace( /\</g, '&lt;' )
1504 - .replace( />/g, '&gt;' )
1505 - .replace( /\r?\n/g, '<br />' )
1506 - );
1507 - if ( selectAfter ) {
1508 - range.moveStart( 'character', -post.length - selText.length );
1509 - range.moveEnd( 'character', -post.length );
1510 - range.select();
1511 - }
1512 - }
1513 -
1514 - if ( setSelectionTo ) {
1515 - context.fn.setSelection( setSelectionTo );
1516 - }
1517 - // Trigger the encapsulateSelection event (this might need to get named something else/done differently)
1518 - $( context.$iframe[0].contentWindow.document ).trigger(
1519 - 'encapsulateSelection', [ pre, options.peri, post, options.ownline, options.replace ]
1520 - );
1521 - return context.$textarea;
1522 - },
1523 - /**
1524 - * Gets the position (in resolution of bytes not nessecarily characters) in a textarea
1525 - * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
1526 - */
1527 - 'getCaretPosition': function( options ) {
1528 - var startPos = null, endPos = null;
1529 - if ( context.$iframe[0].contentWindow.getSelection ) {
1530 - var selection = context.$iframe[0].contentWindow.getSelection();
1531 - if ( selection.rangeCount == 0 ) {
1532 - // We don't know where the cursor is
1533 - return [ 0, 0 ];
1534 - }
1535 - var sc = selection.getRangeAt( 0 ).startContainer, ec = selection.getRangeAt( 0 ).endContainer;
1536 - var so = selection.getRangeAt( 0 ).startOffset, eo = selection.getRangeAt( 0 ).endOffset;
1537 - if ( sc.nodeName == 'BODY' ) {
1538 - // Grab the node just before the start of the selection
1539 - var n = sc.firstChild;
1540 - for ( var i = 0; i < so - 1 && n; i++ ) {
1541 - n = n.nextSibling;
1542 - }
1543 - sc = n;
1544 - so = 0;
1545 - }
1546 - if ( ec.nodeName == 'BODY' ) {
1547 - var n = ec.firstChild;
1548 - for ( var i = 0; i < eo - 1 && n; i++ ) {
1549 - n = n.nextSibling;
1550 - }
1551 - ec = n;
1552 - eo = 0;
1553 - }
1554 -
1555 - // Make sure sc and ec are leaf nodes
1556 - while ( sc.firstChild ) {
1557 - sc = sc.firstChild;
1558 - }
1559 - while ( ec.firstChild ) {
1560 - ec = ec.firstChild;
1561 - }
1562 - // Make sure the offsets are regenerated if necessary
1563 - context.fn.getOffset( 0 );
1564 - var o;
1565 - for ( o in context.offsets ) {
1566 - if ( startPos === null && context.offsets[o].node == sc ) {
1567 - // For some wicked reason o is a string, even though
1568 - // we put it in as an integer. Use ~~ to coerce it too an int
1569 - startPos = ~~o + so - context.offsets[o].offset;
1570 - }
1571 - if ( startPos !== null && context.offsets[o].node == ec ) {
1572 - endPos = ~~o + eo - context.offsets[o].offset;
1573 - break;
1574 - }
1575 - }
1576 - } else if ( context.$iframe[0].contentWindow.document.selection ) {
1577 - // IE
1578 - // FIXME: This is mostly copypasted from the textSelection plugin
1579 - var d = context.$iframe[0].contentWindow.document;
1580 - var postFinished = false;
1581 - var periFinished = false;
1582 - var postFinished = false;
1583 - var preText, rawPreText, periText;
1584 - var rawPeriText, postText, rawPostText;
1585 - // Depending on the document state, and if the cursor has ever been manually placed within the document
1586 - // the following call such as setEndPoint can result in nasty errors. These cases are always cases
1587 - // in which the start and end points can safely be assumed to be 0, so we will just try our best to do
1588 - // the full process but fall back to 0.
1589 - try {
1590 - // Create range containing text in the selection
1591 - var periRange = d.selection.createRange().duplicate();
1592 - // Create range containing text before the selection
1593 - var preRange = d.body.createTextRange();
1594 - // Move the end where we need it
1595 - preRange.setEndPoint( "EndToStart", periRange );
1596 - // Create range containing text after the selection
1597 - var postRange = d.body.createTextRange();
1598 - // Move the start where we need it
1599 - postRange.setEndPoint( "StartToEnd", periRange );
1600 - // Load the text values we need to compare
1601 - preText = rawPreText = preRange.text;
1602 - periText = rawPeriText = periRange.text;
1603 - postText = rawPostText = postRange.text;
1604 - /*
1605 - * Check each range for trimmed newlines by shrinking the range by 1
1606 - * character and seeing if the text property has changed. If it has
1607 - * not changed then we know that IE has trimmed a \r\n from the end.
1608 - */
1609 - do {
1610 - if ( !postFinished ) {
1611 - if ( preRange.compareEndPoints( "StartToEnd", preRange ) == 0 ) {
1612 - postFinished = true;
1613 - } else {
1614 - preRange.moveEnd( "character", -1 )
1615 - if ( preRange.text == preText ) {
1616 - rawPreText += "\r\n";
1617 - } else {
1618 - postFinished = true;
1619 - }
1620 - }
1621 - }
1622 - if ( !periFinished ) {
1623 - if ( periRange.compareEndPoints( "StartToEnd", periRange ) == 0 ) {
1624 - periFinished = true;
1625 - } else {
1626 - periRange.moveEnd( "character", -1 )
1627 - if ( periRange.text == periText ) {
1628 - rawPeriText += "\r\n";
1629 - } else {
1630 - periFinished = true;
1631 - }
1632 - }
1633 - }
1634 - if ( !postFinished ) {
1635 - if ( postRange.compareEndPoints("StartToEnd", postRange) == 0 ) {
1636 - postFinished = true;
1637 - } else {
1638 - postRange.moveEnd( "character", -1 )
1639 - if ( postRange.text == postText ) {
1640 - rawPostText += "\r\n";
1641 - } else {
1642 - postFinished = true;
1643 - }
1644 - }
1645 - }
1646 - } while ( ( !postFinished || !periFinished || !postFinished ) );
1647 - startPos = rawPreText.replace( /\r\n/g, "\n" ).length;
1648 - endPos = startPos + rawPeriText.replace( /\r\n/g, "\n" ).length;
1649 - } catch( e ) {
1650 - startPos = endPos = 0;
1651 - }
1652 - }
1653 - return [ startPos, endPos ];
1654 - },
1655 - /**
1656 - * Sets the selection of the content
1657 - * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
1658 - *
1659 - * @param start Character offset of selection start
1660 - * @param end Character offset of selection end
1661 - * @param startContainer Element in iframe to start selection in. If not set, start is a character offset
1662 - * @param endContainer Element in iframe to end selection in. If not set, end is a character offset
1663 - */
1664 - 'setSelection': function( options ) {
1665 - var sc = options.startContainer, ec = options.endContainer;
1666 - sc = sc && sc.jquery ? sc[0] : sc;
1667 - ec = ec && ec.jquery ? ec[0] : ec;
1668 - if ( context.$iframe[0].contentWindow.getSelection ) {
1669 - // Firefox and Opera
1670 - var start = options.start, end = options.end;
1671 - if ( !sc || !ec ) {
1672 - var s = context.fn.getOffset( start );
1673 - var e = context.fn.getOffset( end );
1674 - sc = s ? s.node : null;
1675 - ec = e ? e.node : null;
1676 - start = s ? s.offset : null;
1677 - end = e ? e.offset : null;
1678 - // Don't try to set the selection past the end of a node, causes errors
1679 - // Just put the selection at the end of the node in this case
1680 - if ( sc != null && sc.nodeName == '#text' && start > sc.nodeValue.length ) {
1681 - start = sc.nodeValue.length - 1;
1682 - }
1683 - if ( ec != null && ec.nodeName == '#text' && end > ec.nodeValue.length ) {
1684 - end = ec.nodeValue.length - 1;
1685 - }
1686 - }
1687 - if ( !sc || !ec ) {
1688 - // The requested offset isn't in the offsets array
1689 - // Give up
1690 - return context.$textarea;
1691 - }
1692 -
1693 - var sel = context.$iframe[0].contentWindow.getSelection();
1694 - while ( sc.firstChild && sc.nodeName != '#text' ) {
1695 - sc = sc.firstChild;
1696 - }
1697 - while ( ec.firstChild && ec.nodeName != '#text' ) {
1698 - ec = ec.firstChild;
1699 - }
1700 - var range = context.$iframe[0].contentWindow.document.createRange();
1701 - range.setStart( sc, start );
1702 - range.setEnd( ec, end );
1703 - sel.removeAllRanges();
1704 - sel.addRange( range );
1705 - context.$iframe[0].contentWindow.focus();
1706 - } else if ( context.$iframe[0].contentWindow.document.body.createTextRange ) {
1707 - // IE
1708 - var range = context.$iframe[0].contentWindow.document.body.createTextRange();
1709 - if ( sc ) {
1710 - range.moveToElementText( sc );
1711 - }
1712 - range.collapse();
1713 - range.moveEnd( 'character', options.start );
1714 -
1715 - var range2 = context.$iframe[0].contentWindow.document.body.createTextRange();
1716 - if ( ec ) {
1717 - range2.moveToElementText( ec );
1718 - }
1719 - range2.collapse();
1720 - range2.moveEnd( 'character', options.end );
1721 -
1722 - // IE does newline emulation for <p>s: <p>foo</p><p>bar</p> becomes foo\nbar just fine
1723 - // but <p>foo</p><br><br><p>bar</p> becomes foo\n\n\n\nbar , one \n too many
1724 - // Correct for this
1725 - var matches, counted = 0;
1726 - // while ( matches = range.htmlText.match( regex ) && matches.length <= counted ) doesn't work
1727 - // because the assignment side effect hasn't happened yet when the second term is evaluated
1728 - while ( matches = range.htmlText.match( /\<\/p\>(\<br[^\>]*\>)+\<p\>/gi ) ) {
1729 - if ( matches.length <= counted )
1730 - break;
1731 - range.moveEnd( 'character', matches.length );
1732 - counted += matches.length;
1733 - }
1734 - range2.moveEnd( 'character', counted );
1735 - while ( matches = range2.htmlText.match( /\<\/p\>(\<br[^\>]*\>)+\<p\>/gi ) ) {
1736 - if ( matches.length <= counted )
1737 - break;
1738 - range2.moveEnd( 'character', matches.length );
1739 - counted += matches.length;
1740 - }
1741 -
1742 - range2.setEndPoint( 'StartToEnd', range );
1743 - range2.select();
1744 - }
1745 - return context.$textarea;
1746 - },
1747 - /**
1748 - * Scroll a textarea to the current cursor position. You can set the cursor position with setSelection()
1749 - * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
1750 - */
1751 - 'scrollToCaretPosition': function( options ) {
1752 - context.fn.scrollToTop( context.fn.getElementAtCursor(), true );
1753 - },
1754 - /**
1755 - * Scroll an element to the top of the iframe
1756 - * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
1757 - *
1758 - * @param $element jQuery object containing an element in the iframe
1759 - * @param force If true, scroll the element even if it's already visible
1760 - */
1761 - 'scrollToTop': function( $element, force ) {
1762 - var html = context.$content.closest( 'html' ),
1763 - body = context.$content.closest( 'body' ),
1764 - parentHtml = $( 'html' ),
1765 - parentBody = $( 'body' );
1766 - var y = $element.offset().top;
1767 - if ( !$.browser.msie && ! $element.is( 'body' ) ) {
1768 - y = parentHtml.scrollTop() > 0 ? y + html.scrollTop() - parentHtml.scrollTop() : y;
1769 - y = parentBody.scrollTop() > 0 ? y + body.scrollTop() - parentBody.scrollTop() : y;
1770 - }
1771 - var topBound = html.scrollTop() > body.scrollTop() ? html.scrollTop() : body.scrollTop(),
1772 - bottomBound = topBound + context.$iframe.height();
1773 - if ( force || y < topBound || y > bottomBound ) {
1774 - html.scrollTop( y );
1775 - body.scrollTop( y );
1776 - }
1777 - $element.trigger( 'scrollToTop' );
1778 - },
1779 - /**
1780490 * Save scrollTop and cursor position for IE.
1781491 */
1782492 'saveStuffForIE': function() {
@@ -1862,19 +572,26 @@
1863573 // Since javascript gives arguments as an object, we need to convert them so they can be used more easily
1864574 var args = $.makeArray( arguments );
1865575
1866 -// Dynamically setup the Iframe when needed when adding modules
1867 -if ( typeof context.$iframe === 'undefined' && args[0] == 'addModule' && typeof args[1] != 'undefined' ) {
 576+// Dynamically setup core extensions for modules that are required
 577+if ( args[0] == 'addModule' && typeof args[1] != 'undefined' ) {
1868578 var modules = args[1];
1869579 if ( typeof modules != "object" ) {
1870580 modules = {};
1871581 modules[args[1]] = '';
1872582 }
1873583 for ( module in modules ) {
1874 - // Only allow modules which are supported (and thus actually being turned on) affect this decision
1875 - if ( module in $.wikiEditor.modules && $.wikiEditor.isSupported( $.wikiEditor.modules[module] ) &&
1876 - $.wikiEditor.isRequired( $.wikiEditor.modules[module], 'iframe' ) ) {
1877 -
1878 - context.fn.setupIframe();
 584+ // Only allow modules which are supported (and thus actually being turned on) affect the decision to extend
 585+ if ( module in $.wikiEditor.modules && $.wikiEditor.isSupported( $.wikiEditor.modules[module] ) ) {
 586+ // Activate all required core extensions on context
 587+ for ( e in $.wikiEditor.extensions ) {
 588+ if (
 589+ $.wikiEditor.isRequired( $.wikiEditor.modules[module], e ) &&
 590+ context.extensions.indexOf( e ) === -1
 591+ ) {
 592+ context.extensions[context.extensions.length] = e;
 593+ $.wikiEditor.extensions[e]( context );
 594+ }
 595+ }
1879596 break;
1880597 }
1881598 }
Index: trunk/extensions/WikiEditor/modules/jquery.wikiEditor.iframe.js
@@ -0,0 +1,1317 @@
 2+/* IFrame extension for wikiEditor */
 3+
 4+( function( $ ) { $.wikiEditor.extensions.iframe = function( context ) {
 5+
 6+/*
 7+ * Event Handlers
 8+ *
 9+ * These act as filters returning false if the event should be ignored or returning true if it should be passed
 10+ * on to all modules. This is also where we can attach some extra information to the events.
 11+ */
 12+context.evt = $.extend( context.evt, {
 13+ 'change': function( event ) {
 14+ event.data.scope = 'division';
 15+ var newHTML = context.$content.html();
 16+ if ( context.oldHTML != newHTML ) {
 17+ context.fn.purgeOffsets();
 18+ context.oldHTML = newHTML;
 19+ event.data.scope = 'realchange';
 20+ }
 21+ // Never let the body be totally empty
 22+ if ( context.$content.children().length == 0 ) {
 23+ context.$content.append( '<p></p>' );
 24+ }
 25+ return true;
 26+ },
 27+ 'delayedChange': function( event ) {
 28+ event.data.scope = 'division';
 29+ var newHTML = context.$content.html();
 30+ if ( context.oldDelayedHTML != newHTML ) {
 31+ context.oldDelayedHTML = newHTML;
 32+ event.data.scope = 'realchange';
 33+ // Surround by <p> if it does not already have it
 34+ var cursorPos = context.fn.getCaretPosition();
 35+ var t = context.fn.getOffset( cursorPos[0] );
 36+ if ( ! $.browser.msie && t && t.node.nodeName == '#text' && t.node.parentNode.nodeName.toLowerCase() == 'body' ) {
 37+ $( t.node ).wrap( "<p></p>" );
 38+ context.fn.purgeOffsets();
 39+ context.fn.setSelection( { start: cursorPos[0], end: cursorPos[1] } );
 40+ }
 41+ }
 42+ context.fn.updateHistory( event.data.scope == 'realchange' );
 43+ return true;
 44+ },
 45+ 'cut': function( event ) {
 46+ setTimeout( function() {
 47+ context.$content.find( 'br' ).each( function() {
 48+ if ( $(this).parent().is( 'body' ) ) {
 49+ $(this).wrap( $( '<p></p>' ) );
 50+ }
 51+ } );
 52+ }, 100 );
 53+ return true;
 54+ },
 55+ 'paste': function( event ) {
 56+ // Save the cursor position to restore it after all this voodoo
 57+ var cursorPos = context.fn.getCaretPosition();
 58+ var oldLength = context.fn.getContents().length;
 59+ var positionFromEnd = oldLength - cursorPos[1];
 60+
 61+ //give everything the wikiEditor class so that we can easily pick out things without that class as pasted
 62+ context.$content.find( '*' ).addClass( 'wikiEditor' );
 63+ if ( $.layout.name !== 'webkit' ) {
 64+ context.$content.addClass( 'pasting' );
 65+ }
 66+
 67+ setTimeout( function() {
 68+ // Kill stuff we know we don't want
 69+ context.$content.find( 'script,style,img,input,select,textarea,hr,button,link,meta' ).remove();
 70+ var nodeToDelete = [];
 71+ var pastedContent = [];
 72+ var firstDirtyNode;
 73+ var $lastDirtyNode;
 74+ var elementAtCursor;
 75+ if ( $.browser.msie && !context.offsets ) {
 76+ elementAtCursor = null;
 77+ } else {
 78+ elementAtCursor = context.fn.getOffset( cursorPos[0] );
 79+ }
 80+ if ( elementAtCursor == null || elementAtCursor.node == null ) {
 81+ context.$content.prepend( '<p class = wikiEditor></p>' );
 82+ firstDirtyNode = context.$content.children()[0];
 83+ } else {
 84+ firstDirtyNode = elementAtCursor.node;
 85+ }
 86+
 87+ //this is ugly but seems like the best way to handle the case where we select and replace all editor contents
 88+ try {
 89+ firstDirtyNode.parentNode;
 90+ } catch ( err ) {
 91+ context.$content.prepend( '<p class = wikiEditor></p>' );
 92+ firstDirtyNode = context.$content.children()[0];
 93+ }
 94+
 95+ while ( firstDirtyNode != null ) {
 96+ //we're going to replace the contents of the entire parent node.
 97+ while ( firstDirtyNode.parentNode && firstDirtyNode.parentNode.nodeName != 'BODY'
 98+ && ! $( firstDirtyNode ).hasClass( 'wikiEditor' )
 99+ ) {
 100+ firstDirtyNode = firstDirtyNode.parentNode;
 101+ }
 102+ //go back till we find the first pasted node
 103+ while ( firstDirtyNode.previousSibling != null
 104+ && ! $( firstDirtyNode.previousSibling ).hasClass( 'wikiEditor' )
 105+ ) {
 106+
 107+ if ( $( firstDirtyNode.previousSibling ).hasClass( '#comment' ) ) {
 108+ $( firstDirtyNode ).remove();
 109+ } else {
 110+ firstDirtyNode = firstDirtyNode.previousSibling;
 111+ }
 112+ }
 113+
 114+ if ( firstDirtyNode.previousSibling != null ) {
 115+ $lastDirtyNode = $( firstDirtyNode.previousSibling );
 116+ } else {
 117+ $lastDirtyNode = $( firstDirtyNode );
 118+ }
 119+
 120+ var cc = makeContentCollector( $.browser, null );
 121+ while ( firstDirtyNode != null ) {
 122+ cc.collectContent(firstDirtyNode);
 123+ cc.notifyNextNode(firstDirtyNode.nextSibling);
 124+
 125+ nodeToDelete.push( firstDirtyNode );
 126+
 127+ firstDirtyNode = firstDirtyNode.nextSibling;
 128+ if ( $( firstDirtyNode ).hasClass( 'wikiEditor' ) ) {
 129+ break;
 130+ }
 131+ }
 132+
 133+ var ccData = cc.finish();
 134+ pastedContent = ccData.lines;
 135+ var pastedPretty = '';
 136+ for ( var i = 0; i < pastedContent.length; i++ ) {
 137+ //escape html
 138+ pastedPretty = pastedContent[i].replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\r?\n/g, '\\n');
 139+ //replace leading white spaces with &nbsp;
 140+ match = pastedContent[i].match(/^[\s]+[^\s]/);
 141+ if ( match != null && match.length > 0 ) {
 142+ index = match[0].length;
 143+ leadingSpace = match[0].replace(/[\s]/g, '&nbsp;');
 144+ pastedPretty = leadingSpace + pastedPretty.substring(index, pastedPretty.length);
 145+ }
 146+
 147+
 148+ if( !pastedPretty && $.browser.msie && i == 0 ) {
 149+ continue;
 150+ }
 151+ $newElement = $( '<p class="wikiEditor pasted" ></p>' );
 152+ if ( pastedPretty ) {
 153+ $newElement.html( pastedPretty );
 154+ } else {
 155+ $newElement.html( '<br class="wikiEditor">' );
 156+ }
 157+ $newElement.insertAfter( $lastDirtyNode );
 158+
 159+ $lastDirtyNode = $newElement;
 160+
 161+ }
 162+
 163+ //now delete all the original nodes that we prettified already
 164+ while ( nodeToDelete.length > 0 ) {
 165+ $deleteNode = $( nodeToDelete.pop() );
 166+ $deleteNode.remove();
 167+ }
 168+
 169+ //anything without wikiEditor class was pasted.
 170+ $selection = context.$content.find( ':not(.wikiEditor)' );
 171+ if ( $selection.length == 0 ) {
 172+ break;
 173+ } else {
 174+ firstDirtyNode = $selection.eq( 0 )[0];
 175+ }
 176+ }
 177+ context.$content.find( '.wikiEditor' ).removeClass( 'wikiEditor' );
 178+
 179+ //now place the cursor at the end of pasted content
 180+ var newLength = context.fn.getContents().length;
 181+ var newPos = newLength - positionFromEnd;
 182+
 183+ context.fn.purgeOffsets();
 184+ context.fn.setSelection( { start: newPos, end: newPos } );
 185+
 186+ context.fn.scrollToCaretPosition();
 187+ }, 0 );
 188+ return true;
 189+ },
 190+ 'ready': function( event ) {
 191+ // Initialize our history queue
 192+ if ( context.$content ) {
 193+ context.history.push( { 'html': context.$content.html(), 'sel': context.fn.getCaretPosition() } );
 194+ } else {
 195+ context.history.push( { 'html': '', 'sel': context.fn.getCaretPosition() } );
 196+ }
 197+ return true;
 198+ }
 199+} );
 200+
 201+/**
 202+ * Internally used functions
 203+ */
 204+context.fn = $.extend( context.fn, {
 205+ 'highlightLine': function( $element, mode ) {
 206+ if ( !$element.is( 'p' ) ) {
 207+ $element = $element.closest( 'p' );
 208+ }
 209+ $element.css( 'backgroundColor', '#AACCFF' );
 210+ setTimeout( function() { $element.animate( { 'backgroundColor': 'white' }, 'slow' ); }, 100 );
 211+ setTimeout( function() { $element.css( 'backgroundColor', 'white' ); }, 1000 );
 212+ },
 213+ 'htmlToText': function( html ) {
 214+ // This function is slow for large inputs, so aggressively cache input/output pairs
 215+ if ( html in context.htmlToTextMap ) {
 216+ return context.htmlToTextMap[html];
 217+ }
 218+ var origHTML = html;
 219+
 220+ // We use this elaborate trickery for cross-browser compatibility
 221+ // IE does overzealous whitespace collapsing for $( '<pre />' ).html( html );
 222+ // We also do <br> and easy cases for <p> conversion here, complicated cases are handled later
 223+ html = html
 224+ .replace( /\r?\n/g, "" ) // IE7 inserts newlines before block elements
 225+ .replace( /&nbsp;/g, " " ) // We inserted these to prevent IE from collapsing spaces
 226+ .replace( /\<br[^\>]*\>\<\/p\>/gi, '</p>' ) // Remove trailing <br> from <p>
 227+ .replace( /\<\/p\>\s*\<p[^\>]*\>/gi, "\n" ) // Easy case for <p> conversion
 228+ .replace( /\<br[^\>]*\>/gi, "\n" ) // <br> conversion
 229+ .replace( /\<\/p\>(\n*)\<p[^\>]*\>/gi, "$1\n" )
 230+ // Un-nest <p> tags
 231+ .replace( /\<p[^\>]*\><p[^\>]*\>/gi, '<p>' )
 232+ .replace( /\<\/p\><\/p\>/gi, '</p>' );
 233+ // Save leading and trailing whitespace now and restore it later. IE eats it all, and even Firefox
 234+ // won't leave everything alone
 235+ var leading = html.match( /^\s*/ )[0];
 236+ var trailing = html.match( /\s*$/ )[0];
 237+ html = html.substr( leading.length, html.length - leading.length - trailing.length );
 238+ var $pre = $( '<pre>' + html + '</pre>' );
 239+ $pre.find( '.wikiEditor-noinclude' ).each( function() { $( this ).remove(); } );
 240+ // Convert tabs, <p>s and <br>s back
 241+ $pre.find( '.wikiEditor-tab' ).each( function() { $( this ).text( "\t" ); } );
 242+ $pre.find( 'br' ).each( function() { $( this ).replaceWith( "\n" ); } );
 243+ // Converting <p>s is wrong if there's nothing before them, so check that.
 244+ // .find( '* + p' ) isn't good enough because textnodes aren't considered
 245+ $pre.find( 'p' ).each( function() {
 246+ var text = $( this ).text();
 247+ // If this <p> is preceded by some text, add a \n at the beginning, and if
 248+ // it's followed by a textnode, add a \n at the end
 249+ // We need the traverser because there can be other weird stuff in between
 250+
 251+ // Check for preceding text
 252+ var t = new context.fn.rawTraverser( this.firstChild, this, $pre.get( 0 ), true ).prev();
 253+ while ( t && t.node.nodeName != '#text' && t.node.nodeName != 'BR' && t.node.nodeName != 'P' ) {
 254+ t = t.prev();
 255+ }
 256+ if ( t ) {
 257+ text = "\n" + text;
 258+ }
 259+
 260+ // Check for following text
 261+ t = new context.fn.rawTraverser( this.lastChild, this, $pre.get( 0 ), true ).next();
 262+ while ( t && t.node.nodeName != '#text' && t.node.nodeName != 'BR' && t.node.nodeName != 'P' ) {
 263+ t = t.next();
 264+ }
 265+ if ( t && !t.inP && t.node.nodeName == '#text' && t.node.nodeValue.charAt( 0 ) != '\n'
 266+ && t.node.nodeValue.charAt( 0 ) != '\r' ) {
 267+ text += "\n";
 268+ }
 269+ $( this ).text( text );
 270+ } );
 271+ var retval;
 272+ if ( $.browser.msie ) {
 273+ // IE aggressively collapses whitespace in .text() after having done DOM manipulation,
 274+ // but for some crazy reason this does work. Also convert \r back to \n
 275+ retval = $( '<pre>' + $pre.html() + '</pre>' ).text().replace( /\r/g, '\n' );
 276+ } else {
 277+ retval = $pre.text();
 278+ }
 279+ return context.htmlToTextMap[origHTML] = leading + retval + trailing;
 280+ },
 281+ /**
 282+ * Get the first element before the selection that's in a certain class
 283+ * @param classname Class to match. Defaults to '', meaning any class
 284+ * @param strict If true, the element the selection starts in cannot match (default: false)
 285+ * @return jQuery object or null if unknown
 286+ */
 287+ 'beforeSelection': function( classname, strict ) {
 288+ if ( typeof classname == 'undefined' ) {
 289+ classname = '';
 290+ }
 291+ var e = null, offset = null;
 292+ if ( $.browser.msie && !context.$iframe[0].contentWindow.document.body ) {
 293+ return null;
 294+ }
 295+ if ( context.$iframe[0].contentWindow.getSelection ) {
 296+ // Firefox and Opera
 297+ var selection = context.$iframe[0].contentWindow.getSelection();
 298+ // On load, webkit seems to not have a valid selection
 299+ if ( selection.baseNode !== null ) {
 300+ // Start at the selection's start and traverse the DOM backwards
 301+ // This is done by traversing an element's children first, then the element itself, then its parent
 302+ e = selection.getRangeAt( 0 ).startContainer;
 303+ offset = selection.getRangeAt( 0 ).startOffset;
 304+ } else {
 305+ return null;
 306+ }
 307+
 308+ // When the cursor is on an empty line, Opera gives us a bogus range object with
 309+ // startContainer=endContainer=body and startOffset=endOffset=1
 310+ var body = context.$iframe[0].contentWindow.document.body;
 311+ if ( $.browser.opera && e == body && offset == 1 ) {
 312+ return null;
 313+ }
 314+ }
 315+ if ( !e && context.$iframe[0].contentWindow.document.selection ) {
 316+ // IE
 317+ // Because there's nothing like range.startContainer in IE, we need to do a DOM traversal
 318+ // to find the element the start of the selection is in
 319+ var range = context.$iframe[0].contentWindow.document.selection.createRange();
 320+ // Set range2 to the text before the selection
 321+ var range2 = context.$iframe[0].contentWindow.document.body.createTextRange();
 322+ // For some reason this call throws errors in certain cases, e.g. when the selection is
 323+ // not in the iframe
 324+ try {
 325+ range2.setEndPoint( 'EndToStart', range );
 326+ } catch ( ex ) {
 327+ return null;
 328+ }
 329+ var seekPos = context.fn.htmlToText( range2.htmlText ).length;
 330+ var offset = context.fn.getOffset( seekPos );
 331+ e = offset ? offset.node : null;
 332+ offset = offset ? offset.offset : null;
 333+ if ( !e ) {
 334+ return null;
 335+ }
 336+ }
 337+ if ( e.nodeName != '#text' ) {
 338+ // The selection is not in a textnode, but between two non-text nodes
 339+ // (usually inside the <body> between two <br>s). Go to the rightmost
 340+ // child of the node just before the selection
 341+ var newE = e.firstChild;
 342+ for ( var i = 0; i < offset - 1 && newE; i++ ) {
 343+ newE = newE.nextSibling;
 344+ }
 345+ while ( newE && newE.lastChild ) {
 346+ newE = newE.lastChild;
 347+ }
 348+ e = newE || e;
 349+ }
 350+
 351+ // We'd normally use if( $( e ).hasClass( class ) in the while loop, but running the jQuery
 352+ // constructor thousands of times is very inefficient
 353+ var classStr = ' ' + classname + ' ';
 354+ while ( e ) {
 355+ if ( !strict && ( !classname || ( ' ' + e.className + ' ' ).indexOf( classStr ) != -1 ) ) {
 356+ return $( e );
 357+ }
 358+ var next = e.previousSibling;
 359+ while ( next && next.lastChild ) {
 360+ next = next.lastChild;
 361+ }
 362+ e = next || e.parentNode;
 363+ strict = false;
 364+ }
 365+ return $( [] );
 366+ },
 367+ /**
 368+ * Object used by traverser(). Don't use this unless you know what you're doing
 369+ */
 370+ 'rawTraverser': function( node, inP, ancestor, skipNoinclude ) {
 371+ this.node = node;
 372+ this.inP = inP;
 373+ this.ancestor = ancestor;
 374+ this.skipNoinclude = skipNoinclude;
 375+ this.next = function() {
 376+ var p = this.node;
 377+ var nextInP = this.inP;
 378+ while ( p && !p.nextSibling ) {
 379+ p = p.parentNode;
 380+ if ( p == this.ancestor ) {
 381+ // We're back at the ancestor, stop here
 382+ p = null;
 383+ }
 384+ if ( p && p.nodeName == "P" ) {
 385+ nextInP = null;
 386+ }
 387+ }
 388+ p = p ? p.nextSibling : null;
 389+ if ( p && p.nodeName == "P" ) {
 390+ nextInP = p;
 391+ }
 392+ do {
 393+ // Filter nodes with the wikiEditor-noinclude class
 394+ // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because
 395+ // $() is slow in a tight loop
 396+ if ( this.skipNoinclude ) {
 397+ while ( p && ( ' ' + p.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) {
 398+ p = p.nextSibling;
 399+ }
 400+ }
 401+ if ( p && p.firstChild ) {
 402+ p = p.firstChild;
 403+ if ( p.nodeName == "P" ) {
 404+ nextInP = p;
 405+ }
 406+ }
 407+ } while ( p && p.firstChild );
 408+ // Instead of calling the rawTraverser constructor, inline it. This avoids function call overhead
 409+ return p ? { 'node': p, 'inP': nextInP, 'ancestor': this.ancestor,
 410+ 'skipNoinclude': this.skipNoinclude, 'next': this.next, 'prev': this.prev } : null;
 411+ };
 412+ this.prev = function() {
 413+ var p = this.node;
 414+ var prevInP = this.inP;
 415+ while ( p && !p.previousSibling ) {
 416+ p = p.parentNode;
 417+ if ( p == this.ancestor ) {
 418+ // We're back at the ancestor, stop here
 419+ p = null;
 420+ }
 421+ if ( p && p.nodeName == "P" ) {
 422+ prevInP = null;
 423+ }
 424+ }
 425+ p = p ? p.previousSibling : null;
 426+ if ( p && p.nodeName == "P" ) {
 427+ prevInP = p;
 428+ }
 429+ do {
 430+ // Filter nodes with the wikiEditor-noinclude class
 431+ // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because
 432+ // $() is slow in a tight loop
 433+ if ( this.skipNoinclude ) {
 434+ while ( p && ( ' ' + p.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) {
 435+ p = p.previousSibling;
 436+ }
 437+ }
 438+ if ( p && p.lastChild ) {
 439+ p = p.lastChild;
 440+ if ( p.nodeName == "P" ) {
 441+ prevInP = p;
 442+ }
 443+ }
 444+ } while ( p && p.lastChild );
 445+ // Instead of calling the rawTraverser constructor, inline it. This avoids function call overhead
 446+ return p ? { 'node': p, 'inP': prevInP, 'ancestor': this.ancestor,
 447+ 'skipNoinclude': this.skipNoinclude, 'next': this.next, 'prev': this.prev } : null;
 448+ };
 449+ },
 450+ /**
 451+ * Get an object used to traverse the leaf nodes in the iframe DOM. This traversal skips leaf nodes
 452+ * inside an element with the wikiEditor-noinclude class. This basically wraps rawTraverser
 453+ *
 454+ * @param start Node to start at
 455+ * @return Traverser object, use .next() or .prev() to get a traverser object referring to the
 456+ * previous/next node
 457+ */
 458+ 'traverser': function( start ) {
 459+ // Find the leftmost leaf node in the tree
 460+ var startNode = start.jquery ? start.get( 0 ) : start;
 461+ var node = startNode;
 462+ var inP = node.nodeName == "P" ? node : null;
 463+ do {
 464+ // Filter nodes with the wikiEditor-noinclude class
 465+ // Don't use $( p ).hasClass( 'wikiEditor-noinclude' ) because
 466+ // $() is slow in a tight loop
 467+ while ( node && ( ' ' + node.className + ' ' ).indexOf( ' wikiEditor-noinclude ' ) != -1 ) {
 468+ node = node.nextSibling;
 469+ }
 470+ if ( node && node.firstChild ) {
 471+ node = node.firstChild;
 472+ if ( node.nodeName == "P" ) {
 473+ inP = node;
 474+ }
 475+ }
 476+ } while ( node && node.firstChild );
 477+ return new context.fn.rawTraverser( node, inP, startNode, true );
 478+ },
 479+ 'getOffset': function( offset ) {
 480+ if ( !context.offsets ) {
 481+ context.fn.refreshOffsets();
 482+ }
 483+ if ( offset in context.offsets ) {
 484+ return context.offsets[offset];
 485+ }
 486+ // Our offset is not pre-cached. Find the highest offset below it and interpolate
 487+ // We need to traverse the entire object because for() doesn't traverse in order
 488+ // We don't do in-order traversal because the object is sparse
 489+ var lowerBound = -1;
 490+ for ( var o in context.offsets ) {
 491+ var realO = parseInt( o );
 492+ if ( realO < offset && realO > lowerBound) {
 493+ lowerBound = realO;
 494+ }
 495+ }
 496+ if ( !( lowerBound in context.offsets ) ) {
 497+ // Weird edge case: either offset is too large or the document is empty
 498+ return null;
 499+ }
 500+ var base = context.offsets[lowerBound];
 501+ return context.offsets[offset] = {
 502+ 'node': base.node,
 503+ 'offset': base.offset + offset - lowerBound,
 504+ 'length': base.length,
 505+ 'lastTextNode': base.lastTextNode
 506+ };
 507+ },
 508+ 'purgeOffsets': function() {
 509+ context.offsets = null;
 510+ },
 511+ 'refreshOffsets': function() {
 512+ context.offsets = [ ];
 513+ var t = context.fn.traverser( context.$content );
 514+ var pos = 0, lastTextNode = null;
 515+ while ( t ) {
 516+ if ( t.node.nodeName != '#text' && t.node.nodeName != 'BR' ) {
 517+ t = t.next();
 518+ continue;
 519+ }
 520+ var nextPos = t.node.nodeName == '#text' ? pos + t.node.nodeValue.length : pos + 1;
 521+ var nextT = t.next();
 522+ var leavingP = t.node.nodeName == '#text' && t.inP && nextT && ( !nextT.inP || nextT.inP != t.inP );
 523+ context.offsets[pos] = {
 524+ 'node': t.node,
 525+ 'offset': 0,
 526+ 'length': nextPos - pos + ( leavingP ? 1 : 0 ),
 527+ 'lastTextNode': lastTextNode
 528+ };
 529+ if ( leavingP ) {
 530+ // <p>Foo</p> looks like "Foo\n", make it quack like it too
 531+ // Basically we're faking the \n character much like we're treating <br>s
 532+ context.offsets[nextPos] = {
 533+ 'node': t.node,
 534+ 'offset': nextPos - pos,
 535+ 'length': nextPos - pos + 1,
 536+ 'lastTextNode': lastTextNode
 537+ };
 538+ }
 539+ pos = nextPos + ( leavingP ? 1 : 0 );
 540+ if ( t.node.nodeName == '#text' ) {
 541+ lastTextNode = t.node;
 542+ }
 543+ t = nextT;
 544+ }
 545+ },
 546+ 'saveSelection': function() {
 547+ if ( !$.browser.msie ) {
 548+ // Only IE needs this
 549+ return;
 550+ }
 551+ if ( typeof context.$iframe != 'undefined' ) {
 552+ context.$iframe[0].contentWindow.focus();
 553+ context.savedSelection = context.$iframe[0].contentWindow.document.selection.createRange();
 554+ } else {
 555+ context.$textarea.focus();
 556+ context.savedSelection = document.selection.createRange();
 557+ }
 558+ },
 559+ 'restoreSelection': function() {
 560+ if ( !$.browser.msie || context.savedSelection === null ) {
 561+ return;
 562+ }
 563+ if ( typeof context.$iframe != 'undefined' ) {
 564+ context.$iframe[0].contentWindow.focus();
 565+ } else {
 566+ context.$textarea.focus();
 567+ }
 568+ context.savedSelection.select();
 569+ context.savedSelection = null;
 570+ },
 571+ /**
 572+ * Update the history queue
 573+ *
 574+ * @param htmlChange pass true or false to inidicate if there was a text change that should potentially
 575+ * be given a new history state.
 576+ */
 577+ 'updateHistory': function( htmlChange ) {
 578+ var newHTML = context.$content.html();
 579+ var newSel = context.fn.getCaretPosition();
 580+ // Was text changed? Was it because of a REDO or UNDO action?
 581+ if (
 582+ context.history.length == 0 ||
 583+ ( htmlChange && context.oldDelayedHistoryPosition == context.historyPosition )
 584+ ) {
 585+ context.oldDelayedSel = newSel;
 586+ // Do we need to trim extras from our history?
 587+ // FIXME: this should really be happing on change, not on the delay
 588+ if ( context.historyPosition < -1 ) {
 589+ //clear out the extras
 590+ context.history.splice( context.history.length + context.historyPosition + 1 );
 591+ context.historyPosition = -1;
 592+ }
 593+ context.history.push( { 'html': newHTML, 'sel': newSel } );
 594+ // If the history has grown longer than 10 items, remove the earliest one
 595+ while ( context.history.length > 10 ) {
 596+ context.history.shift();
 597+ }
 598+ } else if ( context.oldDelayedSel != newSel ) {
 599+ // If only the selection was changed, update it
 600+ context.oldDelayedSel = newSel;
 601+ context.history[context.history.length + context.historyPosition].sel = newSel;
 602+ }
 603+ // synch our old delayed history position until the next undo/redo action
 604+ context.oldDelayedHistoryPosition = context.historyPosition;
 605+ },
 606+ /**
 607+ * Sets up the iframe in place of the textarea to allow more advanced operations
 608+ */
 609+ 'setupIframe': function() {
 610+ context.$iframe = $( '<iframe></iframe>' )
 611+ .attr( {
 612+ 'frameBorder': 0,
 613+ 'border': 0,
 614+ 'tabindex': 1,
 615+ 'src': wgScriptPath + '/extensions/WikiEditor/modules/jquery.wikiEditor.html?' +
 616+ 'instance=' + context.instance + '&ts=' + ( new Date() ).getTime() + '&is=content',
 617+ 'id': 'wikiEditor-iframe-' + context.instance
 618+ } )
 619+ .css( {
 620+ 'backgroundColor': 'white',
 621+ 'width': '100%',
 622+ 'height': context.$textarea.height(),
 623+ 'display': 'none',
 624+ 'overflow-y': 'scroll',
 625+ 'overflow-x': 'hidden'
 626+ } )
 627+ .insertAfter( context.$textarea )
 628+ .load( function() {
 629+ // Internet Explorer will reload the iframe once we turn on design mode, so we need to only turn it
 630+ // on during the first run, and then bail
 631+ if ( !this.isSecondRun ) {
 632+ // Turn the document's design mode on
 633+ context.$iframe[0].contentWindow.document.designMode = 'on';
 634+ // Let the rest of this function happen next time around
 635+ if ( $.browser.msie ) {
 636+ this.isSecondRun = true;
 637+ return;
 638+ }
 639+ }
 640+ // Get a reference to the content area of the iframe
 641+ context.$content = $( context.$iframe[0].contentWindow.document.body );
 642+ // Add classes to the body to influence the styles based on what's enabled
 643+ for ( module in context.modules ) {
 644+ context.$content.addClass( 'wikiEditor-' + module );
 645+ }
 646+ // If we just do "context.$content.text( context.$textarea.val() )", Internet Explorer will strip
 647+ // out the whitespace charcters, specifically "\n" - so we must manually encode text and append it
 648+ // TODO: Refactor this into a textToHtml() function
 649+ var html = context.$textarea.val()
 650+ // We're gonna use &esc; as an escape sequence
 651+ .replace( /&esc;/g, '&esc;esc;' )
 652+ // Escape existing uses of <p>, </p>, &nbsp; and <span class="wikiEditor-tab"></span>
 653+ .replace( /\<p\>/g, '&esc;&lt;p&gt;' )
 654+ .replace( /\<\/p\>/g, '&esc;&lt;/p&gt;' )
 655+ .replace(
 656+ /\<span class="wikiEditor-tab"\>\<\/span\>/g,
 657+ '&esc;&lt;span&nbsp;class=&quot;wikiEditor-tab&quot;&gt;&lt;/span&gt;'
 658+ )
 659+ .replace( /&nbsp;/g, '&esc;&amp;nbsp;' );
 660+ // We must do some extra processing on IE to avoid dirty diffs, specifically IE will collapse
 661+ // leading spaces - browser sniffing is not ideal, but executing this code on a non-broken browser
 662+ // doesn't cause harm
 663+ if ( $.browser.msie ) {
 664+ html = html.replace( /\t/g, '<span class="wikiEditor-tab"></span>' );
 665+ if ( $.browser.versionNumber <= 7 ) {
 666+ // Replace all spaces matching &nbsp; - IE <= 7 needs this because of its overzealous
 667+ // whitespace collapsing
 668+ html = html.replace( / /g, "&nbsp;" );
 669+ } else {
 670+ // IE8 is happy if we just convert the first leading space to &nbsp;
 671+ html = html.replace( /(^|\n) /g, "$1&nbsp;" );
 672+ }
 673+ }
 674+ // Use a dummy div to escape all entities
 675+ // This'll also escape <br>, <span> and &nbsp; , so we unescape those after
 676+ // We also need to unescape the doubly-escaped things mentioned above
 677+ html = $( '<div />' ).text( '<p>' + html.replace( /\r?\n/g, '</p><p>' ) + '</p>' ).html()
 678+ .replace( /&amp;nbsp;/g, '&nbsp;' )
 679+ // Allow <p> tags to survive encoding
 680+ .replace( /&lt;p&gt;/g, '<p>' )
 681+ .replace( /&lt;\/p&gt;/g, '</p>' )
 682+ // And <span class="wikiEditor-tab"></span> too
 683+ .replace(
 684+ /&lt;span( |&nbsp;)class=("|&quot;)wikiEditor-tab("|&quot;)&gt;&lt;\/span&gt;/g,
 685+ '<span class="wikiEditor-tab"></span>'
 686+ )
 687+ // Empty <p> tags need <br> tags in them
 688+ .replace( /<p><\/p>/g, '<p><br></p>' )
 689+ // Unescape &esc; stuff
 690+ .replace( /&amp;esc;&amp;amp;nbsp;/g, '&amp;nbsp;' )
 691+ .replace( /&amp;esc;&amp;lt;p&amp;gt;/g, '&lt;p&gt;' )
 692+ .replace( /&amp;esc;&amp;lt;\/p&amp;gt;/g, '&lt;/p&gt;' )
 693+ .replace(
 694+ /&amp;esc;&amp;lt;span&amp;nbsp;class=&amp;quot;wikiEditor-tab&amp;quot;&amp;gt;&amp;lt;\/span&amp;gt;/g,
 695+ '&lt;span class="wikiEditor-tab"&gt;&lt;\/span&gt;'
 696+ )
 697+ .replace( /&amp;esc;esc;/g, '&amp;esc;' );
 698+ context.$content.html( html );
 699+
 700+ // Reflect direction of parent frame into child
 701+ if ( $( 'body' ).is( '.rtl' ) ) {
 702+ context.$content.addClass( 'rtl' ).attr( 'dir', 'rtl' );
 703+ }
 704+ // Activate the iframe, encoding the content of the textarea and copying it to the content of iframe
 705+ context.$textarea.attr( 'disabled', true );
 706+ context.$textarea.hide();
 707+ context.$iframe.show();
 708+ // Let modules know we're ready to start working with the content
 709+ context.fn.trigger( 'ready' );
 710+ // Only save HTML now: ready handlers may have modified it
 711+ context.oldHTML = context.oldDelayedHTML = context.$content.html();
 712+ //remove our temporary loading
 713+ /* Disaling our loading div for now
 714+ $( '.wikiEditor-ui-loading' ).fadeOut( 'fast', function() {
 715+ $( this ).remove();
 716+ } );
 717+ */
 718+ // Setup event handling on the iframe
 719+ $( context.$iframe[0].contentWindow.document )
 720+ .bind( 'keydown', function( event ) {
 721+ event.jQueryNode = context.fn.getElementAtCursor();
 722+ return context.fn.trigger( 'keydown', event );
 723+
 724+ } )
 725+ .bind( 'keyup', function( event ) {
 726+ event.jQueryNode = context.fn.getElementAtCursor();
 727+ return context.fn.trigger( 'keyup', event );
 728+ } )
 729+ .bind( 'keypress', function( event ) {
 730+ event.jQueryNode = context.fn.getElementAtCursor();
 731+ return context.fn.trigger( 'keypress', event );
 732+ } )
 733+ .bind( 'paste', function( event ) {
 734+ return context.fn.trigger( 'paste', event );
 735+ } )
 736+ .bind( 'cut', function( event ) {
 737+ return context.fn.trigger( 'cut', event );
 738+ } )
 739+ .bind( 'keyup paste mouseup cut encapsulateSelection', function( event ) {
 740+ return context.fn.trigger( 'change', event );
 741+ } )
 742+ .delayedBind( 250, 'keyup paste mouseup cut encapsulateSelection', function( event ) {
 743+ context.fn.trigger( 'delayedChange', event );
 744+ } );
 745+ } );
 746+ // Attach a submit handler to the form so that when the form is submitted the content of the iframe gets
 747+ // decoded and copied over to the textarea
 748+ context.$textarea.closest( 'form' ).submit( function() {
 749+ context.$textarea.attr( 'disabled', false );
 750+ context.$textarea.val( context.$textarea.textSelection( 'getContents' ) );
 751+ } );
 752+ /* FIXME: This was taken from EditWarning.js - maybe we could do a jquery plugin for this? */
 753+ // Attach our own handler for onbeforeunload which respects the current one
 754+ context.fallbackWindowOnBeforeUnload = window.onbeforeunload;
 755+ window.onbeforeunload = function() {
 756+ context.$textarea.val( context.$textarea.textSelection( 'getContents' ) );
 757+ if ( context.fallbackWindowOnBeforeUnload ) {
 758+ return context.fallbackWindowOnBeforeUnload();
 759+ }
 760+ };
 761+ },
 762+
 763+ /*
 764+ * Compatibility with the $.textSelection jQuery plug-in. When the iframe is in use, these functions provide
 765+ * equivilant functionality to the otherwise textarea-based functionality.
 766+ */
 767+
 768+ 'getElementAtCursor': function() {
 769+ if ( context.$iframe[0].contentWindow.getSelection ) {
 770+ // Firefox and Opera
 771+ var selection = context.$iframe[0].contentWindow.getSelection();
 772+ if ( selection.rangeCount == 0 ) {
 773+ // We don't know where the cursor is
 774+ return $( [] );
 775+ }
 776+ var sc = selection.getRangeAt( 0 ).startContainer;
 777+ if ( sc.nodeName == "#text" ) sc = sc.parentNode;
 778+ return $( sc );
 779+ } else if ( context.$iframe[0].contentWindow.document.selection ) { // should come last; Opera!
 780+ // IE
 781+ var selection = context.$iframe[0].contentWindow.document.selection.createRange();
 782+ return $( selection.parentElement() );
 783+ }
 784+ },
 785+
 786+ /**
 787+ * Gets the complete contents of the iframe (in plain text, not HTML)
 788+ */
 789+ 'getContents': function() {
 790+ // For <p></p>, .html() returns <p>&nbsp;</p> in IE
 791+ // This seems to convince IE while not affecting display
 792+ if ( !context.$content ) {
 793+ return '';
 794+ }
 795+ var html;
 796+ if ( $.browser.msie ) {
 797+ // Don't manipulate the iframe DOM itself, causes cursor jumping issues
 798+ var $c = $( context.$content.get( 0 ).cloneNode( true ) );
 799+ $c.find( 'p' ).each( function() {
 800+ if ( $(this).html() == '' ) {
 801+ $(this).replaceWith( '<p></p>' );
 802+ }
 803+ } );
 804+ html = $c.html();
 805+ } else {
 806+ html = context.$content.html();
 807+ }
 808+ return context.fn.htmlToText( html );
 809+ },
 810+ /**
 811+ * Gets the currently selected text in the content
 812+ * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
 813+ */
 814+ 'getSelection': function() {
 815+ var retval;
 816+ if ( context.$iframe[0].contentWindow.getSelection ) {
 817+ // Firefox and Opera
 818+ retval = context.$iframe[0].contentWindow.getSelection();
 819+ if ( $.browser.opera ) {
 820+ // Opera strips newlines in getSelection(), so we need something more sophisticated
 821+ if ( retval.rangeCount > 0 ) {
 822+ retval = context.fn.htmlToText( $( '<pre />' )
 823+ .append( retval.getRangeAt( 0 ).cloneContents() )
 824+ .html()
 825+ );
 826+ } else {
 827+ retval = '';
 828+ }
 829+ }
 830+ } else if ( context.$iframe[0].contentWindow.document.selection ) { // should come last; Opera!
 831+ // IE
 832+ retval = context.$iframe[0].contentWindow.document.selection.createRange();
 833+ }
 834+ if ( typeof retval.text != 'undefined' ) {
 835+ // In IE8, retval.text is stripped of newlines, so we need to process retval.htmlText
 836+ // to get a reliable answer. IE7 does get this right though
 837+ // Run this fix for all IE versions anyway, it doesn't hurt
 838+ retval = context.fn.htmlToText( retval.htmlText );
 839+ } else if ( typeof retval.toString != 'undefined' ) {
 840+ retval = retval.toString();
 841+ }
 842+ return retval;
 843+ },
 844+ /**
 845+ * Inserts text at the begining and end of a text selection, optionally inserting text at the caret when
 846+ * selection is empty.
 847+ * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
 848+ */
 849+ 'encapsulateSelection': function( options ) {
 850+ var selText = $(this).textSelection( 'getSelection' );
 851+ var selTextArr;
 852+ var collapseToEnd = false;
 853+ var selectAfter = false;
 854+ var setSelectionTo = null;
 855+ var pre = options.pre, post = options.post;
 856+ if ( !selText ) {
 857+ selText = options.peri;
 858+ selectAfter = true;
 859+ } else if ( options.peri == selText.replace( /\s+$/, '' ) ) {
 860+ // Probably a successive button press
 861+ // strip any extra white space from selText
 862+ selText = selText.replace( /\s+$/, '' );
 863+ // set the collapseToEnd flag to ensure our selection is collapsed to the end before any insertion is done
 864+ collapseToEnd = true;
 865+ // set selectAfter to true since we know we'll be populating with our default text
 866+ selectAfter = true;
 867+ } else if ( options.replace ) {
 868+ selText = options.peri;
 869+ } else if ( selText.charAt( selText.length - 1 ) == ' ' ) {
 870+ // Exclude ending space char
 871+ // FIXME: Why?
 872+ selText = selText.substring( 0, selText.length - 1 );
 873+ post += ' ';
 874+ }
 875+ if ( options.splitlines ) {
 876+ selTextArr = selText.split( /\n/ );
 877+ }
 878+
 879+ if ( context.$iframe[0].contentWindow.getSelection ) {
 880+ // Firefox and Opera
 881+ var range = context.$iframe[0].contentWindow.getSelection().getRangeAt( 0 );
 882+ // if our test above indicated that this was a sucessive button press, we need to collapse the
 883+ // selection to the end to avoid replacing text
 884+ if ( collapseToEnd ) {
 885+ // Make sure we're not collapsing ourselves into a BR tag
 886+ if ( range.endContainer.nodeName == 'BR' ) {
 887+ range.setEndBefore( range.endContainer );
 888+ }
 889+ range.collapse( false );
 890+ }
 891+ if ( options.ownline ) {
 892+ // We need to figure out if the cursor is at the start or end of a line
 893+ var atStart = false, atEnd = false;
 894+ var body = context.$content.get( 0 );
 895+ if ( range.startOffset == 0 ) {
 896+ // Start of a line
 897+ // FIXME: Not necessarily the case with syntax highlighting or
 898+ // template collapsing
 899+ atStart = true;
 900+ } else if ( range.startContainer == body ) {
 901+ // Look up the node just before the start of the selection
 902+ // If it's a <BR>, we're at the start of a line that starts with a
 903+ // block element; if not, we're at the end of a line
 904+ var n = body.firstChild;
 905+ for ( var i = 0; i < range.startOffset - 1 && n; i++ ) {
 906+ n = n.nextSibling;
 907+ }
 908+ if ( n && n.nodeName == 'BR' ) {
 909+ atStart = true;
 910+ } else {
 911+ atEnd = true;
 912+ }
 913+ }
 914+ if ( ( range.endOffset == 0 && range.endContainer.nodeValue == null ) ||
 915+ ( range.endContainer.nodeName == '#text' &&
 916+ range.endOffset == range.endContainer.nodeValue.length ) ||
 917+ ( range.endContainer.nodeName == 'P' && range.endContainer.nodeValue == null ) ) {
 918+ atEnd = true;
 919+ }
 920+ if ( !atStart ) {
 921+ pre = "\n" + options.pre;
 922+ }
 923+ if ( !atEnd ) {
 924+ post += "\n";
 925+ }
 926+ }
 927+ var insertText = "";
 928+ if ( options.splitlines ) {
 929+ for( var j = 0; j < selTextArr.length; j++ ) {
 930+ insertText = insertText + pre + selTextArr[j] + post;
 931+ if( j != selTextArr.length - 1 ) {
 932+ insertText += "\n";
 933+ }
 934+ }
 935+ } else {
 936+ insertText = pre + selText + post;
 937+ }
 938+ var insertLines = insertText.split( "\n" );
 939+ range.extractContents();
 940+ // Insert the contents one line at a time - insertNode() inserts at the beginning, so this has to happen
 941+ // in reverse order
 942+ // Track the first and last inserted node, and if we need to also track where the text we need to select
 943+ // afterwards starts and ends
 944+ var firstNode = null, lastNode = null;
 945+ var selSC = null, selEC = null, selSO = null, selEO = null, offset = 0;
 946+ for ( var i = insertLines.length - 1; i >= 0; i-- ) {
 947+ firstNode = context.$iframe[0].contentWindow.document.createTextNode( insertLines[i] );
 948+ range.insertNode( firstNode );
 949+ lastNode = lastNode || firstNode;
 950+ var newOffset = offset + insertLines[i].length;
 951+ if ( !selEC && post.length <= newOffset ) {
 952+ selEC = firstNode;
 953+ selEO = selEC.nodeValue.length - ( post.length - offset );
 954+ }
 955+ if ( selEC && !selSC && pre.length >= insertText.length - newOffset ) {
 956+ selSC = firstNode;
 957+ selSO = pre.length - ( insertText.length - newOffset );
 958+ }
 959+ offset = newOffset;
 960+ if ( i > 0 ) {
 961+ firstNode = context.$iframe[0].contentWindow.document.createElement( 'br' );
 962+ range.insertNode( firstNode );
 963+ newOffset = offset + 1;
 964+ if ( !selEC && post.length <= newOffset ) {
 965+ selEC = firstNode;
 966+ selEO = 1 - ( post.length - offset );
 967+ }
 968+ if ( selEC && !selSC && pre.length >= insertText.length - newOffset ) {
 969+ selSC = firstNode;
 970+ selSO = pre.length - ( insertText.length - newOffset );
 971+ }
 972+ offset = newOffset;
 973+ }
 974+ }
 975+ if ( firstNode ) {
 976+ context.fn.scrollToTop( $( firstNode.parentNode ) );
 977+ }
 978+ if ( selectAfter ) {
 979+ setSelectionTo = {
 980+ startContainer: selSC,
 981+ endContainer: selEC,
 982+ start: selSO,
 983+ end: selEO
 984+ };
 985+ } else if ( lastNode ) {
 986+ setSelectionTo = {
 987+ startContainer: lastNode,
 988+ endContainer: lastNode,
 989+ start: lastNode.nodeValue.length,
 990+ end: lastNode.nodeValue.length
 991+ };
 992+ }
 993+ } else if ( context.$iframe[0].contentWindow.document.selection ) {
 994+ // IE
 995+ context.$iframe[0].contentWindow.focus();
 996+ var range = context.$iframe[0].contentWindow.document.selection.createRange();
 997+ if ( options.ownline && range.moveStart ) {
 998+ // Check if we're at the start of a line
 999+ // If not, prepend a newline
 1000+ var range2 = context.$iframe[0].contentWindow.document.selection.createRange();
 1001+ range2.collapse();
 1002+ range2.moveStart( 'character', -1 );
 1003+ // FIXME: Which check is correct?
 1004+ if ( range2.text != "\r" && range2.text != "\n" && range2.text != "" ) {
 1005+ pre = "\n" + pre;
 1006+ }
 1007+
 1008+ // Check if we're at the end of a line
 1009+ // If not, append a newline
 1010+ var range3 = context.$iframe[0].contentWindow.document.selection.createRange();
 1011+ range3.collapse( false );
 1012+ range3.moveEnd( 'character', 1 );
 1013+ if ( range3.text != "\r" && range3.text != "\n" && range3.text != "" ) {
 1014+ post += "\n";
 1015+ }
 1016+ }
 1017+ // if our test above indicated that this was a sucessive button press, we need to collapse the
 1018+ // selection to the end to avoid replacing text
 1019+ if ( collapseToEnd ) {
 1020+ range.collapse( false );
 1021+ }
 1022+ // TODO: Clean this up. Duplicate code due to the pre-existing browser specific structure of this
 1023+ // function
 1024+ var insertText = "";
 1025+ if ( options.splitlines ) {
 1026+ for( var j = 0; j < selTextArr.length; j++ ) {
 1027+ insertText = insertText + pre + selTextArr[j] + post;
 1028+ if( j != selTextArr.length - 1 ) {
 1029+ insertText += "\n";
 1030+ }
 1031+ }
 1032+ } else {
 1033+ insertText = pre + selText + post;
 1034+ }
 1035+ // TODO: Maybe find a more elegant way of doing this like the Firefox code above?
 1036+ range.pasteHTML( insertText
 1037+ .replace( /\</g, '&lt;' )
 1038+ .replace( />/g, '&gt;' )
 1039+ .replace( /\r?\n/g, '<br />' )
 1040+ );
 1041+ if ( selectAfter ) {
 1042+ range.moveStart( 'character', -post.length - selText.length );
 1043+ range.moveEnd( 'character', -post.length );
 1044+ range.select();
 1045+ }
 1046+ }
 1047+
 1048+ if ( setSelectionTo ) {
 1049+ context.fn.setSelection( setSelectionTo );
 1050+ }
 1051+ // Trigger the encapsulateSelection event (this might need to get named something else/done differently)
 1052+ $( context.$iframe[0].contentWindow.document ).trigger(
 1053+ 'encapsulateSelection', [ pre, options.peri, post, options.ownline, options.replace ]
 1054+ );
 1055+ return context.$textarea;
 1056+ },
 1057+ /**
 1058+ * Gets the position (in resolution of bytes not nessecarily characters) in a textarea
 1059+ * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
 1060+ */
 1061+ 'getCaretPosition': function( options ) {
 1062+ var startPos = null, endPos = null;
 1063+ if ( context.$iframe[0].contentWindow.getSelection ) {
 1064+ var selection = context.$iframe[0].contentWindow.getSelection();
 1065+ if ( selection.rangeCount == 0 ) {
 1066+ // We don't know where the cursor is
 1067+ return [ 0, 0 ];
 1068+ }
 1069+ var sc = selection.getRangeAt( 0 ).startContainer, ec = selection.getRangeAt( 0 ).endContainer;
 1070+ var so = selection.getRangeAt( 0 ).startOffset, eo = selection.getRangeAt( 0 ).endOffset;
 1071+ if ( sc.nodeName == 'BODY' ) {
 1072+ // Grab the node just before the start of the selection
 1073+ var n = sc.firstChild;
 1074+ for ( var i = 0; i < so - 1 && n; i++ ) {
 1075+ n = n.nextSibling;
 1076+ }
 1077+ sc = n;
 1078+ so = 0;
 1079+ }
 1080+ if ( ec.nodeName == 'BODY' ) {
 1081+ var n = ec.firstChild;
 1082+ for ( var i = 0; i < eo - 1 && n; i++ ) {
 1083+ n = n.nextSibling;
 1084+ }
 1085+ ec = n;
 1086+ eo = 0;
 1087+ }
 1088+
 1089+ // Make sure sc and ec are leaf nodes
 1090+ while ( sc.firstChild ) {
 1091+ sc = sc.firstChild;
 1092+ }
 1093+ while ( ec.firstChild ) {
 1094+ ec = ec.firstChild;
 1095+ }
 1096+ // Make sure the offsets are regenerated if necessary
 1097+ context.fn.getOffset( 0 );
 1098+ var o;
 1099+ for ( o in context.offsets ) {
 1100+ if ( startPos === null && context.offsets[o].node == sc ) {
 1101+ // For some wicked reason o is a string, even though
 1102+ // we put it in as an integer. Use ~~ to coerce it too an int
 1103+ startPos = ~~o + so - context.offsets[o].offset;
 1104+ }
 1105+ if ( startPos !== null && context.offsets[o].node == ec ) {
 1106+ endPos = ~~o + eo - context.offsets[o].offset;
 1107+ break;
 1108+ }
 1109+ }
 1110+ } else if ( context.$iframe[0].contentWindow.document.selection ) {
 1111+ // IE
 1112+ // FIXME: This is mostly copypasted from the textSelection plugin
 1113+ var d = context.$iframe[0].contentWindow.document;
 1114+ var postFinished = false;
 1115+ var periFinished = false;
 1116+ var postFinished = false;
 1117+ var preText, rawPreText, periText;
 1118+ var rawPeriText, postText, rawPostText;
 1119+ // Depending on the document state, and if the cursor has ever been manually placed within the document
 1120+ // the following call such as setEndPoint can result in nasty errors. These cases are always cases
 1121+ // in which the start and end points can safely be assumed to be 0, so we will just try our best to do
 1122+ // the full process but fall back to 0.
 1123+ try {
 1124+ // Create range containing text in the selection
 1125+ var periRange = d.selection.createRange().duplicate();
 1126+ // Create range containing text before the selection
 1127+ var preRange = d.body.createTextRange();
 1128+ // Move the end where we need it
 1129+ preRange.setEndPoint( "EndToStart", periRange );
 1130+ // Create range containing text after the selection
 1131+ var postRange = d.body.createTextRange();
 1132+ // Move the start where we need it
 1133+ postRange.setEndPoint( "StartToEnd", periRange );
 1134+ // Load the text values we need to compare
 1135+ preText = rawPreText = preRange.text;
 1136+ periText = rawPeriText = periRange.text;
 1137+ postText = rawPostText = postRange.text;
 1138+ /*
 1139+ * Check each range for trimmed newlines by shrinking the range by 1
 1140+ * character and seeing if the text property has changed. If it has
 1141+ * not changed then we know that IE has trimmed a \r\n from the end.
 1142+ */
 1143+ do {
 1144+ if ( !postFinished ) {
 1145+ if ( preRange.compareEndPoints( "StartToEnd", preRange ) == 0 ) {
 1146+ postFinished = true;
 1147+ } else {
 1148+ preRange.moveEnd( "character", -1 )
 1149+ if ( preRange.text == preText ) {
 1150+ rawPreText += "\r\n";
 1151+ } else {
 1152+ postFinished = true;
 1153+ }
 1154+ }
 1155+ }
 1156+ if ( !periFinished ) {
 1157+ if ( periRange.compareEndPoints( "StartToEnd", periRange ) == 0 ) {
 1158+ periFinished = true;
 1159+ } else {
 1160+ periRange.moveEnd( "character", -1 )
 1161+ if ( periRange.text == periText ) {
 1162+ rawPeriText += "\r\n";
 1163+ } else {
 1164+ periFinished = true;
 1165+ }
 1166+ }
 1167+ }
 1168+ if ( !postFinished ) {
 1169+ if ( postRange.compareEndPoints("StartToEnd", postRange) == 0 ) {
 1170+ postFinished = true;
 1171+ } else {
 1172+ postRange.moveEnd( "character", -1 )
 1173+ if ( postRange.text == postText ) {
 1174+ rawPostText += "\r\n";
 1175+ } else {
 1176+ postFinished = true;
 1177+ }
 1178+ }
 1179+ }
 1180+ } while ( ( !postFinished || !periFinished || !postFinished ) );
 1181+ startPos = rawPreText.replace( /\r\n/g, "\n" ).length;
 1182+ endPos = startPos + rawPeriText.replace( /\r\n/g, "\n" ).length;
 1183+ } catch( e ) {
 1184+ startPos = endPos = 0;
 1185+ }
 1186+ }
 1187+ return [ startPos, endPos ];
 1188+ },
 1189+ /**
 1190+ * Sets the selection of the content
 1191+ * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
 1192+ *
 1193+ * @param start Character offset of selection start
 1194+ * @param end Character offset of selection end
 1195+ * @param startContainer Element in iframe to start selection in. If not set, start is a character offset
 1196+ * @param endContainer Element in iframe to end selection in. If not set, end is a character offset
 1197+ */
 1198+ 'setSelection': function( options ) {
 1199+ var sc = options.startContainer, ec = options.endContainer;
 1200+ sc = sc && sc.jquery ? sc[0] : sc;
 1201+ ec = ec && ec.jquery ? ec[0] : ec;
 1202+ if ( context.$iframe[0].contentWindow.getSelection ) {
 1203+ // Firefox and Opera
 1204+ var start = options.start, end = options.end;
 1205+ if ( !sc || !ec ) {
 1206+ var s = context.fn.getOffset( start );
 1207+ var e = context.fn.getOffset( end );
 1208+ sc = s ? s.node : null;
 1209+ ec = e ? e.node : null;
 1210+ start = s ? s.offset : null;
 1211+ end = e ? e.offset : null;
 1212+ // Don't try to set the selection past the end of a node, causes errors
 1213+ // Just put the selection at the end of the node in this case
 1214+ if ( sc != null && sc.nodeName == '#text' && start > sc.nodeValue.length ) {
 1215+ start = sc.nodeValue.length - 1;
 1216+ }
 1217+ if ( ec != null && ec.nodeName == '#text' && end > ec.nodeValue.length ) {
 1218+ end = ec.nodeValue.length - 1;
 1219+ }
 1220+ }
 1221+ if ( !sc || !ec ) {
 1222+ // The requested offset isn't in the offsets array
 1223+ // Give up
 1224+ return context.$textarea;
 1225+ }
 1226+
 1227+ var sel = context.$iframe[0].contentWindow.getSelection();
 1228+ while ( sc.firstChild && sc.nodeName != '#text' ) {
 1229+ sc = sc.firstChild;
 1230+ }
 1231+ while ( ec.firstChild && ec.nodeName != '#text' ) {
 1232+ ec = ec.firstChild;
 1233+ }
 1234+ var range = context.$iframe[0].contentWindow.document.createRange();
 1235+ range.setStart( sc, start );
 1236+ range.setEnd( ec, end );
 1237+ sel.removeAllRanges();
 1238+ sel.addRange( range );
 1239+ context.$iframe[0].contentWindow.focus();
 1240+ } else if ( context.$iframe[0].contentWindow.document.body.createTextRange ) {
 1241+ // IE
 1242+ var range = context.$iframe[0].contentWindow.document.body.createTextRange();
 1243+ if ( sc ) {
 1244+ range.moveToElementText( sc );
 1245+ }
 1246+ range.collapse();
 1247+ range.moveEnd( 'character', options.start );
 1248+
 1249+ var range2 = context.$iframe[0].contentWindow.document.body.createTextRange();
 1250+ if ( ec ) {
 1251+ range2.moveToElementText( ec );
 1252+ }
 1253+ range2.collapse();
 1254+ range2.moveEnd( 'character', options.end );
 1255+
 1256+ // IE does newline emulation for <p>s: <p>foo</p><p>bar</p> becomes foo\nbar just fine
 1257+ // but <p>foo</p><br><br><p>bar</p> becomes foo\n\n\n\nbar , one \n too many
 1258+ // Correct for this
 1259+ var matches, counted = 0;
 1260+ // while ( matches = range.htmlText.match( regex ) && matches.length <= counted ) doesn't work
 1261+ // because the assignment side effect hasn't happened yet when the second term is evaluated
 1262+ while ( matches = range.htmlText.match( /\<\/p\>(\<br[^\>]*\>)+\<p\>/gi ) ) {
 1263+ if ( matches.length <= counted )
 1264+ break;
 1265+ range.moveEnd( 'character', matches.length );
 1266+ counted += matches.length;
 1267+ }
 1268+ range2.moveEnd( 'character', counted );
 1269+ while ( matches = range2.htmlText.match( /\<\/p\>(\<br[^\>]*\>)+\<p\>/gi ) ) {
 1270+ if ( matches.length <= counted )
 1271+ break;
 1272+ range2.moveEnd( 'character', matches.length );
 1273+ counted += matches.length;
 1274+ }
 1275+
 1276+ range2.setEndPoint( 'StartToEnd', range );
 1277+ range2.select();
 1278+ }
 1279+ return context.$textarea;
 1280+ },
 1281+ /**
 1282+ * Scroll a textarea to the current cursor position. You can set the cursor position with setSelection()
 1283+ * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
 1284+ */
 1285+ 'scrollToCaretPosition': function( options ) {
 1286+ context.fn.scrollToTop( context.fn.getElementAtCursor(), true );
 1287+ },
 1288+ /**
 1289+ * Scroll an element to the top of the iframe
 1290+ * DO NOT CALL THIS DIRECTLY, use $.textSelection( 'functionname', options ) instead
 1291+ *
 1292+ * @param $element jQuery object containing an element in the iframe
 1293+ * @param force If true, scroll the element even if it's already visible
 1294+ */
 1295+ 'scrollToTop': function( $element, force ) {
 1296+ var html = context.$content.closest( 'html' ),
 1297+ body = context.$content.closest( 'body' ),
 1298+ parentHtml = $( 'html' ),
 1299+ parentBody = $( 'body' );
 1300+ var y = $element.offset().top;
 1301+ if ( !$.browser.msie && ! $element.is( 'body' ) ) {
 1302+ y = parentHtml.scrollTop() > 0 ? y + html.scrollTop() - parentHtml.scrollTop() : y;
 1303+ y = parentBody.scrollTop() > 0 ? y + body.scrollTop() - parentBody.scrollTop() : y;
 1304+ }
 1305+ var topBound = html.scrollTop() > body.scrollTop() ? html.scrollTop() : body.scrollTop(),
 1306+ bottomBound = topBound + context.$iframe.height();
 1307+ if ( force || y < topBound || y > bottomBound ) {
 1308+ html.scrollTop( y );
 1309+ body.scrollTop( y );
 1310+ }
 1311+ $element.trigger( 'scrollToTop' );
 1312+ }
 1313+} );
 1314+
 1315+/* Setup the IFrame */
 1316+context.fn.setupIframe();
 1317+
 1318+} } )( jQuery );
Property changes on: trunk/extensions/WikiEditor/modules/jquery.wikiEditor.iframe.js
___________________________________________________________________
Added: svn:eol-style
11319 + native

Follow-up revisions

RevisionCommit summaryAuthorDate
r74325* Fixed mistake made in r74271 where some functions were moved from core jque...tparscal20:54, 5 October 2010
r81694WikiEditor: Add some var statements to code introduced in r74271catrope07:04, 8 February 2011
r89757Fix for WikiEditor iframe extension on IE: use $.inArray() instead of Array.i...brion00:20, 9 June 2011

Comments

#Comment by Brion VIBBER (talk | contribs)   15:44, 5 October 2010

Looks like some functions got moved to the iframe-only class that are called from elsewhere; for instance saveSelection is called from jquery.wikiEditor.toolbar.js when opening a dialog, causing it to fail on Firefox 4.0b:

Error: context.fn.saveSelection is not a function
Source File: [http://lazarus.local/trunk/load.php?debug=false&lang=en&modules=ext.wikiEditor%7Cext.wikiEditor.dialogs%7Cext.wikiEditor.toolbar%7Cjquery.wikiEditor%7Cjquery.wikiEditor.dialogs%7Cjquery.wikiEditor.toolbar&skin=vector&version=20101005T153308Z http://lazarus.local/trunk/load.php?debug=false&lang=en&modules=ext.wikiEditor%7Cext.wikiEditor.dialogs%7Cext.wikiEditor.toolbar%7Cjquery.wikiEditor%7Cjquery.wikiEditor.dialogs%7Cjquery.wikiEditor.toolbar&skin=vector&version=20101005T153308Z]
Line: 3261

Status & tagging log