with another list tree as the first
+ // child, we'll need to append a filler ( /NBSP) or the list item
+ // wouldn't be editable. (#1420)
+ if ( nextBlock.is( 'li' )
+ && ( node = nextBlock.getFirst( CKEDITOR.dom.walker.invisible( true ) ) )
+ && node.is && node.is( 'ul', 'ol' ) )
+ ( CKEDITOR.env.ie ? doc.createText( '\xa0' ) : doc.createElement( 'br' ) ).insertBefore( node );
+
+ // Move the selection to the end block.
+ if ( nextBlock )
+ range.moveToElementEditStart( nextBlock );
+ }
+ else
+ {
+ var newBlock,
+ newBlockDir;
+
+ if ( previousBlock )
+ {
+ // Do not enter this block if it's a header tag, or we are in
+ // a Shift+Enter (#77). Create a new block element instead
+ // (later in the code).
+ if ( previousBlock.is( 'li' ) || !headerTagRegex.test( previousBlock.getName() ) )
+ {
+ // Otherwise, duplicate the previous block.
+ newBlock = previousBlock.clone();
+ // Value attribute of list item should not be duplicated (#7330).
+ newBlock.is( 'li' ) && newBlock.removeAttribute( 'value' );
+ }
+ }
+ else if ( nextBlock )
+ newBlock = nextBlock.clone();
+
+ if ( !newBlock )
+ {
+ // We have already created a new list item. (#6849)
+ if ( node && node.is( 'li' ) )
+ newBlock = node;
+ else
+ {
+ newBlock = doc.createElement( blockTag );
+ if ( previousBlock && ( newBlockDir = previousBlock.getDirection() ) )
+ newBlock.setAttribute( 'dir', newBlockDir );
+ }
+ }
+ // Force the enter block unless we're talking of a list item.
+ else if ( forceMode && !newBlock.is( 'li' ) )
+ newBlock.renameNode( blockTag );
+
+ // Recreate the inline elements tree, which was available
+ // before hitting enter, so the same styles will be available in
+ // the new block.
+ var elementPath = splitInfo.elementPath;
+ if ( elementPath )
+ {
+ for ( var i = 0, len = elementPath.elements.length ; i < len ; i++ )
+ {
+ var element = elementPath.elements[ i ];
+
+ if ( element.equals( elementPath.block ) || element.equals( elementPath.blockLimit ) )
+ break;
+
+ if ( CKEDITOR.dtd.$removeEmpty[ element.getName() ] )
+ {
+ element = element.clone();
+ newBlock.moveChildren( element );
+ newBlock.append( element );
+ }
+ }
+ }
+
+ if ( !CKEDITOR.env.ie )
+ newBlock.appendBogus();
+
+ if ( !newBlock.getParent() )
+ range.insertNode( newBlock );
+
+ // This is tricky, but to make the new block visible correctly
+ // we must select it.
+ // The previousBlock check has been included because it may be
+ // empty if we have fixed a block-less space (like ENTER into an
+ // empty table cell).
+ if ( CKEDITOR.env.ie && isStartOfBlock && ( !isEndOfBlock || !previousBlock.getChildCount() ) )
+ {
+ // Move the selection to the new block.
+ range.moveToElementEditStart( isEndOfBlock ? previousBlock : newBlock );
+ range.select();
+ }
+
+ // Move the selection to the new block.
+ range.moveToElementEditStart( isStartOfBlock && !isEndOfBlock ? nextBlock : newBlock );
+ }
+
+ if ( !CKEDITOR.env.ie )
+ {
+ if ( nextBlock )
+ {
+ // If we have split the block, adds a temporary span at the
+ // range position and scroll relatively to it.
+ var tmpNode = doc.createElement( 'span' );
+
+ // We need some content for Safari.
+ tmpNode.setHtml( ' ' );
+
+ range.insertNode( tmpNode );
+ tmpNode.scrollIntoView();
+ range.deleteContents();
+ }
+ else
+ {
+ // We may use the above scroll logic for the new block case
+ // too, but it gives some weird result with Opera.
+ newBlock.scrollIntoView();
+ }
+ }
+
+ range.select();
+ },
+
+ enterBr : function( editor, mode, range, forceMode )
+ {
+ // Get the range for the current selection.
+ range = range || getRange( editor );
+
+ // We may not have valid ranges to work on, like when inside a
+ // contenteditable=false element.
+ if ( !range )
+ return;
+
+ var doc = range.document;
+
+ // Determine the block element to be used.
+ var blockTag = ( mode == CKEDITOR.ENTER_DIV ? 'div' : 'p' );
+
+ var isEndOfBlock = range.checkEndOfBlock();
+
+ var elementPath = new CKEDITOR.dom.elementPath( editor.getSelection().getStartElement() );
+
+ var startBlock = elementPath.block,
+ startBlockTag = startBlock && elementPath.block.getName();
+
+ var isPre = false;
+
+ if ( !forceMode && startBlockTag == 'li' )
+ {
+ enterBlock( editor, mode, range, forceMode );
+ return;
+ }
+
+ // If we are at the end of a header block.
+ if ( !forceMode && isEndOfBlock && headerTagRegex.test( startBlockTag ) )
+ {
+ var newBlock,
+ newBlockDir;
+
+ if ( ( newBlockDir = startBlock.getDirection() ) )
+ {
+ newBlock = doc.createElement( 'div' );
+ newBlock.setAttribute( 'dir', newBlockDir );
+ newBlock.insertAfter( startBlock );
+ range.setStart( newBlock, 0 );
+ }
+ else
+ {
+ // Insert a after the current paragraph.
+ doc.createElement( 'br' ).insertAfter( startBlock );
+
+ // A text node is required by Gecko only to make the cursor blink.
+ if ( CKEDITOR.env.gecko )
+ doc.createText( '' ).insertAfter( startBlock );
+
+ // IE has different behaviors regarding position.
+ range.setStartAt( startBlock.getNext(), CKEDITOR.env.ie ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_START );
+ }
+ }
+ else
+ {
+ var lineBreak;
+
+ isPre = ( startBlockTag == 'pre' );
+
+ // Gecko prefers as line-break inside (#4711).
+ if ( isPre && !CKEDITOR.env.gecko )
+ lineBreak = doc.createText( CKEDITOR.env.ie ? '\r' : '\n' );
+ else
+ lineBreak = doc.createElement( 'br' );
+
+ range.deleteContents();
+ range.insertNode( lineBreak );
+
+ // IE has different behavior regarding position.
+ if ( CKEDITOR.env.ie )
+ range.setStartAt( lineBreak, CKEDITOR.POSITION_AFTER_END );
+ else
+ {
+ // A text node is required by Gecko only to make the cursor blink.
+ // We need some text inside of it, so the bogus is properly
+ // created.
+ doc.createText( '\ufeff' ).insertAfter( lineBreak );
+
+ // If we are at the end of a block, we must be sure the bogus node is available in that block.
+ if ( isEndOfBlock )
+ lineBreak.getParent().appendBogus();
+
+ // Now we can remove the text node contents, so the caret doesn't
+ // stop on it.
+ lineBreak.getNext().$.nodeValue = '';
+
+ range.setStartAt( lineBreak.getNext(), CKEDITOR.POSITION_AFTER_START );
+
+ // Scroll into view, for non IE.
+ var dummy = null;
+
+ // BR is not positioned in Opera and Webkit.
+ if ( !CKEDITOR.env.gecko )
+ {
+ dummy = doc.createElement( 'span' );
+ // We need have some contents for Webkit to position it
+ // under parent node. ( #3681)
+ dummy.setHtml(' ');
+ }
+ else
+ dummy = doc.createElement( 'br' );
+
+ dummy.insertBefore( lineBreak.getNext() );
+ dummy.scrollIntoView();
+ dummy.remove();
+ }
+ }
+
+ // This collapse guarantees the cursor will be blinking.
+ range.collapse( true );
+
+ range.select( isPre );
+ }
+ };
+
+ var plugin = CKEDITOR.plugins.enterkey,
+ enterBr = plugin.enterBr,
+ enterBlock = plugin.enterBlock,
+ headerTagRegex = /^h[1-6]$/;
+
+ function shiftEnter( editor )
+ {
+ // Only effective within document.
+ if ( editor.mode != 'wysiwyg' )
+ return false;
+
+ // On SHIFT+ENTER:
+ // 1. We want to enforce the mode to be respected, instead
+ // of cloning the current block. (#77)
+ // 2. Always perform a block break when inside (#5402).
+ if ( editor.getSelection().getStartElement().hasAscendant( 'pre', true ) )
+ {
+ setTimeout( function() { enterBlock( editor, editor.config.enterMode, null, true ); }, 0 );
+ return true;
+ }
+ else
+ return enter( editor, editor.config.shiftEnterMode, 1 );
+ }
+
+ function enter( editor, mode, forceMode )
+ {
+ forceMode = editor.config.forceEnterMode || forceMode;
+
+ // Only effective within document.
+ if ( editor.mode != 'wysiwyg' )
+ return false;
+
+ if ( !mode )
+ mode = editor.config.enterMode;
+
+ // Use setTimout so the keys get cancelled immediatelly.
+ setTimeout( function()
+ {
+ editor.fire( 'saveSnapshot' ); // Save undo step.
+ if ( mode == CKEDITOR.ENTER_BR || editor.getSelection().getStartElement().hasAscendant( 'pre', 1 ) )
+ enterBr( editor, mode, null, forceMode );
+ else
+ enterBlock( editor, mode, null, forceMode );
+
+ }, 0 );
+
+ return true;
+ }
+
+ function getRange( editor )
+ {
+ // Get the selection ranges.
+ var ranges = editor.getSelection().getRanges( true );
+
+ // Delete the contents of all ranges except the first one.
+ for ( var i = ranges.length - 1 ; i > 0 ; i-- )
+ {
+ ranges[ i ].deleteContents();
+ }
+
+ // Return the first range.
+ return ranges[ 0 ];
+ }
+})();
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/entities/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/entities/plugin.js
new file mode 100644
index 00000000..3c267650
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/entities/plugin.js
@@ -0,0 +1,226 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+(function()
+{
+ // Base HTML entities.
+ var htmlbase = 'nbsp,gt,lt';
+
+ var entities =
+ // Latin-1 Entities
+ 'quot,iexcl,cent,pound,curren,yen,brvbar,sect,uml,copy,ordf,laquo,' +
+ 'not,shy,reg,macr,deg,plusmn,sup2,sup3,acute,micro,para,middot,' +
+ 'cedil,sup1,ordm,raquo,frac14,frac12,frac34,iquest,times,divide,' +
+
+ // Symbols
+ 'fnof,bull,hellip,prime,Prime,oline,frasl,weierp,image,real,trade,' +
+ 'alefsym,larr,uarr,rarr,darr,harr,crarr,lArr,uArr,rArr,dArr,hArr,' +
+ 'forall,part,exist,empty,nabla,isin,notin,ni,prod,sum,minus,lowast,' +
+ 'radic,prop,infin,ang,and,or,cap,cup,int,there4,sim,cong,asymp,ne,' +
+ 'equiv,le,ge,sub,sup,nsub,sube,supe,oplus,otimes,perp,sdot,lceil,' +
+ 'rceil,lfloor,rfloor,lang,rang,loz,spades,clubs,hearts,diams,' +
+
+ // Other Special Characters
+ 'circ,tilde,ensp,emsp,thinsp,zwnj,zwj,lrm,rlm,ndash,mdash,lsquo,' +
+ 'rsquo,sbquo,ldquo,rdquo,bdquo,dagger,Dagger,permil,lsaquo,rsaquo,' +
+ 'euro';
+
+ // Latin Letters Entities
+ var latin =
+ 'Agrave,Aacute,Acirc,Atilde,Auml,Aring,AElig,Ccedil,Egrave,Eacute,' +
+ 'Ecirc,Euml,Igrave,Iacute,Icirc,Iuml,ETH,Ntilde,Ograve,Oacute,Ocirc,' +
+ 'Otilde,Ouml,Oslash,Ugrave,Uacute,Ucirc,Uuml,Yacute,THORN,szlig,' +
+ 'agrave,aacute,acirc,atilde,auml,aring,aelig,ccedil,egrave,eacute,' +
+ 'ecirc,euml,igrave,iacute,icirc,iuml,eth,ntilde,ograve,oacute,ocirc,' +
+ 'otilde,ouml,oslash,ugrave,uacute,ucirc,uuml,yacute,thorn,yuml,' +
+ 'OElig,oelig,Scaron,scaron,Yuml';
+
+ // Greek Letters Entities.
+ var greek =
+ 'Alpha,Beta,Gamma,Delta,Epsilon,Zeta,Eta,Theta,Iota,Kappa,Lambda,Mu,' +
+ 'Nu,Xi,Omicron,Pi,Rho,Sigma,Tau,Upsilon,Phi,Chi,Psi,Omega,alpha,' +
+ 'beta,gamma,delta,epsilon,zeta,eta,theta,iota,kappa,lambda,mu,nu,xi,' +
+ 'omicron,pi,rho,sigmaf,sigma,tau,upsilon,phi,chi,psi,omega,thetasym,' +
+ 'upsih,piv';
+
+ /**
+ * Create a mapping table between one character and it's entity form from a list of entity names.
+ * @param reverse {Boolean} Whether create a reverse map from the entity string form to actual character.
+ */
+ function buildTable( entities, reverse )
+ {
+ var table = {},
+ regex = [];
+
+ // Entities that the browsers DOM don't transform to the final char
+ // automatically.
+ var specialTable =
+ {
+ nbsp : '\u00A0', // IE | FF
+ shy : '\u00AD', // IE
+ gt : '\u003E', // IE | FF | -- | Opera
+ lt : '\u003C' // IE | FF | Safari | Opera
+ };
+
+ entities = entities.replace( /\b(nbsp|shy|gt|lt|amp)(?:,|$)/g, function( match, entity )
+ {
+ var org = reverse ? '&' + entity + ';' : specialTable[ entity ],
+ result = reverse ? specialTable[ entity ] : '&' + entity + ';';
+
+ table[ org ] = result;
+ regex.push( org );
+ return '';
+ });
+
+ if ( !reverse && entities )
+ {
+ // Transforms the entities string into an array.
+ entities = entities.split( ',' );
+
+ // Put all entities inside a DOM element, transforming them to their
+ // final chars.
+ var div = document.createElement( 'div' ),
+ chars;
+ div.innerHTML = '&' + entities.join( ';&' ) + ';';
+ chars = div.innerHTML;
+ div = null;
+
+ // Add all chars to the table.
+ for ( var i = 0 ; i < chars.length ; i++ )
+ {
+ var charAt = chars.charAt( i );
+ table[ charAt ] = '&' + entities[ i ] + ';';
+ regex.push( charAt );
+ }
+ }
+
+ table.regex = regex.join( reverse ? '|' : '' );
+
+ return table;
+ }
+
+ CKEDITOR.plugins.add( 'entities',
+ {
+ afterInit : function( editor )
+ {
+ var config = editor.config;
+
+ var dataProcessor = editor.dataProcessor,
+ htmlFilter = dataProcessor && dataProcessor.htmlFilter;
+
+ if ( htmlFilter )
+ {
+ // Mandatory HTML base entities.
+ var selectedEntities = htmlbase;
+
+ if ( config.entities )
+ {
+ selectedEntities += ',' + entities;
+ if ( config.entities_latin )
+ selectedEntities += ',' + latin;
+
+ if ( config.entities_greek )
+ selectedEntities += ',' + greek;
+
+ if ( config.entities_additional )
+ selectedEntities += ',' + config.entities_additional;
+ }
+
+ var entitiesTable = buildTable( selectedEntities );
+
+ // Create the Regex used to find entities in the text.
+ var entitiesRegex = '[' + entitiesTable.regex + ']';
+ delete entitiesTable.regex;
+
+ if ( config.entities && config.entities_processNumerical )
+ entitiesRegex = '[^ -~]|' + entitiesRegex ;
+
+ entitiesRegex = new RegExp( entitiesRegex, 'g' );
+
+ function getEntity( character )
+ {
+ return config.entities_processNumerical == 'force' || !entitiesTable[ character ] ?
+ '' + character.charCodeAt(0) + ';'
+ : entitiesTable[ character ];
+ }
+
+ // Decode entities that the browsers has transformed
+ // at first place.
+ var baseEntitiesTable = buildTable( [ htmlbase, 'shy' ].join( ',' ) , true ),
+ baseEntitiesRegex = new RegExp( baseEntitiesTable.regex, 'g' );
+
+ function getChar( character )
+ {
+ return baseEntitiesTable[ character ];
+ }
+
+ htmlFilter.addRules(
+ {
+ text : function( text )
+ {
+ return text.replace( baseEntitiesRegex, getChar )
+ .replace( entitiesRegex, getEntity );
+ }
+ });
+ }
+ }
+ });
+})();
+
+/**
+ * Whether to use HTML entities in the output.
+ * @type Boolean
+ * @default true
+ * @example
+ * config.entities = false;
+ */
+CKEDITOR.config.entities = true;
+
+/**
+ * Whether to convert some Latin characters (Latin alphabet No. 1, ISO 8859-1)
+ * to HTML entities. The list of entities can be found at the
+ * W3C HTML 4.01 Specification, section 24.2.1 .
+ * @type Boolean
+ * @default true
+ * @example
+ * config.entities_latin = false;
+ */
+CKEDITOR.config.entities_latin = true;
+
+/**
+ * Whether to convert some symbols, mathematical symbols, and Greek letters to
+ * HTML entities. This may be more relevant for users typing text written in Greek.
+ * The list of entities can be found at the
+ * W3C HTML 4.01 Specification, section 24.3.1 .
+ * @type Boolean
+ * @default true
+ * @example
+ * config.entities_greek = false;
+ */
+CKEDITOR.config.entities_greek = true;
+
+/**
+ * Whether to convert all remaining characters, not comprised in the ASCII
+ * character table, to their relative decimal numeric representation of HTML entity.
+ * When specified as the value 'force', it will simply convert all entities into the above form.
+ * For example, the phrase "This is Chinese: 汉语." is outputted
+ * as "This is Chinese: 汉语."
+ * @type Boolean
+ * @type Boolean|String
+ * @default false
+ * @example
+ * config.entities_processNumerical = true;
+ * config.entities_processNumerical = 'force'; //Convert from " " into " ";
+ */
+
+/**
+ * An additional list of entities to be used. It's a string containing each
+ * entry separated by a comma. Entities names or number must be used, exclusing
+ * the "&" preffix and the ";" termination.
+ * @default '#39' // The single quote (') character.
+ * @type String
+ * @example
+ */
+CKEDITOR.config.entities_additional = '#39';
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/fakeobjects/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/fakeobjects/plugin.js
new file mode 100644
index 00000000..234e3a8c
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/fakeobjects/plugin.js
@@ -0,0 +1,126 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+(function()
+{
+ var htmlFilterRules =
+ {
+ elements :
+ {
+ $ : function( element )
+ {
+ var attributes = element.attributes,
+ realHtml = attributes && attributes[ 'data-cke-realelement' ],
+ realFragment = realHtml && new CKEDITOR.htmlParser.fragment.fromHtml( decodeURIComponent( realHtml ) ),
+ realElement = realFragment && realFragment.children[ 0 ];
+
+ // If we have width/height in the element, we must move it into
+ // the real element.
+ if ( realElement && element.attributes[ 'data-cke-resizable' ] )
+ {
+ var style = element.attributes.style;
+
+ if ( style )
+ {
+ // Get the width from the style.
+ var match = /(?:^|\s)width\s*:\s*(\d+)/i.exec( style ),
+ width = match && match[1];
+
+ // Get the height from the style.
+ match = /(?:^|\s)height\s*:\s*(\d+)/i.exec( style );
+ var height = match && match[1];
+
+ if ( width )
+ realElement.attributes.width = width;
+
+ if ( height )
+ realElement.attributes.height = height;
+ }
+ }
+
+ return realElement;
+ }
+ }
+ };
+
+ CKEDITOR.plugins.add( 'fakeobjects',
+ {
+ requires : [ 'htmlwriter' ],
+
+ afterInit : function( editor )
+ {
+ var dataProcessor = editor.dataProcessor,
+ htmlFilter = dataProcessor && dataProcessor.htmlFilter;
+
+ if ( htmlFilter )
+ htmlFilter.addRules( htmlFilterRules );
+ }
+ });
+})();
+
+CKEDITOR.editor.prototype.createFakeElement = function( realElement, className, realElementType, isResizable )
+{
+ var lang = this.lang.fakeobjects,
+ label = lang[ realElementType ] || lang.unknown;
+
+ var attributes =
+ {
+ 'class' : className,
+ src : CKEDITOR.getUrl( 'images/spacer.gif' ),
+ 'data-cke-realelement' : encodeURIComponent( realElement.getOuterHtml() ),
+ 'data-cke-real-node-type' : realElement.type,
+ alt : label,
+ title : label,
+ align : realElement.getAttribute( 'align' ) || ''
+ };
+
+ if ( realElementType )
+ attributes[ 'data-cke-real-element-type' ] = realElementType;
+
+ if ( isResizable )
+ attributes[ 'data-cke-resizable' ] = isResizable;
+
+ return this.document.createElement( 'img', { attributes : attributes } );
+};
+
+CKEDITOR.editor.prototype.createFakeParserElement = function( realElement, className, realElementType, isResizable )
+{
+ var lang = this.lang.fakeobjects,
+ label = lang[ realElementType ] || lang.unknown,
+ html;
+
+ var writer = new CKEDITOR.htmlParser.basicWriter();
+ realElement.writeHtml( writer );
+ html = writer.getHtml();
+
+ var attributes =
+ {
+ 'class' : className,
+ src : CKEDITOR.getUrl( 'images/spacer.gif' ),
+ 'data-cke-realelement' : encodeURIComponent( html ),
+ 'data-cke-real-node-type' : realElement.type,
+ alt : label,
+ title : label,
+ align : realElement.attributes.align || ''
+ };
+
+ if ( realElementType )
+ attributes[ 'data-cke-real-element-type' ] = realElementType;
+
+ if ( isResizable )
+ attributes[ 'data-cke-resizable' ] = isResizable;
+
+ return new CKEDITOR.htmlParser.element( 'img', attributes );
+};
+
+CKEDITOR.editor.prototype.restoreRealElement = function( fakeElement )
+{
+ if ( fakeElement.data( 'cke-real-node-type' ) != CKEDITOR.NODE_ELEMENT )
+ return null;
+
+ return CKEDITOR.dom.element.createFromHtml(
+ decodeURIComponent( fakeElement.data( 'cke-realelement' ) ),
+ this.document );
+};
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/filebrowser/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/filebrowser/plugin.js
new file mode 100644
index 00000000..799d68f2
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/filebrowser/plugin.js
@@ -0,0 +1,524 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+/**
+ * @fileOverview The "filebrowser" plugin, it adds support for file uploads and
+ * browsing.
+ *
+ * When file is selected inside of the file browser or uploaded, its url is
+ * inserted automatically to a field, which is described in the 'filebrowser'
+ * attribute. To specify field that should be updated, pass the tab id and
+ * element id, separated with a colon.
+ *
+ * Example 1: (Browse)
+ *
+ *
+ * {
+ * type : 'button',
+ * id : 'browse',
+ * filebrowser : 'tabId:elementId',
+ * label : editor.lang.common.browseServer
+ * }
+ *
+ *
+ * If you set the 'filebrowser' attribute on any element other than
+ * 'fileButton', the 'Browse' action will be triggered.
+ *
+ * Example 2: (Quick Upload)
+ *
+ *
+ * {
+ * type : 'fileButton',
+ * id : 'uploadButton',
+ * filebrowser : 'tabId:elementId',
+ * label : editor.lang.common.uploadSubmit,
+ * 'for' : [ 'upload', 'upload' ]
+ * }
+ *
+ *
+ * If you set the 'filebrowser' attribute on a fileButton element, the
+ * 'QuickUpload' action will be executed.
+ *
+ * Filebrowser plugin also supports more advanced configuration (through
+ * javascript object).
+ *
+ * The following settings are supported:
+ *
+ *
+ * [action] - Browse or QuickUpload
+ * [target] - field to update, tabId:elementId
+ * [params] - additional arguments to be passed to the server connector (optional)
+ * [onSelect] - function to execute when file is selected/uploaded (optional)
+ * [url] - the URL to be called (optional)
+ *
+ *
+ * Example 3: (Quick Upload)
+ *
+ *
+ * {
+ * type : 'fileButton',
+ * label : editor.lang.common.uploadSubmit,
+ * id : 'buttonId',
+ * filebrowser :
+ * {
+ * action : 'QuickUpload', //required
+ * target : 'tab1:elementId', //required
+ * params : //optional
+ * {
+ * type : 'Files',
+ * currentFolder : '/folder/'
+ * },
+ * onSelect : function( fileUrl, errorMessage ) //optional
+ * {
+ * // Do not call the built-in selectFuntion
+ * // return false;
+ * }
+ * },
+ * 'for' : [ 'tab1', 'myFile' ]
+ * }
+ *
+ *
+ * Suppose we have a file element with id 'myFile', text field with id
+ * 'elementId' and a fileButton. If filebowser.url is not specified explicitly,
+ * form action will be set to 'filebrowser[DialogName]UploadUrl' or, if not
+ * specified, to 'filebrowserUploadUrl'. Additional parameters from 'params'
+ * object will be added to the query string. It is possible to create your own
+ * uploadHandler and cancel the built-in updateTargetElement command.
+ *
+ * Example 4: (Browse)
+ *
+ *
+ * {
+ * type : 'button',
+ * id : 'buttonId',
+ * label : editor.lang.common.browseServer,
+ * filebrowser :
+ * {
+ * action : 'Browse',
+ * url : '/ckfinder/ckfinder.html&type=Images',
+ * target : 'tab1:elementId'
+ * }
+ * }
+ *
+ *
+ * In this example, after pressing a button, file browser will be opened in a
+ * popup. If we don't specify filebrowser.url attribute,
+ * 'filebrowser[DialogName]BrowseUrl' or 'filebrowserBrowseUrl' will be used.
+ * After selecting a file in a file browser, an element with id 'elementId' will
+ * be updated. Just like in the third example, a custom 'onSelect' function may be
+ * defined.
+ */
+( function()
+{
+ /*
+ * Adds (additional) arguments to given url.
+ *
+ * @param {String}
+ * url The url.
+ * @param {Object}
+ * params Additional parameters.
+ */
+ function addQueryString( url, params )
+ {
+ var queryString = [];
+
+ if ( !params )
+ return url;
+ else
+ {
+ for ( var i in params )
+ queryString.push( i + "=" + encodeURIComponent( params[ i ] ) );
+ }
+
+ return url + ( ( url.indexOf( "?" ) != -1 ) ? "&" : "?" ) + queryString.join( "&" );
+ }
+
+ /*
+ * Make a string's first character uppercase.
+ *
+ * @param {String}
+ * str String.
+ */
+ function ucFirst( str )
+ {
+ str += '';
+ var f = str.charAt( 0 ).toUpperCase();
+ return f + str.substr( 1 );
+ }
+
+ /*
+ * The onlick function assigned to the 'Browse Server' button. Opens the
+ * file browser and updates target field when file is selected.
+ *
+ * @param {CKEDITOR.event}
+ * evt The event object.
+ */
+ function browseServer( evt )
+ {
+ var dialog = this.getDialog();
+ var editor = dialog.getParentEditor();
+
+ editor._.filebrowserSe = this;
+
+ var width = editor.config[ 'filebrowser' + ucFirst( dialog.getName() ) + 'WindowWidth' ]
+ || editor.config.filebrowserWindowWidth || '80%';
+ var height = editor.config[ 'filebrowser' + ucFirst( dialog.getName() ) + 'WindowHeight' ]
+ || editor.config.filebrowserWindowHeight || '70%';
+
+ var params = this.filebrowser.params || {};
+ params.CKEditor = editor.name;
+ params.CKEditorFuncNum = editor._.filebrowserFn;
+ if ( !params.langCode )
+ params.langCode = editor.langCode;
+
+ var url = addQueryString( this.filebrowser.url, params );
+ editor.popup( url, width, height, editor.config.fileBrowserWindowFeatures );
+ }
+
+ /*
+ * The onlick function assigned to the 'Upload' button. Makes the final
+ * decision whether form is really submitted and updates target field when
+ * file is uploaded.
+ *
+ * @param {CKEDITOR.event}
+ * evt The event object.
+ */
+ function uploadFile( evt )
+ {
+ var dialog = this.getDialog();
+ var editor = dialog.getParentEditor();
+
+ editor._.filebrowserSe = this;
+
+ // If user didn't select the file, stop the upload.
+ if ( !dialog.getContentElement( this[ 'for' ][ 0 ], this[ 'for' ][ 1 ] ).getInputElement().$.value )
+ return false;
+
+ if ( !dialog.getContentElement( this[ 'for' ][ 0 ], this[ 'for' ][ 1 ] ).getAction() )
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Setups the file element.
+ *
+ * @param {CKEDITOR.ui.dialog.file}
+ * fileInput The file element used during file upload.
+ * @param {Object}
+ * filebrowser Object containing filebrowser settings assigned to
+ * the fileButton associated with this file element.
+ */
+ function setupFileElement( editor, fileInput, filebrowser )
+ {
+ var params = filebrowser.params || {};
+ params.CKEditor = editor.name;
+ params.CKEditorFuncNum = editor._.filebrowserFn;
+ if ( !params.langCode )
+ params.langCode = editor.langCode;
+
+ fileInput.action = addQueryString( filebrowser.url, params );
+ fileInput.filebrowser = filebrowser;
+ }
+
+ /*
+ * Traverse through the content definition and attach filebrowser to
+ * elements with 'filebrowser' attribute.
+ *
+ * @param String
+ * dialogName Dialog name.
+ * @param {CKEDITOR.dialog.definitionObject}
+ * definition Dialog definition.
+ * @param {Array}
+ * elements Array of {@link CKEDITOR.dialog.definition.content}
+ * objects.
+ */
+ function attachFileBrowser( editor, dialogName, definition, elements )
+ {
+ var element, fileInput;
+
+ for ( var i in elements )
+ {
+ element = elements[ i ];
+
+ if ( element.type == 'hbox' || element.type == 'vbox' )
+ attachFileBrowser( editor, dialogName, definition, element.children );
+
+ if ( !element.filebrowser )
+ continue;
+
+ if ( typeof element.filebrowser == 'string' )
+ {
+ var fb =
+ {
+ action : ( element.type == 'fileButton' ) ? 'QuickUpload' : 'Browse',
+ target : element.filebrowser
+ };
+ element.filebrowser = fb;
+ }
+
+ if ( element.filebrowser.action == 'Browse' )
+ {
+ var url = element.filebrowser.url;
+ if ( url === undefined )
+ {
+ url = editor.config[ 'filebrowser' + ucFirst( dialogName ) + 'BrowseUrl' ];
+ if ( url === undefined )
+ url = editor.config.filebrowserBrowseUrl;
+ }
+
+ if ( url )
+ {
+ element.onClick = browseServer;
+ element.filebrowser.url = url;
+ element.hidden = false;
+ }
+ }
+ else if ( element.filebrowser.action == 'QuickUpload' && element[ 'for' ] )
+ {
+ url = element.filebrowser.url;
+ if ( url === undefined )
+ {
+ url = editor.config[ 'filebrowser' + ucFirst( dialogName ) + 'UploadUrl' ];
+ if ( url === undefined )
+ url = editor.config.filebrowserUploadUrl;
+ }
+
+ if ( url )
+ {
+ var onClick = element.onClick;
+ element.onClick = function( evt )
+ {
+ // "element" here means the definition object, so we need to find the correct
+ // button to scope the event call
+ var sender = evt.sender;
+ if ( onClick && onClick.call( sender, evt ) === false )
+ return false;
+
+ return uploadFile.call( sender, evt );
+ };
+
+ element.filebrowser.url = url;
+ element.hidden = false;
+ setupFileElement( editor, definition.getContents( element[ 'for' ][ 0 ] ).get( element[ 'for' ][ 1 ] ), element.filebrowser );
+ }
+ }
+ }
+ }
+
+ /*
+ * Updates the target element with the url of uploaded/selected file.
+ *
+ * @param {String}
+ * url The url of a file.
+ */
+ function updateTargetElement( url, sourceElement )
+ {
+ var dialog = sourceElement.getDialog();
+ var targetElement = sourceElement.filebrowser.target || null;
+ url = url.replace( /#/g, '%23' );
+
+ // If there is a reference to targetElement, update it.
+ if ( targetElement )
+ {
+ var target = targetElement.split( ':' );
+ var element = dialog.getContentElement( target[ 0 ], target[ 1 ] );
+ if ( element )
+ {
+ element.setValue( url );
+ dialog.selectPage( target[ 0 ] );
+ }
+ }
+ }
+
+ /*
+ * Returns true if filebrowser is configured in one of the elements.
+ *
+ * @param {CKEDITOR.dialog.definitionObject}
+ * definition Dialog definition.
+ * @param String
+ * tabId The tab id where element(s) can be found.
+ * @param String
+ * elementId The element id (or ids, separated with a semicolon) to check.
+ */
+ function isConfigured( definition, tabId, elementId )
+ {
+ if ( elementId.indexOf( ";" ) !== -1 )
+ {
+ var ids = elementId.split( ";" );
+ for ( var i = 0 ; i < ids.length ; i++ )
+ {
+ if ( isConfigured( definition, tabId, ids[i] ) )
+ return true;
+ }
+ return false;
+ }
+
+ var elementFileBrowser = definition.getContents( tabId ).get( elementId ).filebrowser;
+ return ( elementFileBrowser && elementFileBrowser.url );
+ }
+
+ function setUrl( fileUrl, data )
+ {
+ var dialog = this._.filebrowserSe.getDialog(),
+ targetInput = this._.filebrowserSe[ 'for' ],
+ onSelect = this._.filebrowserSe.filebrowser.onSelect;
+
+ if ( targetInput )
+ dialog.getContentElement( targetInput[ 0 ], targetInput[ 1 ] ).reset();
+
+ if ( typeof data == 'function' && data.call( this._.filebrowserSe ) === false )
+ return;
+
+ if ( onSelect && onSelect.call( this._.filebrowserSe, fileUrl, data ) === false )
+ return;
+
+ // The "data" argument may be used to pass the error message to the editor.
+ if ( typeof data == 'string' && data )
+ alert( data );
+
+ if ( fileUrl )
+ updateTargetElement( fileUrl, this._.filebrowserSe );
+ }
+
+ CKEDITOR.plugins.add( 'filebrowser',
+ {
+ init : function( editor, pluginPath )
+ {
+ editor._.filebrowserFn = CKEDITOR.tools.addFunction( setUrl, editor );
+ editor.on( 'destroy', function () { CKEDITOR.tools.removeFunction( this._.filebrowserFn ); } );
+ }
+ } );
+
+ CKEDITOR.on( 'dialogDefinition', function( evt )
+ {
+ var definition = evt.data.definition,
+ element;
+ // Associate filebrowser to elements with 'filebrowser' attribute.
+ for ( var i in definition.contents )
+ {
+ if ( ( element = definition.contents[ i ] ) )
+ {
+ attachFileBrowser( evt.editor, evt.data.name, definition, element.elements );
+ if ( element.hidden && element.filebrowser )
+ {
+ element.hidden = !isConfigured( definition, element[ 'id' ], element.filebrowser );
+ }
+ }
+ }
+ } );
+
+} )();
+
+/**
+ * The location of an external file browser, that should be launched when "Browse Server" button is pressed.
+ * If configured, the "Browse Server" button will appear in Link, Image and Flash dialogs.
+ * @see The File Browser/Uploader documentation.
+ * @name CKEDITOR.config.filebrowserBrowseUrl
+ * @since 3.0
+ * @type String
+ * @default '' (empty string = disabled)
+ * @example
+ * config.filebrowserBrowseUrl = '/browser/browse.php';
+ */
+
+/**
+ * The location of a script that handles file uploads.
+ * If set, the "Upload" tab will appear in "Link", "Image" and "Flash" dialogs.
+ * @name CKEDITOR.config.filebrowserUploadUrl
+ * @see The File Browser/Uploader documentation.
+ * @since 3.0
+ * @type String
+ * @default '' (empty string = disabled)
+ * @example
+ * config.filebrowserUploadUrl = '/uploader/upload.php';
+ */
+
+/**
+ * The location of an external file browser, that should be launched when "Browse Server" button is pressed in the Image dialog.
+ * If not set, CKEditor will use {@link CKEDITOR.config.filebrowserBrowseUrl}.
+ * @name CKEDITOR.config.filebrowserImageBrowseUrl
+ * @since 3.0
+ * @type String
+ * @default '' (empty string = disabled)
+ * @example
+ * config.filebrowserImageBrowseUrl = '/browser/browse.php?type=Images';
+ */
+
+/**
+ * The location of an external file browser, that should be launched when "Browse Server" button is pressed in the Flash dialog.
+ * If not set, CKEditor will use {@link CKEDITOR.config.filebrowserBrowseUrl}.
+ * @name CKEDITOR.config.filebrowserFlashBrowseUrl
+ * @since 3.0
+ * @type String
+ * @default '' (empty string = disabled)
+ * @example
+ * config.filebrowserFlashBrowseUrl = '/browser/browse.php?type=Flash';
+ */
+
+/**
+ * The location of a script that handles file uploads in the Image dialog.
+ * If not set, CKEditor will use {@link CKEDITOR.config.filebrowserUploadUrl}.
+ * @name CKEDITOR.config.filebrowserImageUploadUrl
+ * @since 3.0
+ * @type String
+ * @default '' (empty string = disabled)
+ * @example
+ * config.filebrowserImageUploadUrl = '/uploader/upload.php?type=Images';
+ */
+
+/**
+ * The location of a script that handles file uploads in the Flash dialog.
+ * If not set, CKEditor will use {@link CKEDITOR.config.filebrowserUploadUrl}.
+ * @name CKEDITOR.config.filebrowserFlashUploadUrl
+ * @since 3.0
+ * @type String
+ * @default '' (empty string = disabled)
+ * @example
+ * config.filebrowserFlashUploadUrl = '/uploader/upload.php?type=Flash';
+ */
+
+/**
+ * The location of an external file browser, that should be launched when "Browse Server" button is pressed in the Link tab of Image dialog.
+ * If not set, CKEditor will use {@link CKEDITOR.config.filebrowserBrowseUrl}.
+ * @name CKEDITOR.config.filebrowserImageBrowseLinkUrl
+ * @since 3.2
+ * @type String
+ * @default '' (empty string = disabled)
+ * @example
+ * config.filebrowserImageBrowseLinkUrl = '/browser/browse.php';
+ */
+
+/**
+ * The "features" to use in the file browser popup window.
+ * @name CKEDITOR.config.filebrowserWindowFeatures
+ * @since 3.4.1
+ * @type String
+ * @default 'location=no,menubar=no,toolbar=no,dependent=yes,minimizable=no,modal=yes,alwaysRaised=yes,resizable=yes,scrollbars=yes'
+ * @example
+ * config.filebrowserWindowFeatures = 'resizable=yes,scrollbars=no';
+ */
+
+/**
+ * The width of the file browser popup window. It can be a number or a percent string.
+ * @name CKEDITOR.config.filebrowserWindowWidth
+ * @type Number|String
+ * @default '80%'
+ * @example
+ * config.filebrowserWindowWidth = 750;
+ * @example
+ * config.filebrowserWindowWidth = '50%';
+ */
+
+/**
+ * The height of the file browser popup window. It can be a number or a percent string.
+ * @name CKEDITOR.config.filebrowserWindowHeight
+ * @type Number|String
+ * @default '70%'
+ * @example
+ * config.filebrowserWindowHeight = 580;
+ * @example
+ * config.filebrowserWindowHeight = '50%';
+ */
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/find/dialogs/find.js b/app/assets/javascripts/ckeditor/_source/plugins/find/dialogs/find.js
new file mode 100644
index 00000000..ccf4cf62
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/find/dialogs/find.js
@@ -0,0 +1,890 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+(function()
+{
+ var isReplace;
+
+ function findEvaluator( node )
+ {
+ return node.type == CKEDITOR.NODE_TEXT && node.getLength() > 0 && ( !isReplace || !node.isReadOnly() );
+ }
+
+ /**
+ * Elements which break characters been considered as sequence.
+ */
+ function nonCharactersBoundary( node )
+ {
+ return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary(
+ CKEDITOR.tools.extend( {}, CKEDITOR.dtd.$empty, CKEDITOR.dtd.$nonEditable ) ) );
+ }
+
+ /**
+ * Get the cursor object which represent both current character and it's dom
+ * position thing.
+ */
+ var cursorStep = function()
+ {
+ return {
+ textNode : this.textNode,
+ offset : this.offset,
+ character : this.textNode ?
+ this.textNode.getText().charAt( this.offset ) : null,
+ hitMatchBoundary : this._.matchBoundary
+ };
+ };
+
+ var pages = [ 'find', 'replace' ],
+ fieldsMapping = [
+ [ 'txtFindFind', 'txtFindReplace' ],
+ [ 'txtFindCaseChk', 'txtReplaceCaseChk' ],
+ [ 'txtFindWordChk', 'txtReplaceWordChk' ],
+ [ 'txtFindCyclic', 'txtReplaceCyclic' ] ];
+
+ /**
+ * Synchronize corresponding filed values between 'replace' and 'find' pages.
+ * @param {String} currentPageId The page id which receive values.
+ */
+ function syncFieldsBetweenTabs( currentPageId )
+ {
+ var sourceIndex, targetIndex,
+ sourceField, targetField;
+
+ sourceIndex = currentPageId === 'find' ? 1 : 0;
+ targetIndex = 1 - sourceIndex;
+ var i, l = fieldsMapping.length;
+ for ( i = 0 ; i < l ; i++ )
+ {
+ sourceField = this.getContentElement( pages[ sourceIndex ],
+ fieldsMapping[ i ][ sourceIndex ] );
+ targetField = this.getContentElement( pages[ targetIndex ],
+ fieldsMapping[ i ][ targetIndex ] );
+
+ targetField.setValue( sourceField.getValue() );
+ }
+ }
+
+ var findDialog = function( editor, startupPage )
+ {
+ // Style object for highlights: (#5018)
+ // 1. Defined as full match style to avoid compromising ordinary text color styles.
+ // 2. Must be apply onto inner-most text to avoid conflicting with ordinary text color styles visually.
+ var highlightStyle = new CKEDITOR.style( CKEDITOR.tools.extend( { fullMatch : true, childRule : function(){ return 0; } },
+ editor.config.find_highlight ) );
+
+ /**
+ * Iterator which walk through the specified range char by char. By
+ * default the walking will not stop at the character boundaries, until
+ * the end of the range is encountered.
+ * @param { CKEDITOR.dom.range } range
+ * @param {Boolean} matchWord Whether the walking will stop at character boundary.
+ */
+ var characterWalker = function( range , matchWord )
+ {
+ var self = this;
+ var walker =
+ new CKEDITOR.dom.walker( range );
+ walker.guard = matchWord ? nonCharactersBoundary : function( node )
+ {
+ !nonCharactersBoundary( node ) && ( self._.matchBoundary = true );
+ };
+ walker[ 'evaluator' ] = findEvaluator;
+ walker.breakOnFalse = 1;
+
+ if ( range.startContainer.type == CKEDITOR.NODE_TEXT )
+ {
+ this.textNode = range.startContainer;
+ this.offset = range.startOffset - 1;
+ }
+
+ this._ = {
+ matchWord : matchWord,
+ walker : walker,
+ matchBoundary : false
+ };
+ };
+
+ characterWalker.prototype = {
+ next : function()
+ {
+ return this.move();
+ },
+
+ back : function()
+ {
+ return this.move( true );
+ },
+
+ move : function( rtl )
+ {
+ var currentTextNode = this.textNode;
+ // Already at the end of document, no more character available.
+ if ( currentTextNode === null )
+ return cursorStep.call( this );
+
+ this._.matchBoundary = false;
+
+ // There are more characters in the text node, step forward.
+ if ( currentTextNode
+ && rtl
+ && this.offset > 0 )
+ {
+ this.offset--;
+ return cursorStep.call( this );
+ }
+ else if ( currentTextNode
+ && this.offset < currentTextNode.getLength() - 1 )
+ {
+ this.offset++;
+ return cursorStep.call( this );
+ }
+ else
+ {
+ currentTextNode = null;
+ // At the end of the text node, walking foward for the next.
+ while ( !currentTextNode )
+ {
+ currentTextNode =
+ this._.walker[ rtl ? 'previous' : 'next' ].call( this._.walker );
+
+ // Stop searching if we're need full word match OR
+ // already reach document end.
+ if ( this._.matchWord && !currentTextNode
+ || this._.walker._.end )
+ break;
+ }
+ // Found a fresh text node.
+ this.textNode = currentTextNode;
+ if ( currentTextNode )
+ this.offset = rtl ? currentTextNode.getLength() - 1 : 0;
+ else
+ this.offset = 0;
+ }
+
+ return cursorStep.call( this );
+ }
+
+ };
+
+ /**
+ * A range of cursors which represent a trunk of characters which try to
+ * match, it has the same length as the pattern string.
+ */
+ var characterRange = function( characterWalker, rangeLength )
+ {
+ this._ = {
+ walker : characterWalker,
+ cursors : [],
+ rangeLength : rangeLength,
+ highlightRange : null,
+ isMatched : 0
+ };
+ };
+
+ characterRange.prototype = {
+ /**
+ * Translate this range to {@link CKEDITOR.dom.range}
+ */
+ toDomRange : function()
+ {
+ var range = new CKEDITOR.dom.range( editor.document );
+ var cursors = this._.cursors;
+ if ( cursors.length < 1 )
+ {
+ var textNode = this._.walker.textNode;
+ if ( textNode )
+ range.setStartAfter( textNode );
+ else
+ return null;
+ }
+ else
+ {
+ var first = cursors[0],
+ last = cursors[ cursors.length - 1 ];
+
+ range.setStart( first.textNode, first.offset );
+ range.setEnd( last.textNode, last.offset + 1 );
+ }
+
+ return range;
+ },
+ /**
+ * Reflect the latest changes from dom range.
+ */
+ updateFromDomRange : function( domRange )
+ {
+ var cursor,
+ walker = new characterWalker( domRange );
+ this._.cursors = [];
+ do
+ {
+ cursor = walker.next();
+ if ( cursor.character )
+ this._.cursors.push( cursor );
+ }
+ while ( cursor.character );
+ this._.rangeLength = this._.cursors.length;
+ },
+
+ setMatched : function()
+ {
+ this._.isMatched = true;
+ },
+
+ clearMatched : function()
+ {
+ this._.isMatched = false;
+ },
+
+ isMatched : function()
+ {
+ return this._.isMatched;
+ },
+
+ /**
+ * Hightlight the current matched chunk of text.
+ */
+ highlight : function()
+ {
+ // Do not apply if nothing is found.
+ if ( this._.cursors.length < 1 )
+ return;
+
+ // Remove the previous highlight if there's one.
+ if ( this._.highlightRange )
+ this.removeHighlight();
+
+ // Apply the highlight.
+ var range = this.toDomRange(),
+ bookmark = range.createBookmark();
+ highlightStyle.applyToRange( range );
+ range.moveToBookmark( bookmark );
+ this._.highlightRange = range;
+
+ // Scroll the editor to the highlighted area.
+ var element = range.startContainer;
+ if ( element.type != CKEDITOR.NODE_ELEMENT )
+ element = element.getParent();
+ element.scrollIntoView();
+
+ // Update the character cursors.
+ this.updateFromDomRange( range );
+ },
+
+ /**
+ * Remove highlighted find result.
+ */
+ removeHighlight : function()
+ {
+ if ( !this._.highlightRange )
+ return;
+
+ var bookmark = this._.highlightRange.createBookmark();
+ highlightStyle.removeFromRange( this._.highlightRange );
+ this._.highlightRange.moveToBookmark( bookmark );
+ this.updateFromDomRange( this._.highlightRange );
+ this._.highlightRange = null;
+ },
+
+ isReadOnly : function()
+ {
+ if ( !this._.highlightRange )
+ return 0;
+
+ return this._.highlightRange.startContainer.isReadOnly();
+ },
+
+ moveBack : function()
+ {
+ var retval = this._.walker.back(),
+ cursors = this._.cursors;
+
+ if ( retval.hitMatchBoundary )
+ this._.cursors = cursors = [];
+
+ cursors.unshift( retval );
+ if ( cursors.length > this._.rangeLength )
+ cursors.pop();
+
+ return retval;
+ },
+
+ moveNext : function()
+ {
+ var retval = this._.walker.next(),
+ cursors = this._.cursors;
+
+ // Clear the cursors queue if we've crossed a match boundary.
+ if ( retval.hitMatchBoundary )
+ this._.cursors = cursors = [];
+
+ cursors.push( retval );
+ if ( cursors.length > this._.rangeLength )
+ cursors.shift();
+
+ return retval;
+ },
+
+ getEndCharacter : function()
+ {
+ var cursors = this._.cursors;
+ if ( cursors.length < 1 )
+ return null;
+
+ return cursors[ cursors.length - 1 ].character;
+ },
+
+ getNextCharacterRange : function( maxLength )
+ {
+ var lastCursor,
+ nextRangeWalker,
+ cursors = this._.cursors;
+
+ if ( ( lastCursor = cursors[ cursors.length - 1 ] ) && lastCursor.textNode )
+ nextRangeWalker = new characterWalker( getRangeAfterCursor( lastCursor ) );
+ // In case it's an empty range (no cursors), figure out next range from walker (#4951).
+ else
+ nextRangeWalker = this._.walker;
+
+ return new characterRange( nextRangeWalker, maxLength );
+ },
+
+ getCursors : function()
+ {
+ return this._.cursors;
+ }
+ };
+
+
+ // The remaining document range after the character cursor.
+ function getRangeAfterCursor( cursor , inclusive )
+ {
+ var range = new CKEDITOR.dom.range();
+ range.setStart( cursor.textNode,
+ ( inclusive ? cursor.offset : cursor.offset + 1 ) );
+ range.setEndAt( editor.document.getBody(),
+ CKEDITOR.POSITION_BEFORE_END );
+ return range;
+ }
+
+ // The document range before the character cursor.
+ function getRangeBeforeCursor( cursor )
+ {
+ var range = new CKEDITOR.dom.range();
+ range.setStartAt( editor.document.getBody(),
+ CKEDITOR.POSITION_AFTER_START );
+ range.setEnd( cursor.textNode, cursor.offset );
+ return range;
+ }
+
+ var KMP_NOMATCH = 0,
+ KMP_ADVANCED = 1,
+ KMP_MATCHED = 2;
+ /**
+ * Examination the occurrence of a word which implement KMP algorithm.
+ */
+ var kmpMatcher = function( pattern, ignoreCase )
+ {
+ var overlap = [ -1 ];
+ if ( ignoreCase )
+ pattern = pattern.toLowerCase();
+ for ( var i = 0 ; i < pattern.length ; i++ )
+ {
+ overlap.push( overlap[i] + 1 );
+ while ( overlap[ i + 1 ] > 0
+ && pattern.charAt( i ) != pattern
+ .charAt( overlap[ i + 1 ] - 1 ) )
+ overlap[ i + 1 ] = overlap[ overlap[ i + 1 ] - 1 ] + 1;
+ }
+
+ this._ = {
+ overlap : overlap,
+ state : 0,
+ ignoreCase : !!ignoreCase,
+ pattern : pattern
+ };
+ };
+
+ kmpMatcher.prototype =
+ {
+ feedCharacter : function( c )
+ {
+ if ( this._.ignoreCase )
+ c = c.toLowerCase();
+
+ while ( true )
+ {
+ if ( c == this._.pattern.charAt( this._.state ) )
+ {
+ this._.state++;
+ if ( this._.state == this._.pattern.length )
+ {
+ this._.state = 0;
+ return KMP_MATCHED;
+ }
+ return KMP_ADVANCED;
+ }
+ else if ( !this._.state )
+ return KMP_NOMATCH;
+ else
+ this._.state = this._.overlap[ this._.state ];
+ }
+
+ return null;
+ },
+
+ reset : function()
+ {
+ this._.state = 0;
+ }
+ };
+
+ var wordSeparatorRegex =
+ /[.,"'?!;: \u0085\u00a0\u1680\u280e\u2028\u2029\u202f\u205f\u3000]/;
+
+ var isWordSeparator = function( c )
+ {
+ if ( !c )
+ return true;
+ var code = c.charCodeAt( 0 );
+ return ( code >= 9 && code <= 0xd )
+ || ( code >= 0x2000 && code <= 0x200a )
+ || wordSeparatorRegex.test( c );
+ };
+
+ var finder = {
+ searchRange : null,
+ matchRange : null,
+ find : function( pattern, matchCase, matchWord, matchCyclic, highlightMatched, cyclicRerun )
+ {
+ if ( !this.matchRange )
+ this.matchRange =
+ new characterRange(
+ new characterWalker( this.searchRange ),
+ pattern.length );
+ else
+ {
+ this.matchRange.removeHighlight();
+ this.matchRange = this.matchRange.getNextCharacterRange( pattern.length );
+ }
+
+ var matcher = new kmpMatcher( pattern, !matchCase ),
+ matchState = KMP_NOMATCH,
+ character = '%';
+
+ while ( character !== null )
+ {
+ this.matchRange.moveNext();
+ while ( ( character = this.matchRange.getEndCharacter() ) )
+ {
+ matchState = matcher.feedCharacter( character );
+ if ( matchState == KMP_MATCHED )
+ break;
+ if ( this.matchRange.moveNext().hitMatchBoundary )
+ matcher.reset();
+ }
+
+ if ( matchState == KMP_MATCHED )
+ {
+ if ( matchWord )
+ {
+ var cursors = this.matchRange.getCursors(),
+ tail = cursors[ cursors.length - 1 ],
+ head = cursors[ 0 ];
+
+ var headWalker = new characterWalker( getRangeBeforeCursor( head ), true ),
+ tailWalker = new characterWalker( getRangeAfterCursor( tail ), true );
+
+ if ( ! ( isWordSeparator( headWalker.back().character )
+ && isWordSeparator( tailWalker.next().character ) ) )
+ continue;
+ }
+ this.matchRange.setMatched();
+ if ( highlightMatched !== false )
+ this.matchRange.highlight();
+ return true;
+ }
+ }
+
+ this.matchRange.clearMatched();
+ this.matchRange.removeHighlight();
+ // Clear current session and restart with the default search
+ // range.
+ // Re-run the finding once for cyclic.(#3517)
+ if ( matchCyclic && !cyclicRerun )
+ {
+ this.searchRange = getSearchRange( 1 );
+ this.matchRange = null;
+ return arguments.callee.apply( this,
+ Array.prototype.slice.call( arguments ).concat( [ true ] ) );
+ }
+
+ return false;
+ },
+
+ /**
+ * Record how much replacement occurred toward one replacing.
+ */
+ replaceCounter : 0,
+
+ replace : function( dialog, pattern, newString, matchCase, matchWord,
+ matchCyclic , isReplaceAll )
+ {
+ isReplace = 1;
+
+ // Successiveness of current replace/find.
+ var result = 0;
+
+ // 1. Perform the replace when there's already a match here.
+ // 2. Otherwise perform the find but don't replace it immediately.
+ if ( this.matchRange && this.matchRange.isMatched()
+ && !this.matchRange._.isReplaced && !this.matchRange.isReadOnly() )
+ {
+ // Turn off highlight for a while when saving snapshots.
+ this.matchRange.removeHighlight();
+ var domRange = this.matchRange.toDomRange();
+ var text = editor.document.createText( newString );
+ if ( !isReplaceAll )
+ {
+ // Save undo snaps before and after the replacement.
+ var selection = editor.getSelection();
+ selection.selectRanges( [ domRange ] );
+ editor.fire( 'saveSnapshot' );
+ }
+ domRange.deleteContents();
+ domRange.insertNode( text );
+ if ( !isReplaceAll )
+ {
+ selection.selectRanges( [ domRange ] );
+ editor.fire( 'saveSnapshot' );
+ }
+ this.matchRange.updateFromDomRange( domRange );
+ if ( !isReplaceAll )
+ this.matchRange.highlight();
+ this.matchRange._.isReplaced = true;
+ this.replaceCounter++;
+ result = 1;
+ }
+ else
+ result = this.find( pattern, matchCase, matchWord, matchCyclic, !isReplaceAll );
+
+ isReplace = 0;
+
+ return result;
+ }
+ };
+
+ /**
+ * The range in which find/replace happened, receive from user
+ * selection prior.
+ */
+ function getSearchRange( isDefault )
+ {
+ var searchRange,
+ sel = editor.getSelection(),
+ body = editor.document.getBody();
+ if ( sel && !isDefault )
+ {
+ searchRange = sel.getRanges()[ 0 ].clone();
+ searchRange.collapse( true );
+ }
+ else
+ {
+ searchRange = new CKEDITOR.dom.range();
+ searchRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
+ }
+ searchRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
+ return searchRange;
+ }
+
+ var lang = editor.lang.findAndReplace;
+ return {
+ title : lang.title,
+ resizable : CKEDITOR.DIALOG_RESIZE_NONE,
+ minWidth : 350,
+ minHeight : 170,
+ buttons : [ CKEDITOR.dialog.cancelButton ], // Cancel button only.
+ contents : [
+ {
+ id : 'find',
+ label : lang.find,
+ title : lang.find,
+ accessKey : '',
+ elements : [
+ {
+ type : 'hbox',
+ widths : [ '230px', '90px' ],
+ children :
+ [
+ {
+ type : 'text',
+ id : 'txtFindFind',
+ label : lang.findWhat,
+ isChanged : false,
+ labelLayout : 'horizontal',
+ accessKey : 'F'
+ },
+ {
+ type : 'button',
+ align : 'left',
+ style : 'width:100%',
+ label : lang.find,
+ onClick : function()
+ {
+ var dialog = this.getDialog();
+ if ( !finder.find( dialog.getValueOf( 'find', 'txtFindFind' ),
+ dialog.getValueOf( 'find', 'txtFindCaseChk' ),
+ dialog.getValueOf( 'find', 'txtFindWordChk' ),
+ dialog.getValueOf( 'find', 'txtFindCyclic' ) ) )
+ alert( lang
+ .notFoundMsg );
+ }
+ }
+ ]
+ },
+ {
+ type : 'vbox',
+ padding : 0,
+ children :
+ [
+ {
+ type : 'checkbox',
+ id : 'txtFindCaseChk',
+ isChanged : false,
+ style : 'margin-top:28px',
+ label : lang.matchCase
+ },
+ {
+ type : 'checkbox',
+ id : 'txtFindWordChk',
+ isChanged : false,
+ label : lang.matchWord
+ },
+ {
+ type : 'checkbox',
+ id : 'txtFindCyclic',
+ isChanged : false,
+ 'default' : true,
+ label : lang.matchCyclic
+ }
+ ]
+ }
+ ]
+ },
+ {
+ id : 'replace',
+ label : lang.replace,
+ accessKey : 'M',
+ elements : [
+ {
+ type : 'hbox',
+ widths : [ '230px', '90px' ],
+ children :
+ [
+ {
+ type : 'text',
+ id : 'txtFindReplace',
+ label : lang.findWhat,
+ isChanged : false,
+ labelLayout : 'horizontal',
+ accessKey : 'F'
+ },
+ {
+ type : 'button',
+ align : 'left',
+ style : 'width:100%',
+ label : lang.replace,
+ onClick : function()
+ {
+ var dialog = this.getDialog();
+ if ( !finder.replace( dialog,
+ dialog.getValueOf( 'replace', 'txtFindReplace' ),
+ dialog.getValueOf( 'replace', 'txtReplace' ),
+ dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
+ dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
+ dialog.getValueOf( 'replace', 'txtReplaceCyclic' ) ) )
+ alert( lang
+ .notFoundMsg );
+ }
+ }
+ ]
+ },
+ {
+ type : 'hbox',
+ widths : [ '230px', '90px' ],
+ children :
+ [
+ {
+ type : 'text',
+ id : 'txtReplace',
+ label : lang.replaceWith,
+ isChanged : false,
+ labelLayout : 'horizontal',
+ accessKey : 'R'
+ },
+ {
+ type : 'button',
+ align : 'left',
+ style : 'width:100%',
+ label : lang.replaceAll,
+ isChanged : false,
+ onClick : function()
+ {
+ var dialog = this.getDialog();
+ var replaceNums;
+
+ finder.replaceCounter = 0;
+
+ // Scope to full document.
+ finder.searchRange = getSearchRange( 1 );
+ if ( finder.matchRange )
+ {
+ finder.matchRange.removeHighlight();
+ finder.matchRange = null;
+ }
+ editor.fire( 'saveSnapshot' );
+ while ( finder.replace( dialog,
+ dialog.getValueOf( 'replace', 'txtFindReplace' ),
+ dialog.getValueOf( 'replace', 'txtReplace' ),
+ dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
+ dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
+ false, true ) )
+ { /*jsl:pass*/ }
+
+ if ( finder.replaceCounter )
+ {
+ alert( lang.replaceSuccessMsg.replace( /%1/, finder.replaceCounter ) );
+ editor.fire( 'saveSnapshot' );
+ }
+ else
+ alert( lang.notFoundMsg );
+ }
+ }
+ ]
+ },
+ {
+ type : 'vbox',
+ padding : 0,
+ children :
+ [
+ {
+ type : 'checkbox',
+ id : 'txtReplaceCaseChk',
+ isChanged : false,
+ label : lang
+ .matchCase
+ },
+ {
+ type : 'checkbox',
+ id : 'txtReplaceWordChk',
+ isChanged : false,
+ label : lang
+ .matchWord
+ },
+ {
+ type : 'checkbox',
+ id : 'txtReplaceCyclic',
+ isChanged : false,
+ 'default' : true,
+ label : lang
+ .matchCyclic
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ onLoad : function()
+ {
+ var dialog = this;
+
+ // Keep track of the current pattern field in use.
+ var patternField, wholeWordChkField;
+
+ // Ignore initial page select on dialog show
+ var isUserSelect = 0;
+ this.on( 'hide', function()
+ {
+ isUserSelect = 0;
+ });
+ this.on( 'show', function()
+ {
+ isUserSelect = 1;
+ });
+
+ this.selectPage = CKEDITOR.tools.override( this.selectPage, function( originalFunc )
+ {
+ return function( pageId )
+ {
+ originalFunc.call( dialog, pageId );
+
+ var currPage = dialog._.tabs[ pageId ];
+ var patternFieldInput, patternFieldId, wholeWordChkFieldId;
+ patternFieldId = pageId === 'find' ? 'txtFindFind' : 'txtFindReplace';
+ wholeWordChkFieldId = pageId === 'find' ? 'txtFindWordChk' : 'txtReplaceWordChk';
+
+ patternField = dialog.getContentElement( pageId,
+ patternFieldId );
+ wholeWordChkField = dialog.getContentElement( pageId,
+ wholeWordChkFieldId );
+
+ // Prepare for check pattern text filed 'keyup' event
+ if ( !currPage.initialized )
+ {
+ patternFieldInput = CKEDITOR.document
+ .getById( patternField._.inputId );
+ currPage.initialized = true;
+ }
+
+ // Synchronize fields on tab switch.
+ if ( isUserSelect )
+ syncFieldsBetweenTabs.call( this, pageId );
+ };
+ } );
+
+ },
+ onShow : function()
+ {
+ // Establish initial searching start position.
+ finder.searchRange = getSearchRange();
+
+ this.selectPage( startupPage );
+ },
+ onHide : function()
+ {
+ var range;
+ if ( finder.matchRange && finder.matchRange.isMatched() )
+ {
+ finder.matchRange.removeHighlight();
+ editor.focus();
+
+ range = finder.matchRange.toDomRange();
+ if ( range )
+ editor.getSelection().selectRanges( [ range ] );
+ }
+
+ // Clear current session before dialog close
+ delete finder.matchRange;
+ },
+ onFocus : function()
+ {
+ if ( startupPage == 'replace' )
+ return this.getContentElement( 'replace', 'txtFindReplace' );
+ else
+ return this.getContentElement( 'find', 'txtFindFind' );
+ }
+ };
+ };
+
+ CKEDITOR.dialog.add( 'find', function( editor )
+ {
+ return findDialog( editor, 'find' );
+ });
+
+ CKEDITOR.dialog.add( 'replace', function( editor )
+ {
+ return findDialog( editor, 'replace' );
+ });
+})();
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/find/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/find/plugin.js
new file mode 100644
index 00000000..148d08b8
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/find/plugin.js
@@ -0,0 +1,46 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.plugins.add( 'find',
+{
+ init : function( editor )
+ {
+ var forms = CKEDITOR.plugins.find;
+ editor.ui.addButton( 'Find',
+ {
+ label : editor.lang.findAndReplace.find,
+ command : 'find'
+ });
+ var findCommand = editor.addCommand( 'find', new CKEDITOR.dialogCommand( 'find' ) );
+ findCommand.canUndo = false;
+
+ editor.ui.addButton( 'Replace',
+ {
+ label : editor.lang.findAndReplace.replace,
+ command : 'replace'
+ });
+ var replaceCommand = editor.addCommand( 'replace', new CKEDITOR.dialogCommand( 'replace' ) );
+ replaceCommand.canUndo = false;
+
+ CKEDITOR.dialog.add( 'find', this.path + 'dialogs/find.js' );
+ CKEDITOR.dialog.add( 'replace', this.path + 'dialogs/find.js' );
+ },
+
+ requires : [ 'styles' ]
+} );
+
+/**
+ * Defines the style to be used to highlight results with the find dialog.
+ * @type Object
+ * @default { element : 'span', styles : { 'background-color' : '#004', 'color' : '#fff' } }
+ * @example
+ * // Highlight search results with blue on yellow.
+ * config.find_highlight =
+ * {
+ * element : 'span',
+ * styles : { 'background-color' : '#ff0', 'color' : '#00f' }
+ * };
+ */
+CKEDITOR.config.find_highlight = { element : 'span', styles : { 'background-color' : '#004', 'color' : '#fff' } };
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/flash/dialogs/flash.js b/app/assets/javascripts/ckeditor/_source/plugins/flash/dialogs/flash.js
new file mode 100644
index 00000000..997ced03
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/flash/dialogs/flash.js
@@ -0,0 +1,698 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+(function()
+{
+ /*
+ * It is possible to set things in three different places.
+ * 1. As attributes in the object tag.
+ * 2. As param tags under the object tag.
+ * 3. As attributes in the embed tag.
+ * It is possible for a single attribute to be present in more than one place.
+ * So let's define a mapping between a sementic attribute and its syntactic
+ * equivalents.
+ * Then we'll set and retrieve attribute values according to the mapping,
+ * instead of having to check and set each syntactic attribute every time.
+ *
+ * Reference: http://kb.adobe.com/selfservice/viewContent.do?externalId=tn_12701
+ */
+ var ATTRTYPE_OBJECT = 1,
+ ATTRTYPE_PARAM = 2,
+ ATTRTYPE_EMBED = 4;
+
+ var attributesMap =
+ {
+ id : [ { type : ATTRTYPE_OBJECT, name : 'id' } ],
+ classid : [ { type : ATTRTYPE_OBJECT, name : 'classid' } ],
+ codebase : [ { type : ATTRTYPE_OBJECT, name : 'codebase'} ],
+ pluginspage : [ { type : ATTRTYPE_EMBED, name : 'pluginspage' } ],
+ src : [ { type : ATTRTYPE_PARAM, name : 'movie' }, { type : ATTRTYPE_EMBED, name : 'src' }, { type : ATTRTYPE_OBJECT, name : 'data' } ],
+ name : [ { type : ATTRTYPE_EMBED, name : 'name' } ],
+ align : [ { type : ATTRTYPE_OBJECT, name : 'align' } ],
+ title : [ { type : ATTRTYPE_OBJECT, name : 'title' }, { type : ATTRTYPE_EMBED, name : 'title' } ],
+ 'class' : [ { type : ATTRTYPE_OBJECT, name : 'class' }, { type : ATTRTYPE_EMBED, name : 'class'} ],
+ width : [ { type : ATTRTYPE_OBJECT, name : 'width' }, { type : ATTRTYPE_EMBED, name : 'width' } ],
+ height : [ { type : ATTRTYPE_OBJECT, name : 'height' }, { type : ATTRTYPE_EMBED, name : 'height' } ],
+ hSpace : [ { type : ATTRTYPE_OBJECT, name : 'hSpace' }, { type : ATTRTYPE_EMBED, name : 'hSpace' } ],
+ vSpace : [ { type : ATTRTYPE_OBJECT, name : 'vSpace' }, { type : ATTRTYPE_EMBED, name : 'vSpace' } ],
+ style : [ { type : ATTRTYPE_OBJECT, name : 'style' }, { type : ATTRTYPE_EMBED, name : 'style' } ],
+ type : [ { type : ATTRTYPE_EMBED, name : 'type' } ]
+ };
+
+ var names = [ 'play', 'loop', 'menu', 'quality', 'scale', 'salign', 'wmode', 'bgcolor', 'base', 'flashvars', 'allowScriptAccess',
+ 'allowFullScreen' ];
+ for ( var i = 0 ; i < names.length ; i++ )
+ attributesMap[ names[i] ] = [ { type : ATTRTYPE_EMBED, name : names[i] }, { type : ATTRTYPE_PARAM, name : names[i] } ];
+ names = [ 'allowFullScreen', 'play', 'loop', 'menu' ];
+ for ( i = 0 ; i < names.length ; i++ )
+ attributesMap[ names[i] ][0]['default'] = attributesMap[ names[i] ][1]['default'] = true;
+
+ function loadValue( objectNode, embedNode, paramMap )
+ {
+ var attributes = attributesMap[ this.id ];
+ if ( !attributes )
+ return;
+
+ var isCheckbox = ( this instanceof CKEDITOR.ui.dialog.checkbox );
+ for ( var i = 0 ; i < attributes.length ; i++ )
+ {
+ var attrDef = attributes[ i ];
+ switch ( attrDef.type )
+ {
+ case ATTRTYPE_OBJECT:
+ if ( !objectNode )
+ continue;
+ if ( objectNode.getAttribute( attrDef.name ) !== null )
+ {
+ var value = objectNode.getAttribute( attrDef.name );
+ if ( isCheckbox )
+ this.setValue( value.toLowerCase() == 'true' );
+ else
+ this.setValue( value );
+ return;
+ }
+ else if ( isCheckbox )
+ this.setValue( !!attrDef[ 'default' ] );
+ break;
+ case ATTRTYPE_PARAM:
+ if ( !objectNode )
+ continue;
+ if ( attrDef.name in paramMap )
+ {
+ value = paramMap[ attrDef.name ];
+ if ( isCheckbox )
+ this.setValue( value.toLowerCase() == 'true' );
+ else
+ this.setValue( value );
+ return;
+ }
+ else if ( isCheckbox )
+ this.setValue( !!attrDef[ 'default' ] );
+ break;
+ case ATTRTYPE_EMBED:
+ if ( !embedNode )
+ continue;
+ if ( embedNode.getAttribute( attrDef.name ) )
+ {
+ value = embedNode.getAttribute( attrDef.name );
+ if ( isCheckbox )
+ this.setValue( value.toLowerCase() == 'true' );
+ else
+ this.setValue( value );
+ return;
+ }
+ else if ( isCheckbox )
+ this.setValue( !!attrDef[ 'default' ] );
+ }
+ }
+ }
+
+ function commitValue( objectNode, embedNode, paramMap )
+ {
+ var attributes = attributesMap[ this.id ];
+ if ( !attributes )
+ return;
+
+ var isRemove = ( this.getValue() === '' ),
+ isCheckbox = ( this instanceof CKEDITOR.ui.dialog.checkbox );
+
+ for ( var i = 0 ; i < attributes.length ; i++ )
+ {
+ var attrDef = attributes[i];
+ switch ( attrDef.type )
+ {
+ case ATTRTYPE_OBJECT:
+ if ( !objectNode )
+ continue;
+ var value = this.getValue();
+ if ( isRemove || isCheckbox && value === attrDef[ 'default' ] )
+ objectNode.removeAttribute( attrDef.name );
+ else
+ objectNode.setAttribute( attrDef.name, value );
+ break;
+ case ATTRTYPE_PARAM:
+ if ( !objectNode )
+ continue;
+ value = this.getValue();
+ if ( isRemove || isCheckbox && value === attrDef[ 'default' ] )
+ {
+ if ( attrDef.name in paramMap )
+ paramMap[ attrDef.name ].remove();
+ }
+ else
+ {
+ if ( attrDef.name in paramMap )
+ paramMap[ attrDef.name ].setAttribute( 'value', value );
+ else
+ {
+ var param = CKEDITOR.dom.element.createFromHtml( ' ', objectNode.getDocument() );
+ param.setAttributes( { name : attrDef.name, value : value } );
+ if ( objectNode.getChildCount() < 1 )
+ param.appendTo( objectNode );
+ else
+ param.insertBefore( objectNode.getFirst() );
+ }
+ }
+ break;
+ case ATTRTYPE_EMBED:
+ if ( !embedNode )
+ continue;
+ value = this.getValue();
+ if ( isRemove || isCheckbox && value === attrDef[ 'default' ])
+ embedNode.removeAttribute( attrDef.name );
+ else
+ embedNode.setAttribute( attrDef.name, value );
+ }
+ }
+ }
+
+ CKEDITOR.dialog.add( 'flash', function( editor )
+ {
+ var makeObjectTag = !editor.config.flashEmbedTagOnly,
+ makeEmbedTag = editor.config.flashAddEmbedTag || editor.config.flashEmbedTagOnly;
+
+ var previewPreloader,
+ previewAreaHtml = '' + CKEDITOR.tools.htmlEncode( editor.lang.common.preview ) +'
' +
+ '
' +
+ '
';
+
+ return {
+ title : editor.lang.flash.title,
+ minWidth : 420,
+ minHeight : 310,
+ onShow : function()
+ {
+ // Clear previously saved elements.
+ this.fakeImage = this.objectNode = this.embedNode = null;
+ previewPreloader = new CKEDITOR.dom.element( 'embed', editor.document );
+
+ // Try to detect any embed or object tag that has Flash parameters.
+ var fakeImage = this.getSelectedElement();
+ if ( fakeImage && fakeImage.data( 'cke-real-element-type' ) && fakeImage.data( 'cke-real-element-type' ) == 'flash' )
+ {
+ this.fakeImage = fakeImage;
+
+ var realElement = editor.restoreRealElement( fakeImage ),
+ objectNode = null, embedNode = null, paramMap = {};
+ if ( realElement.getName() == 'cke:object' )
+ {
+ objectNode = realElement;
+ var embedList = objectNode.getElementsByTag( 'embed', 'cke' );
+ if ( embedList.count() > 0 )
+ embedNode = embedList.getItem( 0 );
+ var paramList = objectNode.getElementsByTag( 'param', 'cke' );
+ for ( var i = 0, length = paramList.count() ; i < length ; i++ )
+ {
+ var item = paramList.getItem( i ),
+ name = item.getAttribute( 'name' ),
+ value = item.getAttribute( 'value' );
+ paramMap[ name ] = value;
+ }
+ }
+ else if ( realElement.getName() == 'cke:embed' )
+ embedNode = realElement;
+
+ this.objectNode = objectNode;
+ this.embedNode = embedNode;
+
+ this.setupContent( objectNode, embedNode, paramMap, fakeImage );
+ }
+ },
+ onOk : function()
+ {
+ // If there's no selected object or embed, create one. Otherwise, reuse the
+ // selected object and embed nodes.
+ var objectNode = null,
+ embedNode = null,
+ paramMap = null;
+ if ( !this.fakeImage )
+ {
+ if ( makeObjectTag )
+ {
+ objectNode = CKEDITOR.dom.element.createFromHtml( ' ', editor.document );
+ var attributes = {
+ classid : 'clsid:d27cdb6e-ae6d-11cf-96b8-444553540000',
+ codebase : 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0'
+ };
+ objectNode.setAttributes( attributes );
+ }
+ if ( makeEmbedTag )
+ {
+ embedNode = CKEDITOR.dom.element.createFromHtml( ' ', editor.document );
+ embedNode.setAttributes(
+ {
+ type : 'application/x-shockwave-flash',
+ pluginspage : 'http://www.macromedia.com/go/getflashplayer'
+ } );
+ if ( objectNode )
+ embedNode.appendTo( objectNode );
+ }
+ }
+ else
+ {
+ objectNode = this.objectNode;
+ embedNode = this.embedNode;
+ }
+
+ // Produce the paramMap if there's an object tag.
+ if ( objectNode )
+ {
+ paramMap = {};
+ var paramList = objectNode.getElementsByTag( 'param', 'cke' );
+ for ( var i = 0, length = paramList.count() ; i < length ; i++ )
+ paramMap[ paramList.getItem( i ).getAttribute( 'name' ) ] = paramList.getItem( i );
+ }
+
+ // A subset of the specified attributes/styles
+ // should also be applied on the fake element to
+ // have better visual effect. (#5240)
+ var extraStyles = {}, extraAttributes = {};
+ this.commitContent( objectNode, embedNode, paramMap, extraStyles, extraAttributes );
+
+ // Refresh the fake image.
+ var newFakeImage = editor.createFakeElement( objectNode || embedNode, 'cke_flash', 'flash', true );
+ newFakeImage.setAttributes( extraAttributes );
+ newFakeImage.setStyles( extraStyles );
+ if ( this.fakeImage )
+ {
+ newFakeImage.replace( this.fakeImage );
+ editor.getSelection().selectElement( newFakeImage );
+ }
+ else
+ editor.insertElement( newFakeImage );
+ },
+
+ onHide : function()
+ {
+ if ( this.preview )
+ this.preview.setHtml('');
+ },
+
+ contents : [
+ {
+ id : 'info',
+ label : editor.lang.common.generalTab,
+ accessKey : 'I',
+ elements :
+ [
+ {
+ type : 'vbox',
+ padding : 0,
+ children :
+ [
+ {
+ type : 'hbox',
+ widths : [ '280px', '110px' ],
+ align : 'right',
+ children :
+ [
+ {
+ id : 'src',
+ type : 'text',
+ label : editor.lang.common.url,
+ required : true,
+ validate : CKEDITOR.dialog.validate.notEmpty( editor.lang.flash.validateSrc ),
+ setup : loadValue,
+ commit : commitValue,
+ onLoad : function()
+ {
+ var dialog = this.getDialog(),
+ updatePreview = function( src ){
+ // Query the preloader to figure out the url impacted by based href.
+ previewPreloader.setAttribute( 'src', src );
+ dialog.preview.setHtml( ' ' );
+ };
+ // Preview element
+ dialog.preview = dialog.getContentElement( 'info', 'preview' ).getElement().getChild( 3 );
+
+ // Sync on inital value loaded.
+ this.on( 'change', function( evt ){
+
+ if ( evt.data && evt.data.value )
+ updatePreview( evt.data.value );
+ } );
+ // Sync when input value changed.
+ this.getInputElement().on( 'change', function( evt ){
+
+ updatePreview( this.getValue() );
+ }, this );
+ }
+ },
+ {
+ type : 'button',
+ id : 'browse',
+ filebrowser : 'info:src',
+ hidden : true,
+ // v-align with the 'src' field.
+ // TODO: We need something better than a fixed size here.
+ style : 'display:inline-block;margin-top:10px;',
+ label : editor.lang.common.browseServer
+ }
+ ]
+ }
+ ]
+ },
+ {
+ type : 'hbox',
+ widths : [ '25%', '25%', '25%', '25%', '25%' ],
+ children :
+ [
+ {
+ type : 'text',
+ id : 'width',
+ style : 'width:95px',
+ label : editor.lang.common.width,
+ validate : CKEDITOR.dialog.validate.integer( editor.lang.common.invalidWidth ),
+ setup : function( objectNode, embedNode, paramMap, fakeImage )
+ {
+ loadValue.apply( this, arguments );
+ if ( fakeImage )
+ {
+ var fakeImageWidth = parseInt( fakeImage.$.style.width, 10 );
+ if ( !isNaN( fakeImageWidth ) )
+ this.setValue( fakeImageWidth );
+ }
+ },
+ commit : function( objectNode, embedNode, paramMap, extraStyles )
+ {
+ commitValue.apply( this, arguments );
+ if ( this.getValue() )
+ extraStyles.width = this.getValue() + 'px';
+ }
+ },
+ {
+ type : 'text',
+ id : 'height',
+ style : 'width:95px',
+ label : editor.lang.common.height,
+ validate : CKEDITOR.dialog.validate.integer( editor.lang.common.invalidHeight ),
+ setup : function( objectNode, embedNode, paramMap, fakeImage )
+ {
+ loadValue.apply( this, arguments );
+ if ( fakeImage )
+ {
+ var fakeImageHeight = parseInt( fakeImage.$.style.height, 10 );
+ if ( !isNaN( fakeImageHeight ) )
+ this.setValue( fakeImageHeight );
+ }
+ },
+ commit : function( objectNode, embedNode, paramMap, extraStyles )
+ {
+ commitValue.apply( this, arguments );
+ if ( this.getValue() )
+ extraStyles.height = this.getValue() + 'px';
+ }
+ },
+ {
+ type : 'text',
+ id : 'hSpace',
+ style : 'width:95px',
+ label : editor.lang.flash.hSpace,
+ validate : CKEDITOR.dialog.validate.integer( editor.lang.flash.validateHSpace ),
+ setup : loadValue,
+ commit : commitValue
+ },
+ {
+ type : 'text',
+ id : 'vSpace',
+ style : 'width:95px',
+ label : editor.lang.flash.vSpace,
+ validate : CKEDITOR.dialog.validate.integer( editor.lang.flash.validateVSpace ),
+ setup : loadValue,
+ commit : commitValue
+ }
+ ]
+ },
+
+ {
+ type : 'vbox',
+ children :
+ [
+ {
+ type : 'html',
+ id : 'preview',
+ style : 'width:95%;',
+ html : previewAreaHtml
+ }
+ ]
+ }
+ ]
+ },
+ {
+ id : 'Upload',
+ hidden : true,
+ filebrowser : 'uploadButton',
+ label : editor.lang.common.upload,
+ elements :
+ [
+ {
+ type : 'file',
+ id : 'upload',
+ label : editor.lang.common.upload,
+ size : 38
+ },
+ {
+ type : 'fileButton',
+ id : 'uploadButton',
+ label : editor.lang.common.uploadSubmit,
+ filebrowser : 'info:src',
+ 'for' : [ 'Upload', 'upload' ]
+ }
+ ]
+ },
+ {
+ id : 'properties',
+ label : editor.lang.flash.propertiesTab,
+ elements :
+ [
+ {
+ type : 'hbox',
+ widths : [ '50%', '50%' ],
+ children :
+ [
+ {
+ id : 'scale',
+ type : 'select',
+ label : editor.lang.flash.scale,
+ 'default' : '',
+ style : 'width : 100%;',
+ items :
+ [
+ [ editor.lang.common.notSet , ''],
+ [ editor.lang.flash.scaleAll, 'showall' ],
+ [ editor.lang.flash.scaleNoBorder, 'noborder' ],
+ [ editor.lang.flash.scaleFit, 'exactfit' ]
+ ],
+ setup : loadValue,
+ commit : commitValue
+ },
+ {
+ id : 'allowScriptAccess',
+ type : 'select',
+ label : editor.lang.flash.access,
+ 'default' : '',
+ style : 'width : 100%;',
+ items :
+ [
+ [ editor.lang.common.notSet , ''],
+ [ editor.lang.flash.accessAlways, 'always' ],
+ [ editor.lang.flash.accessSameDomain, 'samedomain' ],
+ [ editor.lang.flash.accessNever, 'never' ]
+ ],
+ setup : loadValue,
+ commit : commitValue
+ }
+ ]
+ },
+ {
+ type : 'hbox',
+ widths : [ '50%', '50%' ],
+ children :
+ [
+ {
+ id : 'wmode',
+ type : 'select',
+ label : editor.lang.flash.windowMode,
+ 'default' : '',
+ style : 'width : 100%;',
+ items :
+ [
+ [ editor.lang.common.notSet , '' ],
+ [ editor.lang.flash.windowModeWindow, 'window' ],
+ [ editor.lang.flash.windowModeOpaque, 'opaque' ],
+ [ editor.lang.flash.windowModeTransparent, 'transparent' ]
+ ],
+ setup : loadValue,
+ commit : commitValue
+ },
+ {
+ id : 'quality',
+ type : 'select',
+ label : editor.lang.flash.quality,
+ 'default' : 'high',
+ style : 'width : 100%;',
+ items :
+ [
+ [ editor.lang.common.notSet , '' ],
+ [ editor.lang.flash.qualityBest, 'best' ],
+ [ editor.lang.flash.qualityHigh, 'high' ],
+ [ editor.lang.flash.qualityAutoHigh, 'autohigh' ],
+ [ editor.lang.flash.qualityMedium, 'medium' ],
+ [ editor.lang.flash.qualityAutoLow, 'autolow' ],
+ [ editor.lang.flash.qualityLow, 'low' ]
+ ],
+ setup : loadValue,
+ commit : commitValue
+ }
+ ]
+ },
+ {
+ type : 'hbox',
+ widths : [ '50%', '50%' ],
+ children :
+ [
+ {
+ id : 'align',
+ type : 'select',
+ label : editor.lang.common.align,
+ 'default' : '',
+ style : 'width : 100%;',
+ items :
+ [
+ [ editor.lang.common.notSet , ''],
+ [ editor.lang.common.alignLeft , 'left'],
+ [ editor.lang.flash.alignAbsBottom , 'absBottom'],
+ [ editor.lang.flash.alignAbsMiddle , 'absMiddle'],
+ [ editor.lang.flash.alignBaseline , 'baseline'],
+ [ editor.lang.common.alignBottom , 'bottom'],
+ [ editor.lang.common.alignMiddle , 'middle'],
+ [ editor.lang.common.alignRight , 'right'],
+ [ editor.lang.flash.alignTextTop , 'textTop'],
+ [ editor.lang.common.alignTop , 'top']
+ ],
+ setup : loadValue,
+ commit : function( objectNode, embedNode, paramMap, extraStyles, extraAttributes )
+ {
+ var value = this.getValue();
+ commitValue.apply( this, arguments );
+ value && ( extraAttributes.align = value );
+ }
+ },
+ {
+ type : 'html',
+ html : '
'
+ }
+ ]
+ },
+ {
+ type : 'fieldset',
+ label : CKEDITOR.tools.htmlEncode( editor.lang.flash.flashvars ),
+ children :
+ [
+ {
+ type : 'vbox',
+ padding : 0,
+ children :
+ [
+ {
+ type : 'checkbox',
+ id : 'menu',
+ label : editor.lang.flash.chkMenu,
+ 'default' : true,
+ setup : loadValue,
+ commit : commitValue
+ },
+ {
+ type : 'checkbox',
+ id : 'play',
+ label : editor.lang.flash.chkPlay,
+ 'default' : true,
+ setup : loadValue,
+ commit : commitValue
+ },
+ {
+ type : 'checkbox',
+ id : 'loop',
+ label : editor.lang.flash.chkLoop,
+ 'default' : true,
+ setup : loadValue,
+ commit : commitValue
+ },
+ {
+ type : 'checkbox',
+ id : 'allowFullScreen',
+ label : editor.lang.flash.chkFull,
+ 'default' : true,
+ setup : loadValue,
+ commit : commitValue
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ id : 'advanced',
+ label : editor.lang.common.advancedTab,
+ elements :
+ [
+ {
+ type : 'hbox',
+ widths : [ '45%', '55%' ],
+ children :
+ [
+ {
+ type : 'text',
+ id : 'id',
+ label : editor.lang.common.id,
+ setup : loadValue,
+ commit : commitValue
+ },
+ {
+ type : 'text',
+ id : 'title',
+ label : editor.lang.common.advisoryTitle,
+ setup : loadValue,
+ commit : commitValue
+ }
+ ]
+ },
+ {
+ type : 'hbox',
+ widths : [ '45%', '55%' ],
+ children :
+ [
+ {
+ type : 'text',
+ id : 'bgcolor',
+ label : editor.lang.flash.bgcolor,
+ setup : loadValue,
+ commit : commitValue
+ },
+ {
+ type : 'text',
+ id : 'class',
+ label : editor.lang.common.cssClass,
+ setup : loadValue,
+ commit : commitValue
+ }
+ ]
+ },
+ {
+ type : 'text',
+ id : 'style',
+ label : editor.lang.common.cssStyle,
+ setup : loadValue,
+ commit : commitValue
+ }
+ ]
+ }
+ ]
+ };
+ } );
+})();
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/flash/images/placeholder.png b/app/assets/javascripts/ckeditor/_source/plugins/flash/images/placeholder.png
new file mode 100644
index 00000000..0bc6caa7
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/flash/images/placeholder.png differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/flash/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/flash/plugin.js
new file mode 100644
index 00000000..7641a9ed
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/flash/plugin.js
@@ -0,0 +1,168 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+(function()
+{
+ var flashFilenameRegex = /\.swf(?:$|\?)/i;
+
+ var cssifyLength = CKEDITOR.tools.cssLength;
+
+ function isFlashEmbed( element )
+ {
+ var attributes = element.attributes;
+
+ return ( attributes.type == 'application/x-shockwave-flash' || flashFilenameRegex.test( attributes.src || '' ) );
+ }
+
+ function createFakeElement( editor, realElement )
+ {
+ var fakeElement = editor.createFakeParserElement( realElement, 'cke_flash', 'flash', true ),
+ fakeStyle = fakeElement.attributes.style || '';
+
+ var width = realElement.attributes.width,
+ height = realElement.attributes.height;
+
+ if ( typeof width != 'undefined' )
+ fakeStyle = fakeElement.attributes.style = fakeStyle + 'width:' + cssifyLength( width ) + ';';
+
+ if ( typeof height != 'undefined' )
+ fakeStyle = fakeElement.attributes.style = fakeStyle + 'height:' + cssifyLength( height ) + ';';
+
+ return fakeElement;
+ }
+
+ CKEDITOR.plugins.add( 'flash',
+ {
+ init : function( editor )
+ {
+ editor.addCommand( 'flash', new CKEDITOR.dialogCommand( 'flash' ) );
+ editor.ui.addButton( 'Flash',
+ {
+ label : editor.lang.common.flash,
+ command : 'flash'
+ });
+ CKEDITOR.dialog.add( 'flash', this.path + 'dialogs/flash.js' );
+
+ editor.addCss(
+ 'img.cke_flash' +
+ '{' +
+ 'background-image: url(' + CKEDITOR.getUrl( this.path + 'images/placeholder.png' ) + ');' +
+ 'background-position: center center;' +
+ 'background-repeat: no-repeat;' +
+ 'border: 1px solid #a9a9a9;' +
+ 'width: 80px;' +
+ 'height: 80px;' +
+ '}'
+ );
+
+ // If the "menu" plugin is loaded, register the menu items.
+ if ( editor.addMenuItems )
+ {
+ editor.addMenuItems(
+ {
+ flash :
+ {
+ label : editor.lang.flash.properties,
+ command : 'flash',
+ group : 'flash'
+ }
+ });
+ }
+
+ editor.on( 'doubleclick', function( evt )
+ {
+ var element = evt.data.element;
+
+ if ( element.is( 'img' ) && element.data( 'cke-real-element-type' ) == 'flash' )
+ evt.data.dialog = 'flash';
+ });
+
+ // If the "contextmenu" plugin is loaded, register the listeners.
+ if ( editor.contextMenu )
+ {
+ editor.contextMenu.addListener( function( element, selection )
+ {
+ if ( element && element.is( 'img' ) && !element.isReadOnly()
+ && element.data( 'cke-real-element-type' ) == 'flash' )
+ return { flash : CKEDITOR.TRISTATE_OFF };
+ });
+ }
+ },
+
+ afterInit : function( editor )
+ {
+ var dataProcessor = editor.dataProcessor,
+ dataFilter = dataProcessor && dataProcessor.dataFilter;
+
+ if ( dataFilter )
+ {
+ dataFilter.addRules(
+ {
+ elements :
+ {
+ 'cke:object' : function( element )
+ {
+ var attributes = element.attributes,
+ classId = attributes.classid && String( attributes.classid ).toLowerCase();
+
+ if ( !classId && !isFlashEmbed( element ) )
+ {
+ // Look for the inner
+ for ( var i = 0 ; i < element.children.length ; i++ )
+ {
+ if ( element.children[ i ].name == 'cke:embed' )
+ {
+ if ( !isFlashEmbed( element.children[ i ] ) )
+ return null;
+
+ return createFakeElement( editor, element );
+ }
+ }
+ return null;
+ }
+
+ return createFakeElement( editor, element );
+ },
+
+ 'cke:embed' : function( element )
+ {
+ if ( !isFlashEmbed( element ) )
+ return null;
+
+ return createFakeElement( editor, element );
+ }
+ }
+ },
+ 5);
+ }
+ },
+
+ requires : [ 'fakeobjects' ]
+ });
+})();
+
+CKEDITOR.tools.extend( CKEDITOR.config,
+{
+ /**
+ * Save as EMBED tag only. This tag is unrecommended.
+ * @type Boolean
+ * @default false
+ */
+ flashEmbedTagOnly : false,
+
+ /**
+ * Add EMBED tag as alternative: <object><embed></embed></object>
+ * @type Boolean
+ * @default false
+ */
+ flashAddEmbedTag : true,
+
+ /**
+ * Use embedTagOnly and addEmbedTag values on edit.
+ * @type Boolean
+ * @default false
+ */
+ flashConvertOnEdit : false
+} );
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/floatpanel/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/floatpanel/plugin.js
new file mode 100644
index 00000000..ab8c19f9
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/floatpanel/plugin.js
@@ -0,0 +1,405 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.plugins.add( 'floatpanel',
+{
+ requires : [ 'panel' ]
+});
+
+(function()
+{
+ var panels = {};
+ var isShowing = false;
+
+ function getPanel( editor, doc, parentElement, definition, level )
+ {
+ // Generates the panel key: docId-eleId-skinName-langDir[-uiColor][-CSSs][-level]
+ var key = CKEDITOR.tools.genKey( doc.getUniqueId(), parentElement.getUniqueId(), editor.skinName, editor.lang.dir,
+ editor.uiColor || '', definition.css || '', level || '' );
+
+ var panel = panels[ key ];
+
+ if ( !panel )
+ {
+ panel = panels[ key ] = new CKEDITOR.ui.panel( doc, definition );
+ panel.element = parentElement.append( CKEDITOR.dom.element.createFromHtml( panel.renderHtml( editor ), doc ) );
+
+ panel.element.setStyles(
+ {
+ display : 'none',
+ position : 'absolute'
+ });
+ }
+
+ return panel;
+ }
+
+ CKEDITOR.ui.floatPanel = CKEDITOR.tools.createClass(
+ {
+ $ : function( editor, parentElement, definition, level )
+ {
+ definition.forceIFrame = 1;
+
+ var doc = parentElement.getDocument(),
+ panel = getPanel( editor, doc, parentElement, definition, level || 0 ),
+ element = panel.element,
+ iframe = element.getFirst().getFirst();
+
+ this.element = element;
+
+ this._ =
+ {
+ // The panel that will be floating.
+ panel : panel,
+ parentElement : parentElement,
+ definition : definition,
+ document : doc,
+ iframe : iframe,
+ children : [],
+ dir : editor.lang.dir
+ };
+
+ editor.on( 'mode', function(){ this.hide(); }, this );
+ },
+
+ proto :
+ {
+ addBlock : function( name, block )
+ {
+ return this._.panel.addBlock( name, block );
+ },
+
+ addListBlock : function( name, multiSelect )
+ {
+ return this._.panel.addListBlock( name, multiSelect );
+ },
+
+ getBlock : function( name )
+ {
+ return this._.panel.getBlock( name );
+ },
+
+ /*
+ corner (LTR):
+ 1 = top-left
+ 2 = top-right
+ 3 = bottom-right
+ 4 = bottom-left
+
+ corner (RTL):
+ 1 = top-right
+ 2 = top-left
+ 3 = bottom-left
+ 4 = bottom-right
+ */
+ showBlock : function( name, offsetParent, corner, offsetX, offsetY )
+ {
+ var panel = this._.panel,
+ block = panel.showBlock( name );
+
+ this.allowBlur( false );
+ isShowing = 1;
+
+ var element = this.element,
+ iframe = this._.iframe,
+ definition = this._.definition,
+ position = offsetParent.getDocumentPosition( element.getDocument() ),
+ rtl = this._.dir == 'rtl';
+
+ var left = position.x + ( offsetX || 0 ),
+ top = position.y + ( offsetY || 0 );
+
+ // Floating panels are off by (-1px, 0px) in RTL mode. (#3438)
+ if ( rtl && ( corner == 1 || corner == 4 ) )
+ left += offsetParent.$.offsetWidth;
+ else if ( !rtl && ( corner == 2 || corner == 3 ) )
+ left += offsetParent.$.offsetWidth - 1;
+
+ if ( corner == 3 || corner == 4 )
+ top += offsetParent.$.offsetHeight - 1;
+
+ // Memorize offsetParent by it's ID.
+ this._.panel._.offsetParentId = offsetParent.getId();
+
+ element.setStyles(
+ {
+ top : top + 'px',
+ left: 0,
+ display : ''
+ });
+
+ // Don't use display or visibility style because we need to
+ // calculate the rendering layout later and focus the element.
+ element.setOpacity( 0 );
+
+ // To allow the context menu to decrease back their width
+ element.getFirst().removeStyle( 'width' );
+
+ // Configure the IFrame blur event. Do that only once.
+ if ( !this._.blurSet )
+ {
+ // Non IE prefer the event into a window object.
+ var focused = CKEDITOR.env.ie ? iframe : new CKEDITOR.dom.window( iframe.$.contentWindow );
+
+ // With addEventListener compatible browsers, we must
+ // useCapture when registering the focus/blur events to
+ // guarantee they will be firing in all situations. (#3068, #3222 )
+ CKEDITOR.event.useCapture = true;
+
+ focused.on( 'blur', function( ev )
+ {
+ if ( !this.allowBlur() )
+ return;
+
+ // As we are using capture to register the listener,
+ // the blur event may get fired even when focusing
+ // inside the window itself, so we must ensure the
+ // target is out of it.
+ var target;
+ if ( CKEDITOR.env.ie && !this.allowBlur()
+ || ( target = ev.data.getTarget() )
+ && target.getName && target.getName() != 'iframe' )
+ return;
+
+ if ( this.visible && !this._.activeChild && !isShowing )
+ this.hide();
+ },
+ this );
+
+ focused.on( 'focus', function()
+ {
+ this._.focused = true;
+ this.hideChild();
+ this.allowBlur( true );
+ },
+ this );
+
+ CKEDITOR.event.useCapture = false;
+
+ this._.blurSet = 1;
+ }
+
+ panel.onEscape = CKEDITOR.tools.bind( function( keystroke )
+ {
+ if ( this.onEscape && this.onEscape( keystroke ) === false )
+ return false;
+ },
+ this );
+
+ CKEDITOR.tools.setTimeout( function()
+ {
+ if ( rtl )
+ left -= element.$.offsetWidth;
+
+ var panelLoad = CKEDITOR.tools.bind( function ()
+ {
+ var target = element.getFirst();
+
+ if ( block.autoSize )
+ {
+ // We must adjust first the width or IE6 could include extra lines in the height computation
+ var widthNode = block.element.$;
+
+ if ( CKEDITOR.env.gecko || CKEDITOR.env.opera )
+ widthNode = widthNode.parentNode;
+
+ if ( CKEDITOR.env.ie )
+ widthNode = widthNode.document.body;
+
+ var width = widthNode.scrollWidth;
+ // Account for extra height needed due to IE quirks box model bug:
+ // http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
+ // (#3426)
+ if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && width > 0 )
+ width += ( target.$.offsetWidth || 0 ) - ( target.$.clientWidth || 0 );
+ // A little extra at the end.
+ // If not present, IE6 might break into the next line, but also it looks better this way
+ width += 4 ;
+
+ target.setStyle( 'width', width + 'px' );
+
+ // IE doesn't compute the scrollWidth if a filter is applied previously
+ block.element.addClass( 'cke_frameLoaded' );
+
+ var height = block.element.$.scrollHeight;
+
+ // Account for extra height needed due to IE quirks box model bug:
+ // http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
+ // (#3426)
+ if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && height > 0 )
+ height += ( target.$.offsetHeight || 0 ) - ( target.$.clientHeight || 0 );
+
+ target.setStyle( 'height', height + 'px' );
+
+ // Fix IE < 8 visibility.
+ panel._.currentBlock.element.setStyle( 'display', 'none' ).removeStyle( 'display' );
+ }
+ else
+ target.removeStyle( 'height' );
+
+ var panelElement = panel.element,
+ panelWindow = panelElement.getWindow(),
+ windowScroll = panelWindow.getScrollPosition(),
+ viewportSize = panelWindow.getViewPaneSize(),
+ panelSize =
+ {
+ 'height' : panelElement.$.offsetHeight,
+ 'width' : panelElement.$.offsetWidth
+ };
+
+ // If the menu is horizontal off, shift it toward
+ // the opposite language direction.
+ if ( rtl ? left < 0 : left + panelSize.width > viewportSize.width + windowScroll.x )
+ left += ( panelSize.width * ( rtl ? 1 : -1 ) );
+
+ // Vertical off screen is simpler.
+ if ( top + panelSize.height > viewportSize.height + windowScroll.y )
+ top -= panelSize.height;
+
+ // If IE is in RTL, we have troubles with absolute
+ // position and horizontal scrolls. Here we have a
+ // series of hacks to workaround it. (#6146)
+ if ( CKEDITOR.env.ie )
+ {
+ var offsetParent = new CKEDITOR.dom.element( element.$.offsetParent ),
+ scrollParent = offsetParent;
+
+ // Quirks returns , but standards returns .
+ if ( scrollParent.getName() == 'html' )
+ scrollParent = scrollParent.getDocument().getBody();
+
+ if ( scrollParent.getComputedStyle( 'direction' ) == 'rtl' )
+ {
+ // For IE8, there is not much logic on this, but it works.
+ if ( CKEDITOR.env.ie8Compat )
+ left -= element.getDocument().getDocumentElement().$.scrollLeft * 2;
+ else
+ left -= ( offsetParent.$.scrollWidth - offsetParent.$.clientWidth );
+ }
+ }
+
+ // Trigger the onHide event of the previously active panel to prevent
+ // incorrect styles from being applied (#6170)
+ var innerElement = element.getFirst(),
+ activePanel;
+ if ( ( activePanel = innerElement.getCustomData( 'activePanel' ) ) )
+ activePanel.onHide && activePanel.onHide.call( this, 1 );
+ innerElement.setCustomData( 'activePanel', this );
+
+ element.setStyles(
+ {
+ top : top + 'px',
+ left : left + 'px'
+ } );
+ element.setOpacity( 1 );
+ } , this );
+
+ panel.isLoaded ? panelLoad() : panel.onLoad = panelLoad;
+
+ // Set the panel frame focus, so the blur event gets fired.
+ CKEDITOR.tools.setTimeout( function()
+ {
+ iframe.$.contentWindow.focus();
+ // We need this get fired manually because of unfired focus() function.
+ this.allowBlur( true );
+ }, 0, this);
+ }, CKEDITOR.env.air ? 200 : 0, this);
+ this.visible = 1;
+
+ if ( this.onShow )
+ this.onShow.call( this );
+
+ isShowing = 0;
+ },
+
+ hide : function()
+ {
+ if ( this.visible && ( !this.onHide || this.onHide.call( this ) !== true ) )
+ {
+ this.hideChild();
+ this.element.setStyle( 'display', 'none' );
+ this.visible = 0;
+ this.element.getFirst().removeCustomData( 'activePanel' );
+ }
+ },
+
+ allowBlur : function( allow ) // Prevent editor from hiding the panel. #3222.
+ {
+ var panel = this._.panel;
+ if ( allow != undefined )
+ panel.allowBlur = allow;
+
+ return panel.allowBlur;
+ },
+
+ showAsChild : function( panel, blockName, offsetParent, corner, offsetX, offsetY )
+ {
+ // Skip reshowing of child which is already visible.
+ if ( this._.activeChild == panel && panel._.panel._.offsetParentId == offsetParent.getId() )
+ return;
+
+ this.hideChild();
+
+ panel.onHide = CKEDITOR.tools.bind( function()
+ {
+ // Use a timeout, so we give time for this menu to get
+ // potentially focused.
+ CKEDITOR.tools.setTimeout( function()
+ {
+ if ( !this._.focused )
+ this.hide();
+ },
+ 0, this );
+ },
+ this );
+
+ this._.activeChild = panel;
+ this._.focused = false;
+
+ panel.showBlock( blockName, offsetParent, corner, offsetX, offsetY );
+
+ /* #3767 IE: Second level menu may not have borders */
+ if ( CKEDITOR.env.ie7Compat || ( CKEDITOR.env.ie8 && CKEDITOR.env.ie6Compat ) )
+ {
+ setTimeout(function()
+ {
+ panel.element.getChild( 0 ).$.style.cssText += '';
+ }, 100);
+ }
+ },
+
+ hideChild : function()
+ {
+ var activeChild = this._.activeChild;
+
+ if ( activeChild )
+ {
+ delete activeChild.onHide;
+ delete this._.activeChild;
+ activeChild.hide();
+ }
+ }
+ }
+ });
+
+ CKEDITOR.on( 'instanceDestroyed', function()
+ {
+ var isLastInstance = CKEDITOR.tools.isEmpty( CKEDITOR.instances );
+
+ for ( var i in panels )
+ {
+ var panel = panels[ i ];
+ // Safe to destroy it since there're no more instances.(#4241)
+ if ( isLastInstance )
+ panel.destroy();
+ // Panel might be used by other instances, just hide them.(#4552)
+ else
+ panel.element.hide();
+ }
+ // Remove the registration.
+ isLastInstance && ( panels = {} );
+
+ } );
+})();
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/font/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/font/plugin.js
new file mode 100644
index 00000000..6a1efee6
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/font/plugin.js
@@ -0,0 +1,234 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+(function()
+{
+ function addCombo( editor, comboName, styleType, lang, entries, defaultLabel, styleDefinition )
+ {
+ var config = editor.config;
+
+ // Gets the list of fonts from the settings.
+ var names = entries.split( ';' ),
+ values = [];
+
+ // Create style objects for all fonts.
+ var styles = {};
+ for ( var i = 0 ; i < names.length ; i++ )
+ {
+ var parts = names[ i ];
+
+ if ( parts )
+ {
+ parts = parts.split( '/' );
+
+ var vars = {},
+ name = names[ i ] = parts[ 0 ];
+
+ vars[ styleType ] = values[ i ] = parts[ 1 ] || name;
+
+ styles[ name ] = new CKEDITOR.style( styleDefinition, vars );
+ styles[ name ]._.definition.name = name;
+ }
+ else
+ names.splice( i--, 1 );
+ }
+
+ editor.ui.addRichCombo( comboName,
+ {
+ label : lang.label,
+ title : lang.panelTitle,
+ className : 'cke_' + ( styleType == 'size' ? 'fontSize' : 'font' ),
+ panel :
+ {
+ css : editor.skin.editor.css.concat( config.contentsCss ),
+ multiSelect : false,
+ attributes : { 'aria-label' : lang.panelTitle }
+ },
+
+ init : function()
+ {
+ this.startGroup( lang.panelTitle );
+
+ for ( var i = 0 ; i < names.length ; i++ )
+ {
+ var name = names[ i ];
+
+ // Add the tag entry to the panel list.
+ this.add( name, styles[ name ].buildPreview(), name );
+ }
+ },
+
+ onClick : function( value )
+ {
+ editor.focus();
+ editor.fire( 'saveSnapshot' );
+
+ var style = styles[ value ];
+
+ if ( this.getValue() == value )
+ style.remove( editor.document );
+ else
+ style.apply( editor.document );
+
+ editor.fire( 'saveSnapshot' );
+ },
+
+ onRender : function()
+ {
+ editor.on( 'selectionChange', function( ev )
+ {
+ var currentValue = this.getValue();
+
+ var elementPath = ev.data.path,
+ elements = elementPath.elements;
+
+ // For each element into the elements path.
+ for ( var i = 0, element ; i < elements.length ; i++ )
+ {
+ element = elements[i];
+
+ // Check if the element is removable by any of
+ // the styles.
+ for ( var value in styles )
+ {
+ if ( styles[ value ].checkElementRemovable( element, true ) )
+ {
+ if ( value != currentValue )
+ this.setValue( value );
+ return;
+ }
+ }
+ }
+
+ // If no styles match, just empty it.
+ this.setValue( '', defaultLabel );
+ },
+ this);
+ }
+ });
+ }
+
+ CKEDITOR.plugins.add( 'font',
+ {
+ requires : [ 'richcombo', 'styles' ],
+
+ init : function( editor )
+ {
+ var config = editor.config;
+
+ addCombo( editor, 'Font', 'family', editor.lang.font, config.font_names, config.font_defaultLabel, config.font_style );
+ addCombo( editor, 'FontSize', 'size', editor.lang.fontSize, config.fontSize_sizes, config.fontSize_defaultLabel, config.fontSize_style );
+ }
+ });
+})();
+
+/**
+ * The list of fonts names to be displayed in the Font combo in the toolbar.
+ * Entries are separated by semi-colons (;), while it's possible to have more
+ * than one font for each entry, in the HTML way (separated by comma).
+ *
+ * A display name may be optionally defined by prefixing the entries with the
+ * name and the slash character. For example, "Arial/Arial, Helvetica, sans-serif"
+ * will be displayed as "Arial" in the list, but will be outputted as
+ * "Arial, Helvetica, sans-serif".
+ * @type String
+ * @example
+ * config.font_names =
+ * 'Arial/Arial, Helvetica, sans-serif;' +
+ * 'Times New Roman/Times New Roman, Times, serif;' +
+ * 'Verdana';
+ * @example
+ * config.font_names = 'Arial;Times New Roman;Verdana';
+ */
+CKEDITOR.config.font_names =
+ 'Arial/Arial, Helvetica, sans-serif;' +
+ 'Comic Sans MS/Comic Sans MS, cursive;' +
+ 'Courier New/Courier New, Courier, monospace;' +
+ 'Georgia/Georgia, serif;' +
+ 'Lucida Sans Unicode/Lucida Sans Unicode, Lucida Grande, sans-serif;' +
+ 'Tahoma/Tahoma, Geneva, sans-serif;' +
+ 'Times New Roman/Times New Roman, Times, serif;' +
+ 'Trebuchet MS/Trebuchet MS, Helvetica, sans-serif;' +
+ 'Verdana/Verdana, Geneva, sans-serif';
+
+/**
+ * The text to be displayed in the Font combo is none of the available values
+ * matches the current cursor position or text selection.
+ * @type String
+ * @example
+ * // If the default site font is Arial, we may making it more explicit to the end user.
+ * config.font_defaultLabel = 'Arial';
+ */
+CKEDITOR.config.font_defaultLabel = '';
+
+/**
+ * The style definition to be used to apply the font in the text.
+ * @type Object
+ * @example
+ * // This is actually the default value for it.
+ * config.font_style =
+ * {
+ * element : 'span',
+ * styles : { 'font-family' : '#(family)' },
+ * overrides : [ { element : 'font', attributes : { 'face' : null } } ]
+ * };
+ */
+CKEDITOR.config.font_style =
+ {
+ element : 'span',
+ styles : { 'font-family' : '#(family)' },
+ overrides : [ { element : 'font', attributes : { 'face' : null } } ]
+ };
+
+/**
+ * The list of fonts size to be displayed in the Font Size combo in the
+ * toolbar. Entries are separated by semi-colons (;).
+ *
+ * Any kind of "CSS like" size can be used, like "12px", "2.3em", "130%",
+ * "larger" or "x-small".
+ *
+ * A display name may be optionally defined by prefixing the entries with the
+ * name and the slash character. For example, "Bigger Font/14px" will be
+ * displayed as "Bigger Font" in the list, but will be outputted as "14px".
+ * @type String
+ * @default '8/8px;9/9px;10/10px;11/11px;12/12px;14/14px;16/16px;18/18px;20/20px;22/22px;24/24px;26/26px;28/28px;36/36px;48/48px;72/72px'
+ * @example
+ * config.fontSize_sizes = '16/16px;24/24px;48/48px;';
+ * @example
+ * config.fontSize_sizes = '12px;2.3em;130%;larger;x-small';
+ * @example
+ * config.fontSize_sizes = '12 Pixels/12px;Big/2.3em;30 Percent More/130%;Bigger/larger;Very Small/x-small';
+ */
+CKEDITOR.config.fontSize_sizes =
+ '8/8px;9/9px;10/10px;11/11px;12/12px;14/14px;16/16px;18/18px;20/20px;22/22px;24/24px;26/26px;28/28px;36/36px;48/48px;72/72px';
+
+/**
+ * The text to be displayed in the Font Size combo is none of the available
+ * values matches the current cursor position or text selection.
+ * @type String
+ * @example
+ * // If the default site font size is 12px, we may making it more explicit to the end user.
+ * config.fontSize_defaultLabel = '12px';
+ */
+CKEDITOR.config.fontSize_defaultLabel = '';
+
+/**
+ * The style definition to be used to apply the font size in the text.
+ * @type Object
+ * @example
+ * // This is actually the default value for it.
+ * config.fontSize_style =
+ * {
+ * element : 'span',
+ * styles : { 'font-size' : '#(size)' },
+ * overrides : [ { element : 'font', attributes : { 'size' : null } } ]
+ * };
+ */
+CKEDITOR.config.fontSize_style =
+ {
+ element : 'span',
+ styles : { 'font-size' : '#(size)' },
+ overrides : [ { element : 'font', attributes : { 'size' : null } } ]
+ };
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/format/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/format/plugin.js
new file mode 100644
index 00000000..6c4faf6f
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/format/plugin.js
@@ -0,0 +1,197 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.plugins.add( 'format',
+{
+ requires : [ 'richcombo', 'styles' ],
+
+ init : function( editor )
+ {
+ var config = editor.config,
+ lang = editor.lang.format;
+
+ // Gets the list of tags from the settings.
+ var tags = config.format_tags.split( ';' );
+
+ // Create style objects for all defined styles.
+ var styles = {};
+ for ( var i = 0 ; i < tags.length ; i++ )
+ {
+ var tag = tags[ i ];
+ styles[ tag ] = new CKEDITOR.style( config[ 'format_' + tag ] );
+ styles[ tag ]._.enterMode = editor.config.enterMode;
+ }
+
+ editor.ui.addRichCombo( 'Format',
+ {
+ label : lang.label,
+ title : lang.panelTitle,
+ className : 'cke_format',
+ panel :
+ {
+ css : editor.skin.editor.css.concat( config.contentsCss ),
+ multiSelect : false,
+ attributes : { 'aria-label' : lang.panelTitle }
+ },
+
+ init : function()
+ {
+ this.startGroup( lang.panelTitle );
+
+ for ( var tag in styles )
+ {
+ var label = lang[ 'tag_' + tag ];
+
+ // Add the tag entry to the panel list.
+ this.add( tag, '<' + tag + '>' + label + '' + tag + '>', label );
+ }
+ },
+
+ onClick : function( value )
+ {
+ editor.focus();
+ editor.fire( 'saveSnapshot' );
+
+ var style = styles[ value ],
+ elementPath = new CKEDITOR.dom.elementPath( editor.getSelection().getStartElement() );
+
+ style[ style.checkActive( elementPath ) ? 'remove' : 'apply' ]( editor.document );
+
+ // Save the undo snapshot after all changes are affected. (#4899)
+ setTimeout( function()
+ {
+ editor.fire( 'saveSnapshot' );
+ }, 0 );
+ },
+
+ onRender : function()
+ {
+ editor.on( 'selectionChange', function( ev )
+ {
+ var currentTag = this.getValue();
+
+ var elementPath = ev.data.path;
+
+ for ( var tag in styles )
+ {
+ if ( styles[ tag ].checkActive( elementPath ) )
+ {
+ if ( tag != currentTag )
+ this.setValue( tag, editor.lang.format[ 'tag_' + tag ] );
+ return;
+ }
+ }
+
+ // If no styles match, just empty it.
+ this.setValue( '' );
+ },
+ this);
+ }
+ });
+ }
+});
+
+/**
+ * A list of semi colon separated style names (by default tags) representing
+ * the style definition for each entry to be displayed in the Format combo in
+ * the toolbar. Each entry must have its relative definition configuration in a
+ * setting named "format_(tagName)". For example, the "p" entry has its
+ * definition taken from config.format_p.
+ * @type String
+ * @default 'p;h1;h2;h3;h4;h5;h6;pre;address;div'
+ * @example
+ * config.format_tags = 'p;h2;h3;pre'
+ */
+CKEDITOR.config.format_tags = 'p;h1;h2;h3;h4;h5;h6;pre;address;div';
+
+/**
+ * The style definition to be used to apply the "Normal" format.
+ * @type Object
+ * @default { element : 'p' }
+ * @example
+ * config.format_p = { element : 'p', attributes : { 'class' : 'normalPara' } };
+ */
+CKEDITOR.config.format_p = { element : 'p' };
+
+/**
+ * The style definition to be used to apply the "Normal (DIV)" format.
+ * @type Object
+ * @default { element : 'div' }
+ * @example
+ * config.format_div = { element : 'div', attributes : { 'class' : 'normalDiv' } };
+ */
+CKEDITOR.config.format_div = { element : 'div' };
+
+/**
+ * The style definition to be used to apply the "Formatted" format.
+ * @type Object
+ * @default { element : 'pre' }
+ * @example
+ * config.format_pre = { element : 'pre', attributes : { 'class' : 'code' } };
+ */
+CKEDITOR.config.format_pre = { element : 'pre' };
+
+/**
+ * The style definition to be used to apply the "Address" format.
+ * @type Object
+ * @default { element : 'address' }
+ * @example
+ * config.format_address = { element : 'address', attributes : { 'class' : 'styledAddress' } };
+ */
+CKEDITOR.config.format_address = { element : 'address' };
+
+/**
+ * The style definition to be used to apply the "Heading 1" format.
+ * @type Object
+ * @default { element : 'h1' }
+ * @example
+ * config.format_h1 = { element : 'h1', attributes : { 'class' : 'contentTitle1' } };
+ */
+CKEDITOR.config.format_h1 = { element : 'h1' };
+
+/**
+ * The style definition to be used to apply the "Heading 1" format.
+ * @type Object
+ * @default { element : 'h2' }
+ * @example
+ * config.format_h2 = { element : 'h2', attributes : { 'class' : 'contentTitle2' } };
+ */
+CKEDITOR.config.format_h2 = { element : 'h2' };
+
+/**
+ * The style definition to be used to apply the "Heading 1" format.
+ * @type Object
+ * @default { element : 'h3' }
+ * @example
+ * config.format_h3 = { element : 'h3', attributes : { 'class' : 'contentTitle3' } };
+ */
+CKEDITOR.config.format_h3 = { element : 'h3' };
+
+/**
+ * The style definition to be used to apply the "Heading 1" format.
+ * @type Object
+ * @default { element : 'h4' }
+ * @example
+ * config.format_h4 = { element : 'h4', attributes : { 'class' : 'contentTitle4' } };
+ */
+CKEDITOR.config.format_h4 = { element : 'h4' };
+
+/**
+ * The style definition to be used to apply the "Heading 1" format.
+ * @type Object
+ * @default { element : 'h5' }
+ * @example
+ * config.format_h5 = { element : 'h5', attributes : { 'class' : 'contentTitle5' } };
+ */
+CKEDITOR.config.format_h5 = { element : 'h5' };
+
+/**
+ * The style definition to be used to apply the "Heading 1" format.
+ * @type Object
+ * @default { element : 'h6' }
+ * @example
+ * config.format_h6 = { element : 'h6', attributes : { 'class' : 'contentTitle6' } };
+ */
+CKEDITOR.config.format_h6 = { element : 'h6' };
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/button.js b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/button.js
new file mode 100644
index 00000000..730864f6
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/button.js
@@ -0,0 +1,118 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.dialog.add( 'button', function( editor )
+{
+ function commitAttributes( element )
+ {
+ var val = this.getValue();
+ if ( val )
+ {
+ element.attributes[ this.id ] = val;
+ if ( this.id == 'name' )
+ element.attributes[ 'data-cke-saved-name' ] = val;
+ }
+ else
+ {
+ delete element.attributes[ this.id ];
+ if ( this.id == 'name' )
+ delete element.attributes[ 'data-cke-saved-name' ];
+ }
+ }
+
+ return {
+ title : editor.lang.button.title,
+ minWidth : 350,
+ minHeight : 150,
+ onShow : function()
+ {
+ delete this.button;
+ var element = this.getParentEditor().getSelection().getSelectedElement();
+ if ( element && element.is( 'input' ) )
+ {
+ var type = element.getAttribute( 'type' );
+ if ( type in { button:1, reset:1, submit:1 } )
+ {
+ this.button = element;
+ this.setupContent( element );
+ }
+ }
+ },
+ onOk : function()
+ {
+ var editor = this.getParentEditor(),
+ element = this.button,
+ isInsertMode = !element;
+
+ var fake = element ? CKEDITOR.htmlParser.fragment.fromHtml( element.getOuterHtml() ).children[ 0 ]
+ : new CKEDITOR.htmlParser.element( 'input' );
+ this.commitContent( fake );
+
+ var writer = new CKEDITOR.htmlParser.basicWriter();
+ fake.writeHtml( writer );
+ var newElement = CKEDITOR.dom.element.createFromHtml( writer.getHtml(), editor.document );
+
+ if ( isInsertMode )
+ editor.insertElement( newElement );
+ else
+ {
+ newElement.replace( element );
+ editor.getSelection().selectElement( newElement );
+ }
+ },
+ contents : [
+ {
+ id : 'info',
+ label : editor.lang.button.title,
+ title : editor.lang.button.title,
+ elements : [
+ {
+ id : 'name',
+ type : 'text',
+ label : editor.lang.common.name,
+ 'default' : '',
+ setup : function( element )
+ {
+ this.setValue(
+ element.data( 'cke-saved-name' ) ||
+ element.getAttribute( 'name' ) ||
+ '' );
+ },
+ commit : commitAttributes
+ },
+ {
+ id : 'value',
+ type : 'text',
+ label : editor.lang.button.text,
+ accessKey : 'V',
+ 'default' : '',
+ setup : function( element )
+ {
+ this.setValue( element.getAttribute( 'value' ) || '' );
+ },
+ commit : commitAttributes
+ },
+ {
+ id : 'type',
+ type : 'select',
+ label : editor.lang.button.type,
+ 'default' : 'button',
+ accessKey : 'T',
+ items :
+ [
+ [ editor.lang.button.typeBtn, 'button' ],
+ [ editor.lang.button.typeSbm, 'submit' ],
+ [ editor.lang.button.typeRst, 'reset' ]
+ ],
+ setup : function( element )
+ {
+ this.setValue( element.getAttribute( 'type' ) || '' );
+ },
+ commit : commitAttributes
+ }
+ ]
+ }
+ ]
+ };
+});
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/checkbox.js b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/checkbox.js
new file mode 100644
index 00000000..8c6e4d25
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/checkbox.js
@@ -0,0 +1,153 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.dialog.add( 'checkbox', function( editor )
+{
+ return {
+ title : editor.lang.checkboxAndRadio.checkboxTitle,
+ minWidth : 350,
+ minHeight : 140,
+ onShow : function()
+ {
+ delete this.checkbox;
+
+ var element = this.getParentEditor().getSelection().getSelectedElement();
+
+ if ( element && element.getAttribute( 'type' ) == 'checkbox' )
+ {
+ this.checkbox = element;
+ this.setupContent( element );
+ }
+ },
+ onOk : function()
+ {
+ var editor,
+ element = this.checkbox,
+ isInsertMode = !element;
+
+ if ( isInsertMode )
+ {
+ editor = this.getParentEditor();
+ element = editor.document.createElement( 'input' );
+ element.setAttribute( 'type', 'checkbox' );
+ editor.insertElement( element );
+ }
+ this.commitContent( { element : element } );
+ },
+ contents : [
+ {
+ id : 'info',
+ label : editor.lang.checkboxAndRadio.checkboxTitle,
+ title : editor.lang.checkboxAndRadio.checkboxTitle,
+ startupFocus : 'txtName',
+ elements : [
+ {
+ id : 'txtName',
+ type : 'text',
+ label : editor.lang.common.name,
+ 'default' : '',
+ accessKey : 'N',
+ setup : function( element )
+ {
+ this.setValue(
+ element.data( 'cke-saved-name' ) ||
+ element.getAttribute( 'name' ) ||
+ '' );
+ },
+ commit : function( data )
+ {
+ var element = data.element;
+
+ // IE failed to update 'name' property on input elements, protect it now.
+ if ( this.getValue() )
+ element.data( 'cke-saved-name', this.getValue() );
+ else
+ {
+ element.data( 'cke-saved-name', false );
+ element.removeAttribute( 'name' );
+ }
+ }
+ },
+ {
+ id : 'txtValue',
+ type : 'text',
+ label : editor.lang.checkboxAndRadio.value,
+ 'default' : '',
+ accessKey : 'V',
+ setup : function( element )
+ {
+ var value = element.getAttribute( 'value' );
+ // IE Return 'on' as default attr value.
+ this.setValue( CKEDITOR.env.ie && value == 'on' ? '' : value );
+ },
+ commit : function( data )
+ {
+ var element = data.element,
+ value = this.getValue();
+
+ if ( value && !( CKEDITOR.env.ie && value == 'on' ) )
+ element.setAttribute( 'value', value );
+ else
+ {
+ if ( CKEDITOR.env.ie )
+ {
+ // Remove attribute 'value' of checkbox (#4721).
+ var checkbox = new CKEDITOR.dom.element( 'input', element.getDocument() );
+ element.copyAttributes( checkbox, { value: 1 } );
+ checkbox.replace( element );
+ editor.getSelection().selectElement( checkbox );
+ data.element = checkbox;
+ }
+ else
+ element.removeAttribute( 'value' );
+ }
+ }
+ },
+ {
+ id : 'cmbSelected',
+ type : 'checkbox',
+ label : editor.lang.checkboxAndRadio.selected,
+ 'default' : '',
+ accessKey : 'S',
+ value : "checked",
+ setup : function( element )
+ {
+ this.setValue( element.getAttribute( 'checked' ) );
+ },
+ commit : function( data )
+ {
+ var element = data.element;
+
+ if ( CKEDITOR.env.ie )
+ {
+ var isElementChecked = !!element.getAttribute( 'checked' ),
+ isChecked = !!this.getValue();
+
+ if ( isElementChecked != isChecked )
+ {
+ var replace = CKEDITOR.dom.element.createFromHtml( ' ', editor.document );
+
+ element.copyAttributes( replace, { type : 1, checked : 1 } );
+ replace.replace( element );
+ editor.getSelection().selectElement( replace );
+ data.element = replace;
+ }
+ }
+ else
+ {
+ var value = this.getValue();
+ if ( value )
+ element.setAttribute( 'checked', 'checked' );
+ else
+ element.removeAttribute( 'checked' );
+ }
+ }
+ }
+ ]
+ }
+ ]
+ };
+});
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/form.js b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/form.js
new file mode 100644
index 00000000..975ebdd7
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/form.js
@@ -0,0 +1,177 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.dialog.add( 'form', function( editor )
+{
+ var autoAttributes =
+ {
+ action : 1,
+ id : 1,
+ method : 1,
+ enctype : 1,
+ target : 1
+ };
+
+ return {
+ title : editor.lang.form.title,
+ minWidth : 350,
+ minHeight : 200,
+ onShow : function()
+ {
+ delete this.form;
+
+ var element = this.getParentEditor().getSelection().getStartElement();
+ var form = element && element.getAscendant( 'form', true );
+ if ( form )
+ {
+ this.form = form;
+ this.setupContent( form );
+ }
+ },
+ onOk : function()
+ {
+ var editor,
+ element = this.form,
+ isInsertMode = !element;
+
+ if ( isInsertMode )
+ {
+ editor = this.getParentEditor();
+ element = editor.document.createElement( 'form' );
+ !CKEDITOR.env.ie && element.append( editor.document.createElement( 'br' ) );
+ }
+
+ if ( isInsertMode )
+ editor.insertElement( element );
+ this.commitContent( element );
+ },
+ onLoad : function()
+ {
+ function autoSetup( element )
+ {
+ this.setValue( element.getAttribute( this.id ) || '' );
+ }
+
+ function autoCommit( element )
+ {
+ if ( this.getValue() )
+ element.setAttribute( this.id, this.getValue() );
+ else
+ element.removeAttribute( this.id );
+ }
+
+ this.foreach( function( contentObj )
+ {
+ if ( autoAttributes[ contentObj.id ] )
+ {
+ contentObj.setup = autoSetup;
+ contentObj.commit = autoCommit;
+ }
+ } );
+ },
+ contents : [
+ {
+ id : 'info',
+ label : editor.lang.form.title,
+ title : editor.lang.form.title,
+ elements : [
+ {
+ id : 'txtName',
+ type : 'text',
+ label : editor.lang.common.name,
+ 'default' : '',
+ accessKey : 'N',
+ setup : function( element )
+ {
+ this.setValue( element.data( 'cke-saved-name' ) ||
+ element.getAttribute( 'name' ) ||
+ '' );
+ },
+ commit : function( element )
+ {
+ if ( this.getValue() )
+ element.data( 'cke-saved-name', this.getValue() );
+ else
+ {
+ element.data( 'cke-saved-name', false );
+ element.removeAttribute( 'name' );
+ }
+ }
+ },
+ {
+ id : 'action',
+ type : 'text',
+ label : editor.lang.form.action,
+ 'default' : '',
+ accessKey : 'T'
+ },
+ {
+ type : 'hbox',
+ widths : [ '45%', '55%' ],
+ children :
+ [
+ {
+ id : 'id',
+ type : 'text',
+ label : editor.lang.common.id,
+ 'default' : '',
+ accessKey : 'I'
+ },
+ {
+ id : 'enctype',
+ type : 'select',
+ label : editor.lang.form.encoding,
+ style : 'width:100%',
+ accessKey : 'E',
+ 'default' : '',
+ items :
+ [
+ [ '' ],
+ [ 'text/plain' ],
+ [ 'multipart/form-data' ],
+ [ 'application/x-www-form-urlencoded' ]
+ ]
+ }
+ ]
+ },
+ {
+ type : 'hbox',
+ widths : [ '45%', '55%' ],
+ children :
+ [
+ {
+ id : 'target',
+ type : 'select',
+ label : editor.lang.common.target,
+ style : 'width:100%',
+ accessKey : 'M',
+ 'default' : '',
+ items :
+ [
+ [ editor.lang.common.notSet, '' ],
+ [ editor.lang.common.targetNew, '_blank' ],
+ [ editor.lang.common.targetTop, '_top' ],
+ [ editor.lang.common.targetSelf, '_self' ],
+ [ editor.lang.common.targetParent, '_parent' ]
+ ]
+ },
+ {
+ id : 'method',
+ type : 'select',
+ label : editor.lang.form.method,
+ accessKey : 'M',
+ 'default' : 'GET',
+ items :
+ [
+ [ 'GET', 'get' ],
+ [ 'POST', 'post' ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ };
+});
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/hiddenfield.js b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/hiddenfield.js
new file mode 100644
index 00000000..ce63239b
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/hiddenfield.js
@@ -0,0 +1,100 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.dialog.add( 'hiddenfield', function( editor )
+{
+ return {
+ title : editor.lang.hidden.title,
+ hiddenField : null,
+ minWidth : 350,
+ minHeight : 110,
+ onShow : function()
+ {
+ delete this.hiddenField;
+
+ var editor = this.getParentEditor(),
+ selection = editor.getSelection(),
+ element = selection.getSelectedElement();
+
+ if ( element && element.data( 'cke-real-element-type' ) && element.data( 'cke-real-element-type' ) == 'hiddenfield' )
+ {
+ this.hiddenField = element;
+ element = editor.restoreRealElement( this.hiddenField );
+ this.setupContent( element );
+ selection.selectElement( this.hiddenField );
+ }
+ },
+ onOk : function()
+ {
+ var name = this.getValueOf( 'info', '_cke_saved_name' ),
+ value = this.getValueOf( 'info', 'value' ),
+ editor = this.getParentEditor(),
+ element = CKEDITOR.env.ie && !( CKEDITOR.document.$.documentMode >= 8 ) ?
+ editor.document.createElement( ' ' )
+ : editor.document.createElement( 'input' );
+
+ element.setAttribute( 'type', 'hidden' );
+ this.commitContent( element );
+ var fakeElement = editor.createFakeElement( element, 'cke_hidden', 'hiddenfield' );
+ if ( !this.hiddenField )
+ editor.insertElement( fakeElement );
+ else
+ {
+ fakeElement.replace( this.hiddenField );
+ editor.getSelection().selectElement( fakeElement );
+ }
+ return true;
+ },
+ contents : [
+ {
+ id : 'info',
+ label : editor.lang.hidden.title,
+ title : editor.lang.hidden.title,
+ elements : [
+ {
+ id : '_cke_saved_name',
+ type : 'text',
+ label : editor.lang.hidden.name,
+ 'default' : '',
+ accessKey : 'N',
+ setup : function( element )
+ {
+ this.setValue(
+ element.data( 'cke-saved-name' ) ||
+ element.getAttribute( 'name' ) ||
+ '' );
+ },
+ commit : function( element )
+ {
+ if ( this.getValue() )
+ element.setAttribute( 'name', this.getValue() );
+ else
+ {
+ element.removeAttribute( 'name' );
+ }
+ }
+ },
+ {
+ id : 'value',
+ type : 'text',
+ label : editor.lang.hidden.value,
+ 'default' : '',
+ accessKey : 'V',
+ setup : function( element )
+ {
+ this.setValue( element.getAttribute( 'value' ) || '' );
+ },
+ commit : function( element )
+ {
+ if ( this.getValue() )
+ element.setAttribute( 'value', this.getValue() );
+ else
+ element.removeAttribute( 'value' );
+ }
+ }
+ ]
+ }
+ ]
+ };
+});
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/radio.js b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/radio.js
new file mode 100644
index 00000000..5b154aaa
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/radio.js
@@ -0,0 +1,135 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.dialog.add( 'radio', function( editor )
+{
+ return {
+ title : editor.lang.checkboxAndRadio.radioTitle,
+ minWidth : 350,
+ minHeight : 140,
+ onShow : function()
+ {
+ delete this.radioButton;
+
+ var element = this.getParentEditor().getSelection().getSelectedElement();
+ if ( element && element.getName() == 'input' && element.getAttribute( 'type' ) == 'radio' )
+ {
+ this.radioButton = element;
+ this.setupContent( element );
+ }
+ },
+ onOk : function()
+ {
+ var editor,
+ element = this.radioButton,
+ isInsertMode = !element;
+
+ if ( isInsertMode )
+ {
+ editor = this.getParentEditor();
+ element = editor.document.createElement( 'input' );
+ element.setAttribute( 'type', 'radio' );
+ }
+
+ if ( isInsertMode )
+ editor.insertElement( element );
+ this.commitContent( { element : element } );
+ },
+ contents : [
+ {
+ id : 'info',
+ label : editor.lang.checkboxAndRadio.radioTitle,
+ title : editor.lang.checkboxAndRadio.radioTitle,
+ elements : [
+ {
+ id : 'name',
+ type : 'text',
+ label : editor.lang.common.name,
+ 'default' : '',
+ accessKey : 'N',
+ setup : function( element )
+ {
+ this.setValue(
+ element.data( 'cke-saved-name' ) ||
+ element.getAttribute( 'name' ) ||
+ '' );
+ },
+ commit : function( data )
+ {
+ var element = data.element;
+
+ if ( this.getValue() )
+ element.data( 'cke-saved-name', this.getValue() );
+ else
+ {
+ element.data( 'cke-saved-name', false );
+ element.removeAttribute( 'name' );
+ }
+ }
+ },
+ {
+ id : 'value',
+ type : 'text',
+ label : editor.lang.checkboxAndRadio.value,
+ 'default' : '',
+ accessKey : 'V',
+ setup : function( element )
+ {
+ this.setValue( element.getAttribute( 'value' ) || '' );
+ },
+ commit : function( data )
+ {
+ var element = data.element;
+
+ if ( this.getValue() )
+ element.setAttribute( 'value', this.getValue() );
+ else
+ element.removeAttribute( 'value' );
+ }
+ },
+ {
+ id : 'checked',
+ type : 'checkbox',
+ label : editor.lang.checkboxAndRadio.selected,
+ 'default' : '',
+ accessKey : 'S',
+ value : "checked",
+ setup : function( element )
+ {
+ this.setValue( element.getAttribute( 'checked' ) );
+ },
+ commit : function( data )
+ {
+ var element = data.element;
+
+ if ( !( CKEDITOR.env.ie || CKEDITOR.env.opera ) )
+ {
+ if ( this.getValue() )
+ element.setAttribute( 'checked', 'checked' );
+ else
+ element.removeAttribute( 'checked' );
+ }
+ else
+ {
+ var isElementChecked = element.getAttribute( 'checked' );
+ var isChecked = !!this.getValue();
+
+ if ( isElementChecked != isChecked )
+ {
+ var replace = CKEDITOR.dom.element.createFromHtml( ' ', editor.document );
+ element.copyAttributes( replace, { type : 1, checked : 1 } );
+ replace.replace( element );
+ editor.getSelection().selectElement( replace );
+ data.element = replace;
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ };
+});
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/select.js b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/select.js
new file mode 100644
index 00000000..fc66d0a8
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/select.js
@@ -0,0 +1,556 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.dialog.add( 'select', function( editor )
+{
+ // Add a new option to a SELECT object (combo or list).
+ function addOption( combo, optionText, optionValue, documentObject, index )
+ {
+ combo = getSelect( combo );
+ var oOption;
+ if ( documentObject )
+ oOption = documentObject.createElement( "OPTION" );
+ else
+ oOption = document.createElement( "OPTION" );
+
+ if ( combo && oOption && oOption.getName() == 'option' )
+ {
+ if ( CKEDITOR.env.ie ) {
+ if ( !isNaN( parseInt( index, 10) ) )
+ combo.$.options.add( oOption.$, index );
+ else
+ combo.$.options.add( oOption.$ );
+
+ oOption.$.innerHTML = optionText.length > 0 ? optionText : '';
+ oOption.$.value = optionValue;
+ }
+ else
+ {
+ if ( index !== null && index < combo.getChildCount() )
+ combo.getChild( index < 0 ? 0 : index ).insertBeforeMe( oOption );
+ else
+ combo.append( oOption );
+
+ oOption.setText( optionText.length > 0 ? optionText : '' );
+ oOption.setValue( optionValue );
+ }
+ }
+ else
+ return false;
+
+ return oOption;
+ }
+ // Remove all selected options from a SELECT object.
+ function removeSelectedOptions( combo )
+ {
+ combo = getSelect( combo );
+
+ // Save the selected index
+ var iSelectedIndex = getSelectedIndex( combo );
+
+ // Remove all selected options.
+ for ( var i = combo.getChildren().count() - 1 ; i >= 0 ; i-- )
+ {
+ if ( combo.getChild( i ).$.selected )
+ combo.getChild( i ).remove();
+ }
+
+ // Reset the selection based on the original selected index.
+ setSelectedIndex( combo, iSelectedIndex );
+ }
+ //Modify option from a SELECT object.
+ function modifyOption( combo, index, title, value )
+ {
+ combo = getSelect( combo );
+ if ( index < 0 )
+ return false;
+ var child = combo.getChild( index );
+ child.setText( title );
+ child.setValue( value );
+ return child;
+ }
+ function removeAllOptions( combo )
+ {
+ combo = getSelect( combo );
+ while ( combo.getChild( 0 ) && combo.getChild( 0 ).remove() )
+ { /*jsl:pass*/ }
+ }
+ // Moves the selected option by a number of steps (also negative).
+ function changeOptionPosition( combo, steps, documentObject )
+ {
+ combo = getSelect( combo );
+ var iActualIndex = getSelectedIndex( combo );
+ if ( iActualIndex < 0 )
+ return false;
+
+ var iFinalIndex = iActualIndex + steps;
+ iFinalIndex = ( iFinalIndex < 0 ) ? 0 : iFinalIndex;
+ iFinalIndex = ( iFinalIndex >= combo.getChildCount() ) ? combo.getChildCount() - 1 : iFinalIndex;
+
+ if ( iActualIndex == iFinalIndex )
+ return false;
+
+ var oOption = combo.getChild( iActualIndex ),
+ sText = oOption.getText(),
+ sValue = oOption.getValue();
+
+ oOption.remove();
+
+ oOption = addOption( combo, sText, sValue, ( !documentObject ) ? null : documentObject, iFinalIndex );
+ setSelectedIndex( combo, iFinalIndex );
+ return oOption;
+ }
+ function getSelectedIndex( combo )
+ {
+ combo = getSelect( combo );
+ return combo ? combo.$.selectedIndex : -1;
+ }
+ function setSelectedIndex( combo, index )
+ {
+ combo = getSelect( combo );
+ if ( index < 0 )
+ return null;
+ var count = combo.getChildren().count();
+ combo.$.selectedIndex = ( index >= count ) ? ( count - 1 ) : index;
+ return combo;
+ }
+ function getOptions( combo )
+ {
+ combo = getSelect( combo );
+ return combo ? combo.getChildren() : false;
+ }
+ function getSelect( obj )
+ {
+ if ( obj && obj.domId && obj.getInputElement().$ ) // Dialog element.
+ return obj.getInputElement();
+ else if ( obj && obj.$ )
+ return obj;
+ return false;
+ }
+
+ return {
+ title : editor.lang.select.title,
+ minWidth : CKEDITOR.env.ie ? 460 : 395,
+ minHeight : CKEDITOR.env.ie ? 320 : 300,
+ onShow : function()
+ {
+ delete this.selectBox;
+ this.setupContent( 'clear' );
+ var element = this.getParentEditor().getSelection().getSelectedElement();
+ if ( element && element.getName() == "select" )
+ {
+ this.selectBox = element;
+ this.setupContent( element.getName(), element );
+
+ // Load Options into dialog.
+ var objOptions = getOptions( element );
+ for ( var i = 0 ; i < objOptions.count() ; i++ )
+ this.setupContent( 'option', objOptions.getItem( i ) );
+ }
+ },
+ onOk : function()
+ {
+ var editor = this.getParentEditor(),
+ element = this.selectBox,
+ isInsertMode = !element;
+
+ if ( isInsertMode )
+ element = editor.document.createElement( 'select' );
+ this.commitContent( element );
+
+ if ( isInsertMode )
+ {
+ editor.insertElement( element );
+ if ( CKEDITOR.env.ie )
+ {
+ var sel = editor.getSelection(),
+ bms = sel.createBookmarks();
+ setTimeout(function()
+ {
+ sel.selectBookmarks( bms );
+ }, 0 );
+ }
+ }
+ },
+ contents : [
+ {
+ id : 'info',
+ label : editor.lang.select.selectInfo,
+ title : editor.lang.select.selectInfo,
+ accessKey : '',
+ elements : [
+ {
+ id : 'txtName',
+ type : 'text',
+ widths : [ '25%','75%' ],
+ labelLayout : 'horizontal',
+ label : editor.lang.common.name,
+ 'default' : '',
+ accessKey : 'N',
+ align : 'center',
+ style : 'width:350px',
+ setup : function( name, element )
+ {
+ if ( name == 'clear' )
+ this.setValue( this[ 'default' ] || '' );
+ else if ( name == 'select' )
+ {
+ this.setValue(
+ element.data( 'cke-saved-name' ) ||
+ element.getAttribute( 'name' ) ||
+ '' );
+ }
+ },
+ commit : function( element )
+ {
+ if ( this.getValue() )
+ element.data( 'cke-saved-name', this.getValue() );
+ else
+ {
+ element.data( 'cke-saved-name', false );
+ element.removeAttribute( 'name' );
+ }
+ }
+ },
+ {
+ id : 'txtValue',
+ type : 'text',
+ widths : [ '25%','75%' ],
+ labelLayout : 'horizontal',
+ label : editor.lang.select.value,
+ style : 'width:350px',
+ 'default' : '',
+ className : 'cke_disabled',
+ onLoad : function()
+ {
+ this.getInputElement().setAttribute( 'readOnly', true );
+ },
+ setup : function( name, element )
+ {
+ if ( name == 'clear' )
+ this.setValue( '' );
+ else if ( name == 'option' && element.getAttribute( 'selected' ) )
+ this.setValue( element.$.value );
+ }
+ },
+ {
+ type : 'hbox',
+ widths : [ '175px', '170px' ],
+ align : 'center',
+ children :
+ [
+ {
+ id : 'txtSize',
+ type : 'text',
+ align : 'center',
+ labelLayout : 'horizontal',
+ label : editor.lang.select.size,
+ 'default' : '',
+ accessKey : 'S',
+ style : 'width:175px',
+ validate: function()
+ {
+ var func = CKEDITOR.dialog.validate.integer( editor.lang.common.validateNumberFailed );
+ return ( ( this.getValue() === '' ) || func.apply( this ) );
+ },
+ setup : function( name, element )
+ {
+ if ( name == 'select' )
+ this.setValue( element.getAttribute( 'size' ) || '' );
+ if ( CKEDITOR.env.webkit )
+ this.getInputElement().setStyle( 'width', '86px' );
+ },
+ commit : function( element )
+ {
+ if ( this.getValue() )
+ element.setAttribute( 'size', this.getValue() );
+ else
+ element.removeAttribute( 'size' );
+ }
+ },
+ {
+ type : 'html',
+ html : '' + CKEDITOR.tools.htmlEncode( editor.lang.select.lines ) + ' '
+ }
+ ]
+ },
+ {
+ type : 'html',
+ html : '' + CKEDITOR.tools.htmlEncode( editor.lang.select.opAvail ) + ' '
+ },
+ {
+ type : 'hbox',
+ widths : [ '115px', '115px' ,'100px' ],
+ align : 'top',
+ children :
+ [
+ {
+ type : 'vbox',
+ children :
+ [
+ {
+ id : 'txtOptName',
+ type : 'text',
+ label : editor.lang.select.opText,
+ style : 'width:115px',
+ setup : function( name, element )
+ {
+ if ( name == 'clear' )
+ this.setValue( "" );
+ }
+ },
+ {
+ type : 'select',
+ id : 'cmbName',
+ label : '',
+ title : '',
+ size : 5,
+ style : 'width:115px;height:75px',
+ items : [],
+ onChange : function()
+ {
+ var dialog = this.getDialog(),
+ values = dialog.getContentElement( 'info', 'cmbValue' ),
+ optName = dialog.getContentElement( 'info', 'txtOptName' ),
+ optValue = dialog.getContentElement( 'info', 'txtOptValue' ),
+ iIndex = getSelectedIndex( this );
+
+ setSelectedIndex( values, iIndex );
+ optName.setValue( this.getValue() );
+ optValue.setValue( values.getValue() );
+ },
+ setup : function( name, element )
+ {
+ if ( name == 'clear' )
+ removeAllOptions( this );
+ else if ( name == 'option' )
+ addOption( this, element.getText(), element.getText(),
+ this.getDialog().getParentEditor().document );
+ },
+ commit : function( element )
+ {
+ var dialog = this.getDialog(),
+ optionsNames = getOptions( this ),
+ optionsValues = getOptions( dialog.getContentElement( 'info', 'cmbValue' ) ),
+ selectValue = dialog.getContentElement( 'info', 'txtValue' ).getValue();
+
+ removeAllOptions( element );
+
+ for ( var i = 0 ; i < optionsNames.count() ; i++ )
+ {
+ var oOption = addOption( element, optionsNames.getItem( i ).getValue(),
+ optionsValues.getItem( i ).getValue(), dialog.getParentEditor().document );
+ if ( optionsValues.getItem( i ).getValue() == selectValue )
+ {
+ oOption.setAttribute( 'selected', 'selected' );
+ oOption.selected = true;
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ type : 'vbox',
+ children :
+ [
+ {
+ id : 'txtOptValue',
+ type : 'text',
+ label : editor.lang.select.opValue,
+ style : 'width:115px',
+ setup : function( name, element )
+ {
+ if ( name == 'clear' )
+ this.setValue( "" );
+ }
+ },
+ {
+ type : 'select',
+ id : 'cmbValue',
+ label : '',
+ size : 5,
+ style : 'width:115px;height:75px',
+ items : [],
+ onChange : function()
+ {
+ var dialog = this.getDialog(),
+ names = dialog.getContentElement( 'info', 'cmbName' ),
+ optName = dialog.getContentElement( 'info', 'txtOptName' ),
+ optValue = dialog.getContentElement( 'info', 'txtOptValue' ),
+ iIndex = getSelectedIndex( this );
+
+ setSelectedIndex( names, iIndex );
+ optName.setValue( names.getValue() );
+ optValue.setValue( this.getValue() );
+ },
+ setup : function( name, element )
+ {
+ if ( name == 'clear' )
+ removeAllOptions( this );
+ else if ( name == 'option' )
+ {
+ var oValue = element.getValue();
+ addOption( this, oValue, oValue,
+ this.getDialog().getParentEditor().document );
+ if ( element.getAttribute( 'selected' ) == 'selected' )
+ this.getDialog().getContentElement( 'info', 'txtValue' ).setValue( oValue );
+ }
+ }
+ }
+ ]
+ },
+ {
+ type : 'vbox',
+ padding : 5,
+ children :
+ [
+ {
+ type : 'button',
+ style : '',
+ label : editor.lang.select.btnAdd,
+ title : editor.lang.select.btnAdd,
+ style : 'width:100%;',
+ onClick : function()
+ {
+ //Add new option.
+ var dialog = this.getDialog(),
+ parentEditor = dialog.getParentEditor(),
+ optName = dialog.getContentElement( 'info', 'txtOptName' ),
+ optValue = dialog.getContentElement( 'info', 'txtOptValue' ),
+ names = dialog.getContentElement( 'info', 'cmbName' ),
+ values = dialog.getContentElement( 'info', 'cmbValue' );
+
+ addOption(names, optName.getValue(), optName.getValue(), dialog.getParentEditor().document );
+ addOption(values, optValue.getValue(), optValue.getValue(), dialog.getParentEditor().document );
+
+ optName.setValue( "" );
+ optValue.setValue( "" );
+ }
+ },
+ {
+ type : 'button',
+ label : editor.lang.select.btnModify,
+ title : editor.lang.select.btnModify,
+ style : 'width:100%;',
+ onClick : function()
+ {
+ //Modify selected option.
+ var dialog = this.getDialog(),
+ optName = dialog.getContentElement( 'info', 'txtOptName' ),
+ optValue = dialog.getContentElement( 'info', 'txtOptValue' ),
+ names = dialog.getContentElement( 'info', 'cmbName' ),
+ values = dialog.getContentElement( 'info', 'cmbValue' ),
+ iIndex = getSelectedIndex( names );
+
+ if ( iIndex >= 0 )
+ {
+ modifyOption( names, iIndex, optName.getValue(), optName.getValue() );
+ modifyOption( values, iIndex, optValue.getValue(), optValue.getValue() );
+ }
+ }
+ },
+ {
+ type : 'button',
+ style : 'width:100%;',
+ label : editor.lang.select.btnUp,
+ title : editor.lang.select.btnUp,
+ onClick : function()
+ {
+ //Move up.
+ var dialog = this.getDialog(),
+ names = dialog.getContentElement( 'info', 'cmbName' ),
+ values = dialog.getContentElement( 'info', 'cmbValue' );
+
+ changeOptionPosition( names, -1, dialog.getParentEditor().document );
+ changeOptionPosition( values, -1, dialog.getParentEditor().document );
+ }
+ },
+ {
+ type : 'button',
+ style : 'width:100%;',
+ label : editor.lang.select.btnDown,
+ title : editor.lang.select.btnDown,
+ onClick : function()
+ {
+ //Move down.
+ var dialog = this.getDialog(),
+ names = dialog.getContentElement( 'info', 'cmbName' ),
+ values = dialog.getContentElement( 'info', 'cmbValue' );
+
+ changeOptionPosition( names, 1, dialog.getParentEditor().document );
+ changeOptionPosition( values, 1, dialog.getParentEditor().document );
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ type : 'hbox',
+ widths : [ '40%', '20%', '40%' ],
+ children :
+ [
+ {
+ type : 'button',
+ label : editor.lang.select.btnSetValue,
+ title : editor.lang.select.btnSetValue,
+ onClick : function()
+ {
+ //Set as default value.
+ var dialog = this.getDialog(),
+ values = dialog.getContentElement( 'info', 'cmbValue' ),
+ txtValue = dialog.getContentElement( 'info', 'txtValue' );
+ txtValue.setValue( values.getValue() );
+ }
+ },
+ {
+ type : 'button',
+ label : editor.lang.select.btnDelete,
+ title : editor.lang.select.btnDelete,
+ onClick : function()
+ {
+ // Delete option.
+ var dialog = this.getDialog(),
+ names = dialog.getContentElement( 'info', 'cmbName' ),
+ values = dialog.getContentElement( 'info', 'cmbValue' ),
+ optName = dialog.getContentElement( 'info', 'txtOptName' ),
+ optValue = dialog.getContentElement( 'info', 'txtOptValue' );
+
+ removeSelectedOptions( names );
+ removeSelectedOptions( values );
+
+ optName.setValue( "" );
+ optValue.setValue( "" );
+ }
+ },
+ {
+ id : 'chkMulti',
+ type : 'checkbox',
+ label : editor.lang.select.chkMulti,
+ 'default' : '',
+ accessKey : 'M',
+ value : "checked",
+ setup : function( name, element )
+ {
+ if ( name == 'select' )
+ this.setValue( element.getAttribute( 'multiple' ) );
+ if ( CKEDITOR.env.webkit )
+ this.getElement().getParent().setStyle( 'vertical-align', 'middle' );
+ },
+ commit : function( element )
+ {
+ if ( this.getValue() )
+ element.setAttribute( 'multiple', this.getValue() );
+ else
+ element.removeAttribute( 'multiple' );
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ };
+});
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/textarea.js b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/textarea.js
new file mode 100644
index 00000000..df29235f
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/textarea.js
@@ -0,0 +1,114 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.dialog.add( 'textarea', function( editor )
+{
+ return {
+ title : editor.lang.textarea.title,
+ minWidth : 350,
+ minHeight : 150,
+ onShow : function()
+ {
+ delete this.textarea;
+
+ var element = this.getParentEditor().getSelection().getSelectedElement();
+ if ( element && element.getName() == "textarea" )
+ {
+ this.textarea = element;
+ this.setupContent( element );
+ }
+ },
+ onOk : function()
+ {
+ var editor,
+ element = this.textarea,
+ isInsertMode = !element;
+
+ if ( isInsertMode )
+ {
+ editor = this.getParentEditor();
+ element = editor.document.createElement( 'textarea' );
+ }
+ this.commitContent( element );
+
+ if ( isInsertMode )
+ editor.insertElement( element );
+ },
+ contents : [
+ {
+ id : 'info',
+ label : editor.lang.textarea.title,
+ title : editor.lang.textarea.title,
+ elements : [
+ {
+ id : '_cke_saved_name',
+ type : 'text',
+ label : editor.lang.common.name,
+ 'default' : '',
+ accessKey : 'N',
+ setup : function( element )
+ {
+ this.setValue(
+ element.data( 'cke-saved-name' ) ||
+ element.getAttribute( 'name' ) ||
+ '' );
+ },
+ commit : function( element )
+ {
+ if ( this.getValue() )
+ element.data( 'cke-saved-name', this.getValue() );
+ else
+ {
+ element.data( 'cke-saved-name', false );
+ element.removeAttribute( 'name' );
+ }
+ }
+ },
+ {
+ id : 'cols',
+ type : 'text',
+ label : editor.lang.textarea.cols,
+ 'default' : '',
+ accessKey : 'C',
+ style : 'width:50px',
+ validate : CKEDITOR.dialog.validate.integer( editor.lang.common.validateNumberFailed ),
+ setup : function( element )
+ {
+ var value = element.hasAttribute( 'cols' ) && element.getAttribute( 'cols' );
+ this.setValue( value || '' );
+ },
+ commit : function( element )
+ {
+ if ( this.getValue() )
+ element.setAttribute( 'cols', this.getValue() );
+ else
+ element.removeAttribute( 'cols' );
+ }
+ },
+ {
+ id : 'rows',
+ type : 'text',
+ label : editor.lang.textarea.rows,
+ 'default' : '',
+ accessKey : 'R',
+ style : 'width:50px',
+ validate : CKEDITOR.dialog.validate.integer( editor.lang.common.validateNumberFailed ),
+ setup : function( element )
+ {
+ var value = element.hasAttribute( 'rows' ) && element.getAttribute( 'rows' );
+ this.setValue( value || '' );
+ },
+ commit : function( element )
+ {
+ if ( this.getValue() )
+ element.setAttribute( 'rows', this.getValue() );
+ else
+ element.removeAttribute( 'rows' );
+ }
+ }
+ ]
+ }
+ ]
+ };
+});
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/textfield.js b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/textfield.js
new file mode 100644
index 00000000..cc250de0
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/forms/dialogs/textfield.js
@@ -0,0 +1,199 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.dialog.add( 'textfield', function( editor )
+{
+ var autoAttributes =
+ {
+ value : 1,
+ size : 1,
+ maxLength : 1
+ };
+
+ var acceptedTypes =
+ {
+ text : 1,
+ password : 1
+ };
+
+ return {
+ title : editor.lang.textfield.title,
+ minWidth : 350,
+ minHeight : 150,
+ onShow : function()
+ {
+ delete this.textField;
+
+ var element = this.getParentEditor().getSelection().getSelectedElement();
+ if ( element && element.getName() == "input" &&
+ ( acceptedTypes[ element.getAttribute( 'type' ) ] || !element.getAttribute( 'type' ) ) )
+ {
+ this.textField = element;
+ this.setupContent( element );
+ }
+ },
+ onOk : function()
+ {
+ var editor,
+ element = this.textField,
+ isInsertMode = !element;
+
+ if ( isInsertMode )
+ {
+ editor = this.getParentEditor();
+ element = editor.document.createElement( 'input' );
+ element.setAttribute( 'type', 'text' );
+ }
+
+ if ( isInsertMode )
+ editor.insertElement( element );
+ this.commitContent( { element : element } );
+ },
+ onLoad : function()
+ {
+ var autoSetup = function( element )
+ {
+ var value = element.hasAttribute( this.id ) && element.getAttribute( this.id );
+ this.setValue( value || '' );
+ };
+
+ var autoCommit = function( data )
+ {
+ var element = data.element;
+ var value = this.getValue();
+
+ if ( value )
+ element.setAttribute( this.id, value );
+ else
+ element.removeAttribute( this.id );
+ };
+
+ this.foreach( function( contentObj )
+ {
+ if ( autoAttributes[ contentObj.id ] )
+ {
+ contentObj.setup = autoSetup;
+ contentObj.commit = autoCommit;
+ }
+ } );
+ },
+ contents : [
+ {
+ id : 'info',
+ label : editor.lang.textfield.title,
+ title : editor.lang.textfield.title,
+ elements : [
+ {
+ type : 'hbox',
+ widths : [ '50%', '50%' ],
+ children :
+ [
+ {
+ id : '_cke_saved_name',
+ type : 'text',
+ label : editor.lang.textfield.name,
+ 'default' : '',
+ accessKey : 'N',
+ setup : function( element )
+ {
+ this.setValue(
+ element.data( 'cke-saved-name' ) ||
+ element.getAttribute( 'name' ) ||
+ '' );
+ },
+ commit : function( data )
+ {
+ var element = data.element;
+
+ if ( this.getValue() )
+ element.data( 'cke-saved-name', this.getValue() );
+ else
+ {
+ element.data( 'cke-saved-name', false );
+ element.removeAttribute( 'name' );
+ }
+ }
+ },
+ {
+ id : 'value',
+ type : 'text',
+ label : editor.lang.textfield.value,
+ 'default' : '',
+ accessKey : 'V'
+ }
+ ]
+ },
+ {
+ type : 'hbox',
+ widths : [ '50%', '50%' ],
+ children :
+ [
+ {
+ id : 'size',
+ type : 'text',
+ label : editor.lang.textfield.charWidth,
+ 'default' : '',
+ accessKey : 'C',
+ style : 'width:50px',
+ validate : CKEDITOR.dialog.validate.integer( editor.lang.common.validateNumberFailed )
+ },
+ {
+ id : 'maxLength',
+ type : 'text',
+ label : editor.lang.textfield.maxChars,
+ 'default' : '',
+ accessKey : 'M',
+ style : 'width:50px',
+ validate : CKEDITOR.dialog.validate.integer( editor.lang.common.validateNumberFailed )
+ }
+ ],
+ onLoad : function()
+ {
+ // Repaint the style for IE7 (#6068)
+ if ( CKEDITOR.env.ie7Compat )
+ this.getElement().setStyle( 'zoom', '100%' );
+ }
+ },
+ {
+ id : 'type',
+ type : 'select',
+ label : editor.lang.textfield.type,
+ 'default' : 'text',
+ accessKey : 'M',
+ items :
+ [
+ [ editor.lang.textfield.typeText, 'text' ],
+ [ editor.lang.textfield.typePass, 'password' ]
+ ],
+ setup : function( element )
+ {
+ this.setValue( element.getAttribute( 'type' ) );
+ },
+ commit : function( data )
+ {
+ var element = data.element;
+
+ if ( CKEDITOR.env.ie )
+ {
+ var elementType = element.getAttribute( 'type' );
+ var myType = this.getValue();
+
+ if ( elementType != myType )
+ {
+ var replace = CKEDITOR.dom.element.createFromHtml( ' ', editor.document );
+ element.copyAttributes( replace, { type : 1 } );
+ replace.replace( element );
+ editor.getSelection().selectElement( replace );
+ data.element = replace;
+ }
+ }
+ else
+ element.setAttribute( 'type', this.getValue() );
+ }
+ }
+ ]
+ }
+ ]
+ };
+});
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/forms/images/hiddenfield.gif b/app/assets/javascripts/ckeditor/_source/plugins/forms/images/hiddenfield.gif
new file mode 100644
index 00000000..953f643b
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/forms/images/hiddenfield.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/forms/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/forms/plugin.js
new file mode 100644
index 00000000..3be5c3b6
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/forms/plugin.js
@@ -0,0 +1,284 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+/**
+ * @file Forms Plugin
+ */
+
+CKEDITOR.plugins.add( 'forms',
+{
+ init : function( editor )
+ {
+ var lang = editor.lang;
+
+ editor.addCss(
+ 'form' +
+ '{' +
+ 'border: 1px dotted #FF0000;' +
+ 'padding: 2px;' +
+ '}\n' );
+
+ editor.addCss(
+ 'img.cke_hidden' +
+ '{' +
+ 'background-image: url(' + CKEDITOR.getUrl( this.path + 'images/hiddenfield.gif' ) + ');' +
+ 'background-position: center center;' +
+ 'background-repeat: no-repeat;' +
+ 'border: 1px solid #a9a9a9;' +
+ 'width: 16px !important;' +
+ 'height: 16px !important;' +
+ '}' );
+
+ // All buttons use the same code to register. So, to avoid
+ // duplications, let's use this tool function.
+ var addButtonCommand = function( buttonName, commandName, dialogFile )
+ {
+ editor.addCommand( commandName, new CKEDITOR.dialogCommand( commandName ) );
+
+ editor.ui.addButton( buttonName,
+ {
+ label : lang.common[ buttonName.charAt(0).toLowerCase() + buttonName.slice(1) ],
+ command : commandName
+ });
+ CKEDITOR.dialog.add( commandName, dialogFile );
+ };
+
+ var dialogPath = this.path + 'dialogs/';
+ addButtonCommand( 'Form', 'form', dialogPath + 'form.js' );
+ addButtonCommand( 'Checkbox', 'checkbox', dialogPath + 'checkbox.js' );
+ addButtonCommand( 'Radio', 'radio', dialogPath + 'radio.js' );
+ addButtonCommand( 'TextField', 'textfield', dialogPath + 'textfield.js' );
+ addButtonCommand( 'Textarea', 'textarea', dialogPath + 'textarea.js' );
+ addButtonCommand( 'Select', 'select', dialogPath + 'select.js' );
+ addButtonCommand( 'Button', 'button', dialogPath + 'button.js' );
+ addButtonCommand( 'ImageButton', 'imagebutton', CKEDITOR.plugins.getPath('image') + 'dialogs/image.js' );
+ addButtonCommand( 'HiddenField', 'hiddenfield', dialogPath + 'hiddenfield.js' );
+
+ // If the "menu" plugin is loaded, register the menu items.
+ if ( editor.addMenuItems )
+ {
+ editor.addMenuItems(
+ {
+ form :
+ {
+ label : lang.form.menu,
+ command : 'form',
+ group : 'form'
+ },
+
+ checkbox :
+ {
+ label : lang.checkboxAndRadio.checkboxTitle,
+ command : 'checkbox',
+ group : 'checkbox'
+ },
+
+ radio :
+ {
+ label : lang.checkboxAndRadio.radioTitle,
+ command : 'radio',
+ group : 'radio'
+ },
+
+ textfield :
+ {
+ label : lang.textfield.title,
+ command : 'textfield',
+ group : 'textfield'
+ },
+
+ hiddenfield :
+ {
+ label : lang.hidden.title,
+ command : 'hiddenfield',
+ group : 'hiddenfield'
+ },
+
+ imagebutton :
+ {
+ label : lang.image.titleButton,
+ command : 'imagebutton',
+ group : 'imagebutton'
+ },
+
+ button :
+ {
+ label : lang.button.title,
+ command : 'button',
+ group : 'button'
+ },
+
+ select :
+ {
+ label : lang.select.title,
+ command : 'select',
+ group : 'select'
+ },
+
+ textarea :
+ {
+ label : lang.textarea.title,
+ command : 'textarea',
+ group : 'textarea'
+ }
+ });
+ }
+
+ // If the "contextmenu" plugin is loaded, register the listeners.
+ if ( editor.contextMenu )
+ {
+ editor.contextMenu.addListener( function( element )
+ {
+ if ( element && element.hasAscendant( 'form', true ) && !element.isReadOnly() )
+ return { form : CKEDITOR.TRISTATE_OFF };
+ });
+
+ editor.contextMenu.addListener( function( element )
+ {
+ if ( element && !element.isReadOnly() )
+ {
+ var name = element.getName();
+
+ if ( name == 'select' )
+ return { select : CKEDITOR.TRISTATE_OFF };
+
+ if ( name == 'textarea' )
+ return { textarea : CKEDITOR.TRISTATE_OFF };
+
+ if ( name == 'input' )
+ {
+ switch( element.getAttribute( 'type' ) )
+ {
+ case 'button' :
+ case 'submit' :
+ case 'reset' :
+ return { button : CKEDITOR.TRISTATE_OFF };
+
+ case 'checkbox' :
+ return { checkbox : CKEDITOR.TRISTATE_OFF };
+
+ case 'radio' :
+ return { radio : CKEDITOR.TRISTATE_OFF };
+
+ case 'image' :
+ return { imagebutton : CKEDITOR.TRISTATE_OFF };
+
+ default :
+ return { textfield : CKEDITOR.TRISTATE_OFF };
+ }
+ }
+
+ if ( name == 'img' && element.data( 'cke-real-element-type' ) == 'hiddenfield' )
+ return { hiddenfield : CKEDITOR.TRISTATE_OFF };
+ }
+ });
+ }
+
+ editor.on( 'doubleclick', function( evt )
+ {
+ var element = evt.data.element;
+
+ if ( element.is( 'form' ) )
+ evt.data.dialog = 'form';
+ else if ( element.is( 'select' ) )
+ evt.data.dialog = 'select';
+ else if ( element.is( 'textarea' ) )
+ evt.data.dialog = 'textarea';
+ else if ( element.is( 'img' ) && element.data( 'cke-real-element-type' ) == 'hiddenfield' )
+ evt.data.dialog = 'hiddenfield';
+ else if ( element.is( 'input' ) )
+ {
+ switch ( element.getAttribute( 'type' ) )
+ {
+ case 'button' :
+ case 'submit' :
+ case 'reset' :
+ evt.data.dialog = 'button';
+ break;
+ case 'checkbox' :
+ evt.data.dialog = 'checkbox';
+ break;
+ case 'radio' :
+ evt.data.dialog = 'radio';
+ break;
+ case 'image' :
+ evt.data.dialog = 'imagebutton';
+ break;
+ default :
+ evt.data.dialog = 'textfield';
+ break;
+ }
+ }
+ });
+ },
+
+ afterInit : function( editor )
+ {
+ var dataProcessor = editor.dataProcessor,
+ htmlFilter = dataProcessor && dataProcessor.htmlFilter,
+ dataFilter = dataProcessor && dataProcessor.dataFilter;
+
+ // Cleanup certain IE form elements default values.
+ if ( CKEDITOR.env.ie )
+ {
+ htmlFilter && htmlFilter.addRules(
+ {
+ elements :
+ {
+ input : function( input )
+ {
+ var attrs = input.attributes,
+ type = attrs.type;
+ // Old IEs don't provide type for Text inputs #5522
+ if ( !type )
+ attrs.type = 'text';
+ if ( type == 'checkbox' || type == 'radio' )
+ attrs.value == 'on' && delete attrs.value;
+ }
+ }
+ } );
+ }
+
+ if ( dataFilter )
+ {
+ dataFilter.addRules(
+ {
+ elements :
+ {
+ input : function( element )
+ {
+ if ( element.attributes.type == 'hidden' )
+ return editor.createFakeParserElement( element, 'cke_hidden', 'hiddenfield' );
+ }
+ }
+ } );
+ }
+ },
+ requires : [ 'image', 'fakeobjects' ]
+} );
+
+if ( CKEDITOR.env.ie )
+{
+ CKEDITOR.dom.element.prototype.hasAttribute = function( name )
+ {
+ var $attr = this.$.attributes.getNamedItem( name );
+
+ if ( this.getName() == 'input' )
+ {
+ switch ( name )
+ {
+ case 'class' :
+ return this.$.className.length > 0;
+ case 'checked' :
+ return !!this.$.checked;
+ case 'value' :
+ var type = this.getAttribute( 'type' );
+ return type == 'checkbox' || type == 'radio' ? this.$.value != 'on' : this.$.value;
+ }
+ }
+
+ return !!( $attr && $attr.specified );
+ };
+}
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/horizontalrule/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/horizontalrule/plugin.js
new file mode 100644
index 00000000..d4ec65a6
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/horizontalrule/plugin.js
@@ -0,0 +1,36 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+/**
+ * @file Horizontal Rule plugin.
+ */
+
+(function()
+{
+ var horizontalruleCmd =
+ {
+ canUndo : false, // The undo snapshot will be handled by 'insertElement'.
+ exec : function( editor )
+ {
+ editor.insertElement( editor.document.createElement( 'hr' ) );
+ }
+ };
+
+ var pluginName = 'horizontalrule';
+
+ // Register a plugin named "horizontalrule".
+ CKEDITOR.plugins.add( pluginName,
+ {
+ init : function( editor )
+ {
+ editor.addCommand( pluginName, horizontalruleCmd );
+ editor.ui.addButton( 'HorizontalRule',
+ {
+ label : editor.lang.horizontalrule,
+ command : pluginName
+ });
+ }
+ });
+})();
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/htmldataprocessor/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/htmldataprocessor/plugin.js
new file mode 100644
index 00000000..677d8f3c
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/htmldataprocessor/plugin.js
@@ -0,0 +1,602 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+(function()
+{
+ // Regex to scan for at the end of blocks, which are actually placeholders.
+ // Safari transforms the to \xa0. (#4172)
+ var tailNbspRegex = /^[\t\r\n ]*(?: |\xa0)$/;
+
+ var protectedSourceMarker = '{cke_protected}';
+
+ // Return the last non-space child node of the block (#4344).
+ function lastNoneSpaceChild( block )
+ {
+ var lastIndex = block.children.length,
+ last = block.children[ lastIndex - 1 ];
+ while ( last && last.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.trim( last.value ) )
+ last = block.children[ --lastIndex ];
+ return last;
+ }
+
+ function trimFillers( block, fromSource )
+ {
+ // If the current node is a block, and if we're converting from source or
+ // we're not in IE then search for and remove any tailing BR node.
+ //
+ // Also, any at the end of blocks are fillers, remove them as well.
+ // (#2886)
+ var children = block.children, lastChild = lastNoneSpaceChild( block );
+ if ( lastChild )
+ {
+ if ( ( fromSource || !CKEDITOR.env.ie ) && lastChild.type == CKEDITOR.NODE_ELEMENT && lastChild.name == 'br' )
+ children.pop();
+ if ( lastChild.type == CKEDITOR.NODE_TEXT && tailNbspRegex.test( lastChild.value ) )
+ children.pop();
+ }
+ }
+
+ function blockNeedsExtension( block, fromSource, extendEmptyBlock )
+ {
+ if( !fromSource && ( !extendEmptyBlock ||
+ typeof extendEmptyBlock == 'function' && ( extendEmptyBlock( block ) === false ) ) )
+ return false;
+
+ // 1. For IE version >=8, empty blocks are displayed correctly themself in wysiwiyg;
+ // 2. For the rest, at least table cell and list item need no filler space.
+ // (#6248)
+ if ( fromSource && CKEDITOR.env.ie &&
+ ( document.documentMode > 7
+ || block.name in CKEDITOR.dtd.tr
+ || block.name in CKEDITOR.dtd.$listItem ) )
+ return false;
+
+ var lastChild = lastNoneSpaceChild( block );
+
+ return !lastChild || lastChild &&
+ ( lastChild.type == CKEDITOR.NODE_ELEMENT && lastChild.name == 'br'
+ // Some of the controls in form needs extension too,
+ // to move cursor at the end of the form. (#4791)
+ || block.name == 'form' && lastChild.name == 'input' );
+ }
+
+ function getBlockExtension( isOutput, emptyBlockFiller )
+ {
+ return function( node )
+ {
+ trimFillers( node, !isOutput );
+
+ if ( blockNeedsExtension( node, !isOutput, emptyBlockFiller ) )
+ {
+ if ( isOutput || CKEDITOR.env.ie )
+ node.add( new CKEDITOR.htmlParser.text( '\xa0' ) );
+ else
+ node.add( new CKEDITOR.htmlParser.element( 'br', {} ) );
+ }
+ };
+ }
+
+ var dtd = CKEDITOR.dtd;
+
+ // Define orders of table elements.
+ var tableOrder = [ 'caption', 'colgroup', 'col', 'thead', 'tfoot', 'tbody' ];
+
+ // Find out the list of block-like tags that can contain .
+ var blockLikeTags = CKEDITOR.tools.extend( {}, dtd.$block, dtd.$listItem, dtd.$tableContent );
+ for ( var i in blockLikeTags )
+ {
+ if ( ! ( 'br' in dtd[i] ) )
+ delete blockLikeTags[i];
+ }
+ // We just avoid filler in right now.
+ // TODO: Support filler for , line break is also occupy line height.
+ delete blockLikeTags.pre;
+ var defaultDataFilterRules =
+ {
+ elements : {
+ a : function( element )
+ {
+ var attrs = element.attributes;
+ if ( attrs && attrs[ 'data-cke-saved-name' ] )
+ attrs[ 'class' ] = ( attrs[ 'class' ] ? attrs[ 'class' ] + ' ' : '' ) + 'cke_anchor';
+ }
+ },
+ attributeNames :
+ [
+ // Event attributes (onXYZ) must not be directly set. They can become
+ // active in the editing area (IE|WebKit).
+ [ ( /^on/ ), 'data-cke-pa-on' ]
+ ]
+ };
+
+ var defaultDataBlockFilterRules = { elements : {} };
+
+ for ( i in blockLikeTags )
+ defaultDataBlockFilterRules.elements[ i ] = getBlockExtension();
+
+ var defaultHtmlFilterRules =
+ {
+ elementNames :
+ [
+ // Remove the "cke:" namespace prefix.
+ [ ( /^cke:/ ), '' ],
+
+ // Ignore tags.
+ [ ( /^\?xml:namespace$/ ), '' ]
+ ],
+
+ attributeNames :
+ [
+ // Attributes saved for changes and protected attributes.
+ [ ( /^data-cke-(saved|pa)-/ ), '' ],
+
+ // All "data-cke-" attributes are to be ignored.
+ [ ( /^data-cke-.*/ ), '' ],
+
+ [ 'hidefocus', '' ]
+ ],
+
+ elements :
+ {
+ $ : function( element )
+ {
+ var attribs = element.attributes;
+
+ if ( attribs )
+ {
+ // Elements marked as temporary are to be ignored.
+ if ( attribs[ 'data-cke-temp' ] )
+ return false;
+
+ // Remove duplicated attributes - #3789.
+ var attributeNames = [ 'name', 'href', 'src' ],
+ savedAttributeName;
+ for ( var i = 0 ; i < attributeNames.length ; i++ )
+ {
+ savedAttributeName = 'data-cke-saved-' + attributeNames[ i ];
+ savedAttributeName in attribs && ( delete attribs[ attributeNames[ i ] ] );
+ }
+ }
+
+ return element;
+ },
+
+ // The contents of table should be in correct order (#4809).
+ table : function( element )
+ {
+ var children = element.children;
+ children.sort( function ( node1, node2 )
+ {
+ return node1.type == CKEDITOR.NODE_ELEMENT && node2.type == node1.type ?
+ CKEDITOR.tools.indexOf( tableOrder, node1.name ) > CKEDITOR.tools.indexOf( tableOrder, node2.name ) ? 1 : -1 : 0;
+ } );
+ },
+
+ embed : function( element )
+ {
+ var parent = element.parent;
+
+ // If the is child of a , copy the width
+ // and height attributes from it.
+ if ( parent && parent.name == 'object' )
+ {
+ var parentWidth = parent.attributes.width,
+ parentHeight = parent.attributes.height;
+ parentWidth && ( element.attributes.width = parentWidth );
+ parentHeight && ( element.attributes.height = parentHeight );
+ }
+ },
+ // Restore param elements into self-closing.
+ param : function( param )
+ {
+ param.children = [];
+ param.isEmpty = true;
+ return param;
+ },
+
+ // Remove empty link but not empty anchor.(#3829)
+ a : function( element )
+ {
+ if ( !( element.children.length ||
+ element.attributes.name ||
+ element.attributes[ 'data-cke-saved-name' ] ) )
+ {
+ return false;
+ }
+ },
+
+ // Remove dummy span in webkit.
+ span: function( element )
+ {
+ if ( element.attributes[ 'class' ] == 'Apple-style-span' )
+ delete element.name;
+ },
+
+ // Empty in IE is reported with filler node ( ).
+ pre : function( element ) { CKEDITOR.env.ie && trimFillers( element ); },
+
+ html : function( element )
+ {
+ delete element.attributes.contenteditable;
+ delete element.attributes[ 'class' ];
+ },
+
+ body : function( element )
+ {
+ delete element.attributes.spellcheck;
+ delete element.attributes.contenteditable;
+ },
+
+ style : function( element )
+ {
+ var child = element.children[ 0 ];
+ child && child.value && ( child.value = CKEDITOR.tools.trim( child.value ));
+
+ if ( !element.attributes.type )
+ element.attributes.type = 'text/css';
+ },
+
+ title : function( element )
+ {
+ var titleText = element.children[ 0 ];
+ titleText && ( titleText.value = element.attributes[ 'data-cke-title' ] || '' );
+ }
+ },
+
+ attributes :
+ {
+ 'class' : function( value, element )
+ {
+ // Remove all class names starting with "cke_".
+ return CKEDITOR.tools.ltrim( value.replace( /(?:^|\s+)cke_[^\s]*/g, '' ) ) || false;
+ }
+ }
+ };
+
+ if ( CKEDITOR.env.ie )
+ {
+ // IE outputs style attribute in capital letters. We should convert
+ // them back to lower case, while not hurting the values (#5930)
+ defaultHtmlFilterRules.attributes.style = function( value, element )
+ {
+ return value.replace( /(^|;)([^\:]+)/g, function( match )
+ {
+ return match.toLowerCase();
+ });
+ };
+ }
+
+ function protectReadOnly( element )
+ {
+ var attrs = element.attributes;
+
+ // We should flag that the element was locked by our code so
+ // it'll be editable by the editor functions (#6046).
+ if ( attrs.contenteditable != "false" )
+ attrs[ 'data-cke-editable' ] = attrs.contenteditable ? 'true' : 1;
+
+ attrs.contenteditable = "false";
+ }
+ function unprotectReadyOnly( element )
+ {
+ var attrs = element.attributes;
+ switch( attrs[ 'data-cke-editable' ] )
+ {
+ case 'true': attrs.contenteditable = 'true'; break;
+ case '1': delete attrs.contenteditable; break;
+ }
+ }
+ // Disable form elements editing mode provided by some browers. (#5746)
+ for ( i in { input : 1, textarea : 1 } )
+ {
+ defaultDataFilterRules.elements[ i ] = protectReadOnly;
+ defaultHtmlFilterRules.elements[ i ] = unprotectReadyOnly;
+ }
+
+ var protectElementRegex = /<(a|area|img|input)\b([^>]*)>/gi,
+ protectAttributeRegex = /\b(href|src|name)\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|(?:[^ "'>]+))/gi;
+
+ var protectElementsRegex = /(?:' +
+ '' +
+ '' +
+ // It looks strange, but for FF2, the styles must go
+ // after , so it (body) becames immediatelly
+ // available. (#3031)
+ CKEDITOR.tools.buildStyleHtml( this.css ) +
+ '<\/html>';
+
+ doc.write( data );
+
+ var win = doc.getWindow();
+
+ // Register the CKEDITOR global.
+ win.$.CKEDITOR = CKEDITOR;
+
+ // Arrow keys for scrolling is only preventable with 'keypress' event in Opera (#4534).
+ doc.on( 'key' + ( CKEDITOR.env.opera? 'press':'down' ), function( evt )
+ {
+ var keystroke = evt.data.getKeystroke(),
+ dir = this.document.getById( this.id ).getAttribute( 'dir' );
+
+ // Delegate key processing to block.
+ if ( this._.onKeyDown && this._.onKeyDown( keystroke ) === false )
+ {
+ evt.data.preventDefault();
+ return;
+ }
+
+ // ESC/ARROW-LEFT(ltr) OR ARROW-RIGHT(rtl)
+ if ( keystroke == 27 || keystroke == ( dir == 'rtl' ? 39 : 37 ) )
+ {
+ if ( this.onEscape && this.onEscape( keystroke ) === false )
+ evt.data.preventDefault();
+ }
+ },
+ this );
+
+ holder = doc.getBody();
+ holder.unselectable();
+ CKEDITOR.env.air && CKEDITOR.tools.callFunction( onLoad );
+ }
+ else
+ holder = this.document.getById( this.id );
+
+ this._.holder = holder;
+ }
+
+ return holder;
+ },
+
+ addBlock : function( name, block )
+ {
+ block = this._.blocks[ name ] = block instanceof CKEDITOR.ui.panel.block ? block
+ : new CKEDITOR.ui.panel.block( this.getHolderElement(), block );
+
+ if ( !this._.currentBlock )
+ this.showBlock( name );
+
+ return block;
+ },
+
+ getBlock : function( name )
+ {
+ return this._.blocks[ name ];
+ },
+
+ showBlock : function( name )
+ {
+ var blocks = this._.blocks,
+ block = blocks[ name ],
+ current = this._.currentBlock,
+ holder = this.forceIFrame ?
+ this.document.getById( this.id + '_frame' )
+ : this._.holder;
+
+ // Disable context menu for block panel.
+ holder.getParent().getParent().disableContextMenu();
+
+ if ( current )
+ {
+ // Clean up the current block's effects on holder.
+ holder.removeAttributes( current.attributes );
+ current.hide();
+ }
+
+ this._.currentBlock = block;
+
+ holder.setAttributes( block.attributes );
+ CKEDITOR.fire( 'ariaWidget', holder );
+
+ // Reset the focus index, so it will always go into the first one.
+ block._.focusIndex = -1;
+
+ this._.onKeyDown = block.onKeyDown && CKEDITOR.tools.bind( block.onKeyDown, block );
+
+ block.onMark = function( item )
+ {
+ holder.setAttribute( 'aria-activedescendant', item.getId() + '_option' );
+ };
+
+ block.onUnmark = function()
+ {
+ holder.removeAttribute( 'aria-activedescendant' );
+ };
+
+ block.show();
+
+ return block;
+ },
+
+ destroy : function()
+ {
+ this.element && this.element.remove();
+ }
+};
+
+CKEDITOR.ui.panel.block = CKEDITOR.tools.createClass(
+{
+ $ : function( blockHolder, blockDefinition )
+ {
+ this.element = blockHolder.append(
+ blockHolder.getDocument().createElement( 'div',
+ {
+ attributes :
+ {
+ 'tabIndex' : -1,
+ 'class' : 'cke_panel_block',
+ 'role' : 'presentation'
+ },
+ styles :
+ {
+ display : 'none'
+ }
+ }) );
+
+ // Copy all definition properties to this object.
+ if ( blockDefinition )
+ CKEDITOR.tools.extend( this, blockDefinition );
+
+ if ( !this.attributes.title )
+ this.attributes.title = this.attributes[ 'aria-label' ];
+
+ this.keys = {};
+
+ this._.focusIndex = -1;
+
+ // Disable context menu for panels.
+ this.element.disableContextMenu();
+ },
+
+ _ : {
+
+ /**
+ * Mark the item specified by the index as current activated.
+ */
+ markItem: function( index )
+ {
+ if ( index == -1 )
+ return;
+ var links = this.element.getElementsByTag( 'a' );
+ var item = links.getItem( this._.focusIndex = index );
+
+ // Safari need focus on the iframe window first(#3389), but we need
+ // lock the blur to avoid hiding the panel.
+ if ( CKEDITOR.env.webkit || CKEDITOR.env.opera )
+ item.getDocument().getWindow().focus();
+ item.focus();
+
+ this.onMark && this.onMark( item );
+ }
+ },
+
+ proto :
+ {
+ show : function()
+ {
+ this.element.setStyle( 'display', '' );
+ },
+
+ hide : function()
+ {
+ if ( !this.onHide || this.onHide.call( this ) !== true )
+ this.element.setStyle( 'display', 'none' );
+ },
+
+ onKeyDown : function( keystroke )
+ {
+ var keyAction = this.keys[ keystroke ];
+ switch ( keyAction )
+ {
+ // Move forward.
+ case 'next' :
+ var index = this._.focusIndex,
+ links = this.element.getElementsByTag( 'a' ),
+ link;
+
+ while ( ( link = links.getItem( ++index ) ) )
+ {
+ // Move the focus only if the element is marked with
+ // the _cke_focus and it it's visible (check if it has
+ // width).
+ if ( link.getAttribute( '_cke_focus' ) && link.$.offsetWidth )
+ {
+ this._.focusIndex = index;
+ link.focus();
+ break;
+ }
+ }
+ return false;
+
+ // Move backward.
+ case 'prev' :
+ index = this._.focusIndex;
+ links = this.element.getElementsByTag( 'a' );
+
+ while ( index > 0 && ( link = links.getItem( --index ) ) )
+ {
+ // Move the focus only if the element is marked with
+ // the _cke_focus and it it's visible (check if it has
+ // width).
+ if ( link.getAttribute( '_cke_focus' ) && link.$.offsetWidth )
+ {
+ this._.focusIndex = index;
+ link.focus();
+ break;
+ }
+ }
+ return false;
+
+ case 'click' :
+ index = this._.focusIndex;
+ link = index >= 0 && this.element.getElementsByTag( 'a' ).getItem( index );
+
+ if ( link )
+ link.$.click ? link.$.click() : link.$.onclick();
+
+ return false;
+ }
+
+ return true;
+ }
+ }
+});
+
+/**
+ * Fired when a panel is added to the document
+ * @name CKEDITOR#ariaWidget
+ * @event
+ * @param {Object} holder The element wrapping the panel
+ */
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/panelbutton/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/panelbutton/plugin.js
new file mode 100644
index 00000000..b51b8a4d
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/panelbutton/plugin.js
@@ -0,0 +1,146 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.plugins.add( 'panelbutton',
+{
+ requires : [ 'button' ],
+ beforeInit : function( editor )
+ {
+ editor.ui.addHandler( CKEDITOR.UI_PANELBUTTON, CKEDITOR.ui.panelButton.handler );
+ }
+});
+
+/**
+ * Button UI element.
+ * @constant
+ * @example
+ */
+CKEDITOR.UI_PANELBUTTON = 4;
+
+(function()
+{
+ var clickFn = function( editor )
+ {
+ var _ = this._;
+
+ if ( _.state == CKEDITOR.TRISTATE_DISABLED )
+ return;
+
+ this.createPanel( editor );
+
+ if ( _.on )
+ {
+ _.panel.hide();
+ return;
+ }
+
+ _.panel.showBlock( this._.id, this.document.getById( this._.id ), 4 );
+ };
+
+
+ CKEDITOR.ui.panelButton = CKEDITOR.tools.createClass(
+ {
+ base : CKEDITOR.ui.button,
+
+ $ : function( definition )
+ {
+ // We don't want the panel definition in this object.
+ var panelDefinition = definition.panel;
+ delete definition.panel;
+
+ this.base( definition );
+
+ this.document = ( panelDefinition
+ && panelDefinition.parent
+ && panelDefinition.parent.getDocument() )
+ || CKEDITOR.document;
+
+ panelDefinition.block =
+ {
+ attributes : panelDefinition.attributes
+ };
+
+ this.hasArrow = true;
+
+ this.click = clickFn;
+
+ this._ =
+ {
+ panelDefinition : panelDefinition
+ };
+ },
+
+ statics :
+ {
+ handler :
+ {
+ create : function( definition )
+ {
+ return new CKEDITOR.ui.panelButton( definition );
+ }
+ }
+ },
+
+ proto :
+ {
+ createPanel : function( editor )
+ {
+ var _ = this._;
+
+ if ( _.panel )
+ return;
+
+ var panelDefinition = this._.panelDefinition || {},
+ panelBlockDefinition = this._.panelDefinition.block,
+ panelParentElement = panelDefinition.parent || CKEDITOR.document.getBody(),
+ panel = this._.panel = new CKEDITOR.ui.floatPanel( editor, panelParentElement, panelDefinition ),
+ block = panel.addBlock( _.id, panelBlockDefinition ),
+ me = this;
+
+ panel.onShow = function()
+ {
+ if ( me.className )
+ this.element.getFirst().addClass( me.className + '_panel' );
+
+ me.setState( CKEDITOR.TRISTATE_ON );
+
+ _.on = 1;
+
+ if ( me.onOpen )
+ me.onOpen();
+ };
+
+ panel.onHide = function( preventOnClose )
+ {
+ if ( me.className )
+ this.element.getFirst().removeClass( me.className + '_panel' );
+
+ me.setState( me.modes && me.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
+
+ _.on = 0;
+
+ if ( !preventOnClose && me.onClose )
+ me.onClose();
+ };
+
+ panel.onEscape = function()
+ {
+ panel.hide();
+ me.document.getById( _.id ).focus();
+ };
+
+ if ( this.onBlock )
+ this.onBlock( panel, block );
+
+ block.onHide = function()
+ {
+ _.on = 0;
+ me.setState( CKEDITOR.TRISTATE_OFF );
+ };
+ }
+ }
+ });
+
+})();
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/pastefromword/filter/default.js b/app/assets/javascripts/ckeditor/_source/plugins/pastefromword/filter/default.js
new file mode 100644
index 00000000..dce3649a
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/pastefromword/filter/default.js
@@ -0,0 +1,1376 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+(function()
+{
+ var fragmentPrototype = CKEDITOR.htmlParser.fragment.prototype,
+ elementPrototype = CKEDITOR.htmlParser.element.prototype;
+
+ fragmentPrototype.onlyChild = elementPrototype.onlyChild = function()
+ {
+ var children = this.children,
+ count = children.length,
+ firstChild = ( count == 1 ) && children[ 0 ];
+ return firstChild || null;
+ };
+
+ elementPrototype.removeAnyChildWithName = function( tagName )
+ {
+ var children = this.children,
+ childs = [],
+ child;
+
+ for ( var i = 0; i < children.length; i++ )
+ {
+ child = children[ i ];
+ if ( !child.name )
+ continue;
+
+ if ( child.name == tagName )
+ {
+ childs.push( child );
+ children.splice( i--, 1 );
+ }
+ childs = childs.concat( child.removeAnyChildWithName( tagName ) );
+ }
+ return childs;
+ };
+
+ elementPrototype.getAncestor = function( tagNameRegex )
+ {
+ var parent = this.parent;
+ while ( parent && !( parent.name && parent.name.match( tagNameRegex ) ) )
+ parent = parent.parent;
+ return parent;
+ };
+
+ fragmentPrototype.firstChild = elementPrototype.firstChild = function( evaluator )
+ {
+ var child;
+
+ for ( var i = 0 ; i < this.children.length ; i++ )
+ {
+ child = this.children[ i ];
+ if ( evaluator( child ) )
+ return child;
+ else if ( child.name )
+ {
+ child = child.firstChild( evaluator );
+ if ( child )
+ return child;
+ }
+ }
+
+ return null;
+ };
+
+ // Adding a (set) of styles to the element's 'style' attributes.
+ elementPrototype.addStyle = function( name, value, isPrepend )
+ {
+ var styleText, addingStyleText = '';
+ // name/value pair.
+ if ( typeof value == 'string' )
+ addingStyleText += name + ':' + value + ';';
+ else
+ {
+ // style literal.
+ if ( typeof name == 'object' )
+ {
+ for ( var style in name )
+ {
+ if ( name.hasOwnProperty( style ) )
+ addingStyleText += style + ':' + name[ style ] + ';';
+ }
+ }
+ // raw style text form.
+ else
+ addingStyleText += name;
+
+ isPrepend = value;
+ }
+
+ if ( !this.attributes )
+ this.attributes = {};
+
+ styleText = this.attributes.style || '';
+
+ styleText = ( isPrepend ?
+ [ addingStyleText, styleText ]
+ : [ styleText, addingStyleText ] ).join( ';' );
+
+ this.attributes.style = styleText.replace( /^;|;(?=;)/, '' );
+ };
+
+ /**
+ * Return the DTD-valid parent tag names of the specified one.
+ * @param tagName
+ */
+ CKEDITOR.dtd.parentOf = function( tagName )
+ {
+ var result = {};
+ for ( var tag in this )
+ {
+ if ( tag.indexOf( '$' ) == -1 && this[ tag ][ tagName ] )
+ result[ tag ] = 1;
+ }
+ return result;
+ };
+
+ // 1. move consistent list item styles up to list root.
+ // 2. clear out unnecessary list item numbering.
+ function postProcessList( list )
+ {
+ var children = list.children,
+ child,
+ attrs,
+ count = list.children.length,
+ match,
+ mergeStyle,
+ styleTypeRegexp = /list-style-type:(.*?)(?:;|$)/,
+ stylesFilter = CKEDITOR.plugins.pastefromword.filters.stylesFilter;
+
+ attrs = list.attributes;
+ if ( styleTypeRegexp.exec( attrs.style ) )
+ return;
+
+ for ( var i = 0; i < count; i++ )
+ {
+ child = children[ i ];
+
+ if ( child.attributes.value && Number( child.attributes.value ) == i + 1 )
+ delete child.attributes.value;
+
+ match = styleTypeRegexp.exec( child.attributes.style );
+
+ if ( match )
+ {
+ if ( match[ 1 ] == mergeStyle || !mergeStyle )
+ mergeStyle = match[ 1 ];
+ else
+ {
+ mergeStyle = null;
+ break;
+ }
+ }
+ }
+
+ if ( mergeStyle )
+ {
+ for ( i = 0; i < count; i++ )
+ {
+ attrs = children[ i ].attributes;
+ attrs.style && ( attrs.style = stylesFilter( [ [ 'list-style-type'] ] )( attrs.style ) || '' );
+ }
+
+ list.addStyle( 'list-style-type', mergeStyle );
+ }
+ }
+
+ var cssLengthRelativeUnit = /^([.\d]*)+(em|ex|px|gd|rem|vw|vh|vm|ch|mm|cm|in|pt|pc|deg|rad|ms|s|hz|khz){1}?/i;
+ var emptyMarginRegex = /^(?:\b0[^\s]*\s*){1,4}$/; // e.g. 0px 0pt 0px
+ var romanLiternalPattern = '^m{0,4}(cm|cd|d?c{0,3})(xc|xl|l?x{0,3})(ix|iv|v?i{0,3})$',
+ lowerRomanLiteralRegex = new RegExp( romanLiternalPattern ),
+ upperRomanLiteralRegex = new RegExp( romanLiternalPattern.toUpperCase() );
+
+ var orderedPatterns = { 'decimal' : /\d+/, 'lower-roman': lowerRomanLiteralRegex, 'upper-roman': upperRomanLiteralRegex, 'lower-alpha' : /^[a-z]+$/, 'upper-alpha': /^[A-Z]+$/ },
+ unorderedPatterns = { 'disc' : /[l\u00B7\u2002]/, 'circle' : /[\u006F\u00D8]/,'square' : /[\u006E\u25C6]/},
+ listMarkerPatterns = { 'ol' : orderedPatterns, 'ul' : unorderedPatterns },
+ romans = [ [1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'], [100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'], [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I'] ],
+ alpahbets = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ // Convert roman numbering back to decimal.
+ function fromRoman( str )
+ {
+ str = str.toUpperCase();
+ var l = romans.length, retVal = 0;
+ for ( var i = 0; i < l; ++i )
+ {
+ for ( var j = romans[i], k = j[1].length; str.substr( 0, k ) == j[1]; str = str.substr( k ) )
+ retVal += j[ 0 ];
+ }
+ return retVal;
+ }
+
+ // Convert alphabet numbering back to decimal.
+ function fromAlphabet( str )
+ {
+ str = str.toUpperCase();
+ var l = alpahbets.length, retVal = 1;
+ for ( var x = 1; str.length > 0; x *= l )
+ {
+ retVal += alpahbets.indexOf( str.charAt( str.length - 1 ) ) * x;
+ str = str.substr( 0, str.length - 1 );
+ }
+ return retVal;
+ }
+
+ var listBaseIndent = 0,
+ previousListItemMargin = null,
+ previousListId;
+
+ var plugin = ( CKEDITOR.plugins.pastefromword =
+ {
+ utils :
+ {
+ // Create a which indicate an list item type.
+ createListBulletMarker : function ( bullet, bulletText )
+ {
+ var marker = new CKEDITOR.htmlParser.element( 'cke:listbullet' );
+ marker.attributes = { 'cke:listsymbol' : bullet[ 0 ] };
+ marker.add( new CKEDITOR.htmlParser.text( bulletText ) );
+ return marker;
+ },
+
+ isListBulletIndicator : function( element )
+ {
+ var styleText = element.attributes && element.attributes.style;
+ if ( /mso-list\s*:\s*Ignore/i.test( styleText ) )
+ return true;
+ },
+
+ isContainingOnlySpaces : function( element )
+ {
+ var text;
+ return ( ( text = element.onlyChild() )
+ && ( /^(:?\s| )+$/ ).test( text.value ) );
+ },
+
+ resolveList : function( element )
+ {
+ // indicate a list item.
+ var attrs = element.attributes,
+ listMarker;
+
+ if ( ( listMarker = element.removeAnyChildWithName( 'cke:listbullet' ) )
+ && listMarker.length
+ && ( listMarker = listMarker[ 0 ] ) )
+ {
+ element.name = 'cke:li';
+
+ if ( attrs.style )
+ {
+ attrs.style = plugin.filters.stylesFilter(
+ [
+ // Text-indent is not representing list item level any more.
+ [ 'text-indent' ],
+ [ 'line-height' ],
+ // First attempt is to resolve indent level from on a constant margin increment.
+ [ ( /^margin(:?-left)?$/ ), null, function( margin )
+ {
+ // Deal with component/short-hand form.
+ var values = margin.split( ' ' );
+ margin = plugin.utils.convertToPx( values[ 3 ] || values[ 1 ] || values [ 0 ] );
+ margin = parseInt( margin, 10 );
+
+ // Figure out the indent unit by checking the first time of incrementation.
+ if ( !listBaseIndent && previousListItemMargin !== null && margin > previousListItemMargin )
+ listBaseIndent = margin - previousListItemMargin;
+
+ previousListItemMargin = margin;
+
+ attrs[ 'cke:indent' ] = listBaseIndent && ( Math.ceil( margin / listBaseIndent ) + 1 ) || 1;
+ } ],
+ // The best situation: "mso-list:l0 level1 lfo2" tells the belonged list root, list item indentation, etc.
+ [ ( /^mso-list$/ ), null, function( val )
+ {
+ val = val.split( ' ' );
+ var listId = Number( val[ 0 ].match( /\d+/ ) ),
+ indent = Number( val[ 1 ].match( /\d+/ ) );
+
+ listId !== previousListId && ( attrs[ 'cke:reset' ] = 1 );
+ previousListId = listId;
+ attrs[ 'cke:indent' ] = indent;
+ } ]
+ ] )( attrs.style, element ) || '';
+ }
+
+ // First level list item might be presented without a margin.
+
+
+ // In case all above doesn't apply.
+ if ( !attrs[ 'cke:indent' ] )
+ {
+ previousListItemMargin = 0;
+ attrs[ 'cke:indent' ] = 1;
+ }
+
+ // Inherit attributes from bullet.
+ CKEDITOR.tools.extend( attrs, listMarker.attributes );
+ return true;
+ }
+ // Current list disconnected.
+ else
+ previousListId = previousListItemMargin = listBaseIndent = null;
+
+ return false;
+ },
+
+ // Convert various length units to 'px' in ignorance of DPI.
+ convertToPx : ( function ()
+ {
+ var calculator = CKEDITOR.dom.element.createFromHtml(
+ '
', CKEDITOR.document );
+ CKEDITOR.document.getBody().append( calculator );
+
+ return function( cssLength )
+ {
+ if ( cssLengthRelativeUnit.test( cssLength ) )
+ {
+ calculator.setStyle( 'width', cssLength );
+ return calculator.$.clientWidth + 'px';
+ }
+
+ return cssLength;
+ };
+ } )(),
+
+ // Providing a shorthand style then retrieve one or more style component values.
+ getStyleComponents : ( function()
+ {
+ var calculator = CKEDITOR.dom.element.createFromHtml(
+ '
',
+ CKEDITOR.document );
+ CKEDITOR.document.getBody().append( calculator );
+
+ return function( name, styleValue, fetchList )
+ {
+ calculator.setStyle( name, styleValue );
+ var styles = {},
+ count = fetchList.length;
+ for ( var i = 0; i < count; i++ )
+ styles[ fetchList[ i ] ] = calculator.getStyle( fetchList[ i ] );
+
+ return styles;
+ };
+ } )(),
+
+ listDtdParents : CKEDITOR.dtd.parentOf( 'ol' )
+ },
+
+ filters :
+ {
+ // Transform a normal list into flat list items only presentation.
+ // E.g. level1level2 =>
+ // level1
+ // level2
+ flattenList : function( element )
+ {
+ var attrs = element.attributes,
+ parent = element.parent;
+
+ var listStyleType,
+ indentLevel = 1;
+
+ // Resolve how many level nested.
+ while ( parent )
+ {
+ parent.attributes && parent.attributes[ 'cke:list' ] && indentLevel++;
+ parent = parent.parent;
+ }
+
+ // All list items are of the same type.
+ switch ( attrs.type )
+ {
+ case 'a' :
+ listStyleType = 'lower-alpha';
+ break;
+ // TODO: Support more list style type from MS-Word.
+ }
+
+ var children = element.children,
+ child;
+
+ for ( var i = 0; i < children.length; i++ )
+ {
+ child = children[ i ];
+ var attributes = child.attributes;
+
+ if ( child.name in CKEDITOR.dtd.$listItem )
+ {
+ var listItemChildren = child.children,
+ count = listItemChildren.length,
+ last = listItemChildren[ count - 1 ];
+
+ // Move out nested list.
+ if ( last.name in CKEDITOR.dtd.$list )
+ {
+ children.splice( i + 1, 0, last );
+ last.parent = element;
+
+ // Remove the parent list item if it's just a holder.
+ if ( !--listItemChildren.length )
+ children.splice( i, 1 );
+ }
+
+ child.name = 'cke:li';
+
+ // Inherit numbering from list root on the first list item.
+ attrs.start && !i && ( attributes.value = attrs.start );
+
+ plugin.filters.stylesFilter(
+ [
+ [ 'tab-stops', null, function( val )
+ {
+ var margin = val.split( ' ' )[ 1 ].match( cssLengthRelativeUnit );
+ margin && ( previousListItemMargin = parseInt( plugin.utils.convertToPx( margin[ 0 ] ), 10 ) );
+ } ],
+ [ 'mso-list', null, function( val )
+ {
+ val = val.split( ' ' );
+ var listId = Number( val[ 0 ].match( /\d+/ ) );
+ listId !== previousListId && ( attributes[ 'cke:reset' ] = 1 );
+ previousListId = listId;
+ } ]
+ ] )( attributes.style );
+
+ attributes[ 'cke:indent' ] = indentLevel;
+ attributes[ 'cke:listtype' ] = element.name;
+ attributes[ 'cke:list-style-type' ] = listStyleType;
+ }
+ }
+
+ delete element.name;
+
+ // We're loosing tag name here, signalize this element as a list.
+ attrs[ 'cke:list' ] = 1;
+ },
+
+ /**
+ * Try to collect all list items among the children and establish one
+ * or more HTML list structures for them.
+ * @param element
+ */
+ assembleList : function( element )
+ {
+ var children = element.children, child,
+ listItem, // The current processing cke:li element.
+ listItemAttrs,
+ listItemIndent, // Indent level of current list item.
+ lastIndent,
+ lastListItem, // The previous one just been added to the list.
+ list, // Current staging list and it's parent list if any.
+ openedLists = [],
+ previousListStyleType,
+ previousListType;
+
+ // Properties of the list item are to be resolved from the list bullet.
+ var bullet,
+ listType,
+ listStyleType,
+ itemNumeric;
+
+ for ( var i = 0; i < children.length; i++ )
+ {
+ child = children[ i ];
+
+ if ( 'cke:li' == child.name )
+ {
+ child.name = 'li';
+ listItem = child;
+ listItemAttrs = listItem.attributes;
+ bullet = listItemAttrs[ 'cke:listsymbol' ];
+ bullet = bullet && bullet.match( /^(?:[(]?)([^\s]+?)([.)]?)$/ );
+ listType = listStyleType = itemNumeric = null;
+
+ if ( listItemAttrs[ 'cke:ignored' ] )
+ {
+ children.splice( i--, 1 );
+ continue;
+ }
+
+
+ // This's from a new list root.
+ listItemAttrs[ 'cke:reset' ] && ( list = lastIndent = lastListItem = null );
+
+ // List item indent level might come from a real list indentation or
+ // been resolved from a pseudo list item's margin value, even get
+ // no indentation at all.
+ listItemIndent = Number( listItemAttrs[ 'cke:indent' ] );
+
+ // We're moving out of the current list, cleaning up.
+ if ( listItemIndent != lastIndent )
+ previousListType = previousListStyleType = null;
+
+ // List type and item style are already resolved.
+ if ( !bullet )
+ {
+ listType = listItemAttrs[ 'cke:listtype' ] || 'ol';
+ listStyleType = listItemAttrs[ 'cke:list-style-type' ];
+ }
+ else
+ {
+ // Probably share the same list style type with previous list item,
+ // give it priority to avoid ambiguous between C(Alpha) and C.(Roman).
+ if ( previousListType && listMarkerPatterns[ previousListType ] [ previousListStyleType ].test( bullet[ 1 ] ) )
+ {
+ listType = previousListType;
+ listStyleType = previousListStyleType;
+ }
+ else
+ {
+ for ( var type in listMarkerPatterns )
+ {
+ for ( var style in listMarkerPatterns[ type ] )
+ {
+ if ( listMarkerPatterns[ type ][ style ].test( bullet[ 1 ] ) )
+ {
+ // Small numbering has higher priority, when dealing with ambiguous
+ // between C(Alpha) and C.(Roman).
+ if ( type == 'ol' && ( /alpha|roman/ ).test( style ) )
+ {
+ var num = /roman/.test( style ) ? fromRoman( bullet[ 1 ] ) : fromAlphabet( bullet[ 1 ] );
+ if ( !itemNumeric || num < itemNumeric )
+ {
+ itemNumeric = num;
+ listType = type;
+ listStyleType = style;
+ }
+ }
+ else
+ {
+ listType = type;
+ listStyleType = style;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Simply use decimal/disc for the rest forms of unrepresentable
+ // numerals, e.g. Chinese..., but as long as there a second part
+ // included, it has a bigger chance of being a order list ;)
+ !listType && ( listType = bullet[ 2 ] ? 'ol' : 'ul' );
+ }
+
+ previousListType = listType;
+ previousListStyleType = listStyleType || ( listType == 'ol' ? 'decimal' : 'disc' );
+ if ( listStyleType && listStyleType != ( listType == 'ol' ? 'decimal' : 'disc' ) )
+ listItem.addStyle( 'list-style-type', listStyleType );
+
+ // Figure out start numbering.
+ if ( listType == 'ol' && bullet )
+ {
+ switch ( listStyleType )
+ {
+ case 'decimal' :
+ itemNumeric = Number( bullet[ 1 ] );
+ break;
+ case 'lower-roman':
+ case 'upper-roman':
+ itemNumeric = fromRoman( bullet[ 1 ] );
+ break;
+ case 'lower-alpha':
+ case 'upper-alpha':
+ itemNumeric = fromAlphabet( bullet[ 1 ] );
+ break;
+ }
+
+ // Always create the numbering, swipe out unnecessary ones later.
+ listItem.attributes.value = itemNumeric;
+ }
+
+ // Start the list construction.
+ if ( !list )
+ {
+ openedLists.push( list = new CKEDITOR.htmlParser.element( listType ) );
+ list.add( listItem );
+ children[ i ] = list;
+ }
+ else
+ {
+ if ( listItemIndent > lastIndent )
+ {
+ openedLists.push( list = new CKEDITOR.htmlParser.element( listType ) );
+ list.add( listItem );
+ lastListItem.add( list );
+ }
+ else if ( listItemIndent < lastIndent )
+ {
+ // There might be a negative gap between two list levels. (#4944)
+ var diff = lastIndent - listItemIndent,
+ parent;
+ while ( diff-- && ( parent = list.parent ) )
+ list = parent.parent;
+
+ list.add( listItem );
+ }
+ else
+ list.add( listItem );
+
+ children.splice( i--, 1 );
+ }
+
+ lastListItem = listItem;
+ lastIndent = listItemIndent;
+ }
+ else if ( list )
+ list = lastIndent = lastListItem = null;
+ }
+
+ for ( i = 0; i < openedLists.length; i++ )
+ postProcessList( openedLists[ i ] );
+
+ list = lastIndent = lastListItem = previousListId = previousListItemMargin = listBaseIndent = null;
+ },
+
+ /**
+ * A simple filter which always rejecting.
+ */
+ falsyFilter : function( value )
+ {
+ return false;
+ },
+
+ /**
+ * A filter dedicated on the 'style' attribute filtering, e.g. dropping/replacing style properties.
+ * @param styles {Array} in form of [ styleNameRegexp, styleValueRegexp,
+ * newStyleValue/newStyleGenerator, newStyleName ] where only the first
+ * parameter is mandatory.
+ * @param whitelist {Boolean} Whether the {@param styles} will be considered as a white-list.
+ */
+ stylesFilter : function( styles, whitelist )
+ {
+ return function( styleText, element )
+ {
+ var rules = [];
+ // html-encoded quote might be introduced by 'font-family'
+ // from MS-Word which confused the following regexp. e.g.
+ //'font-family: "Lucida, Console"'
+ ( styleText || '' )
+ .replace( /"/g, '"' )
+ .replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g,
+ function( match, name, value )
+ {
+ name = name.toLowerCase();
+ name == 'font-family' && ( value = value.replace( /["']/g, '' ) );
+
+ var namePattern,
+ valuePattern,
+ newValue,
+ newName;
+ for ( var i = 0 ; i < styles.length; i++ )
+ {
+ if ( styles[ i ] )
+ {
+ namePattern = styles[ i ][ 0 ];
+ valuePattern = styles[ i ][ 1 ];
+ newValue = styles[ i ][ 2 ];
+ newName = styles[ i ][ 3 ];
+
+ if ( name.match( namePattern )
+ && ( !valuePattern || value.match( valuePattern ) ) )
+ {
+ name = newName || name;
+ whitelist && ( newValue = newValue || value );
+
+ if ( typeof newValue == 'function' )
+ newValue = newValue( value, element, name );
+
+ // Return an couple indicate both name and value
+ // changed.
+ if ( newValue && newValue.push )
+ name = newValue[ 0 ], newValue = newValue[ 1 ];
+
+ if ( typeof newValue == 'string' )
+ rules.push( [ name, newValue ] );
+ return;
+ }
+ }
+ }
+
+ !whitelist && rules.push( [ name, value ] );
+
+ });
+
+ for ( var i = 0 ; i < rules.length ; i++ )
+ rules[ i ] = rules[ i ].join( ':' );
+ return rules.length ?
+ ( rules.join( ';' ) + ';' ) : false;
+ };
+ },
+
+ /**
+ * Migrate the element by decorate styles on it.
+ * @param styleDefiniton
+ * @param variables
+ */
+ elementMigrateFilter : function ( styleDefiniton, variables )
+ {
+ return function( element )
+ {
+ var styleDef =
+ variables ?
+ new CKEDITOR.style( styleDefiniton, variables )._.definition
+ : styleDefiniton;
+ element.name = styleDef.element;
+ CKEDITOR.tools.extend( element.attributes, CKEDITOR.tools.clone( styleDef.attributes ) );
+ element.addStyle( CKEDITOR.style.getStyleText( styleDef ) );
+ };
+ },
+
+ /**
+ * Migrate styles by creating a new nested stylish element.
+ * @param styleDefinition
+ */
+ styleMigrateFilter : function( styleDefinition, variableName )
+ {
+
+ var elementMigrateFilter = this.elementMigrateFilter;
+ return function( value, element )
+ {
+ // Build an stylish element first.
+ var styleElement = new CKEDITOR.htmlParser.element( null ),
+ variables = {};
+
+ variables[ variableName ] = value;
+ elementMigrateFilter( styleDefinition, variables )( styleElement );
+ // Place the new element inside the existing span.
+ styleElement.children = element.children;
+ element.children = [ styleElement ];
+ };
+ },
+
+ /**
+ * A filter which remove cke-namespaced-attribute on
+ * all none-cke-namespaced elements.
+ * @param value
+ * @param element
+ */
+ bogusAttrFilter : function( value, element )
+ {
+ if ( element.name.indexOf( 'cke:' ) == -1 )
+ return false;
+ },
+
+ /**
+ * A filter which will be used to apply inline css style according the stylesheet
+ * definition rules, is generated lazily when filtering.
+ */
+ applyStyleFilter : null
+
+ },
+
+ getRules : function( editor )
+ {
+ var dtd = CKEDITOR.dtd,
+ blockLike = CKEDITOR.tools.extend( {}, dtd.$block, dtd.$listItem, dtd.$tableContent ),
+ config = editor.config,
+ filters = this.filters,
+ falsyFilter = filters.falsyFilter,
+ stylesFilter = filters.stylesFilter,
+ elementMigrateFilter = filters.elementMigrateFilter,
+ styleMigrateFilter = CKEDITOR.tools.bind( this.filters.styleMigrateFilter, this.filters ),
+ createListBulletMarker = this.utils.createListBulletMarker,
+ flattenList = filters.flattenList,
+ assembleList = filters.assembleList,
+ isListBulletIndicator = this.utils.isListBulletIndicator,
+ containsNothingButSpaces = this.utils.isContainingOnlySpaces,
+ resolveListItem = this.utils.resolveList,
+ convertToPx = this.utils.convertToPx,
+ getStyleComponents = this.utils.getStyleComponents,
+ listDtdParents = this.utils.listDtdParents,
+ removeFontStyles = config.pasteFromWordRemoveFontStyles !== false,
+ removeStyles = config.pasteFromWordRemoveStyles !== false;
+
+ return {
+
+ elementNames :
+ [
+ // Remove script, meta and link elements.
+ [ ( /meta|link|script/ ), '' ]
+ ],
+
+ root : function( element )
+ {
+ element.filterChildren();
+ assembleList( element );
+ },
+
+ elements :
+ {
+ '^' : function( element )
+ {
+ // Transform CSS style declaration to inline style.
+ var applyStyleFilter;
+ if ( CKEDITOR.env.gecko && ( applyStyleFilter = filters.applyStyleFilter ) )
+ applyStyleFilter( element );
+ },
+
+ $ : function( element )
+ {
+ var tagName = element.name || '',
+ attrs = element.attributes;
+
+ // Convert length unit of width/height on blocks to
+ // a more editor-friendly way (px).
+ if ( tagName in blockLike
+ && attrs.style )
+ {
+ attrs.style = stylesFilter(
+ [ [ ( /^(:?width|height)$/ ), null, convertToPx ] ] )( attrs.style ) || '';
+ }
+
+ // Processing headings.
+ if ( tagName.match( /h\d/ ) )
+ {
+ element.filterChildren();
+ // Is the heading actually a list item?
+ if ( resolveListItem( element ) )
+ return;
+
+ // Adapt heading styles to editor's convention.
+ elementMigrateFilter( config[ 'format_' + tagName ] )( element );
+ }
+ // Remove inline elements which contain only empty spaces.
+ else if ( tagName in dtd.$inline )
+ {
+ element.filterChildren();
+ if ( containsNothingButSpaces( element ) )
+ delete element.name;
+ }
+ // Remove element with ms-office namespace,
+ // with it's content preserved, e.g. 'o:p'.
+ else if ( tagName.indexOf( ':' ) != -1
+ && tagName.indexOf( 'cke' ) == -1 )
+ {
+ element.filterChildren();
+
+ // Restore image real link from vml.
+ if ( tagName == 'v:imagedata' )
+ {
+ var href = element.attributes[ 'o:href' ];
+ if ( href )
+ element.attributes.src = href;
+ element.name = 'img';
+ return;
+ }
+ delete element.name;
+ }
+
+ // Assembling list items into a whole list.
+ if ( tagName in listDtdParents )
+ {
+ element.filterChildren();
+ assembleList( element );
+ }
+ },
+
+ // We'll drop any style sheet, but Firefox conclude
+ // certain styles in a single style element, which are
+ // required to be changed into inline ones.
+ 'style' : function( element )
+ {
+ if ( CKEDITOR.env.gecko )
+ {
+ // Grab only the style definition section.
+ var styleDefSection = element.onlyChild().value.match( /\/\* Style Definitions \*\/([\s\S]*?)\/\*/ ),
+ styleDefText = styleDefSection && styleDefSection[ 1 ],
+ rules = {}; // Storing the parsed result.
+
+ if ( styleDefText )
+ {
+ styleDefText
+ // Remove line-breaks.
+ .replace(/[\n\r]/g,'')
+ // Extract selectors and style properties.
+ .replace( /(.+?)\{(.+?)\}/g,
+ function( rule, selectors, styleBlock )
+ {
+ selectors = selectors.split( ',' );
+ var length = selectors.length, selector;
+ for ( var i = 0; i < length; i++ )
+ {
+ // Assume MS-Word mostly generate only simple
+ // selector( [Type selector][Class selector]).
+ CKEDITOR.tools.trim( selectors[ i ] )
+ .replace( /^(\w+)(\.[\w-]+)?$/g,
+ function( match, tagName, className )
+ {
+ tagName = tagName || '*';
+ className = className.substring( 1, className.length );
+
+ // Reject MS-Word Normal styles.
+ if ( className.match( /MsoNormal/ ) )
+ return;
+
+ if ( !rules[ tagName ] )
+ rules[ tagName ] = {};
+ if ( className )
+ rules[ tagName ][ className ] = styleBlock;
+ else
+ rules[ tagName ] = styleBlock;
+ } );
+ }
+ });
+
+ filters.applyStyleFilter = function( element )
+ {
+ var name = rules[ '*' ] ? '*' : element.name,
+ className = element.attributes && element.attributes[ 'class' ],
+ style;
+ if ( name in rules )
+ {
+ style = rules[ name ];
+ if ( typeof style == 'object' )
+ style = style[ className ];
+ // Maintain style rules priorities.
+ style && element.addStyle( style, true );
+ }
+ };
+ }
+ }
+ return false;
+ },
+
+ 'p' : function( element )
+ {
+ // This's a fall-back approach to recognize list item in FF3.6,
+ // as it's not perfect as not all list style (e.g. "heading list") is shipped
+ // with this pattern. (#6662)
+ if ( /MsoListParagraph/.exec( element.attributes[ 'class' ] ) )
+ {
+ var bulletText = element.firstChild( function( node )
+ {
+ return node.type == CKEDITOR.NODE_TEXT && !containsNothingButSpaces( node.parent );
+ });
+ var bullet = bulletText && bulletText.parent,
+ bulletAttrs = bullet && bullet.attributes;
+ bulletAttrs && !bulletAttrs.style && ( bulletAttrs.style = 'mso-list: Ignore;' );
+ }
+
+ element.filterChildren();
+
+ // Is the paragraph actually a list item?
+ if ( resolveListItem( element ) )
+ return;
+
+ // Adapt paragraph formatting to editor's convention
+ // according to enter-mode.
+ if ( config.enterMode == CKEDITOR.ENTER_BR )
+ {
+ // We suffer from attribute/style lost in this situation.
+ delete element.name;
+ element.add( new CKEDITOR.htmlParser.element( 'br' ) );
+ }
+ else
+ elementMigrateFilter( config[ 'format_' + ( config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ] )( element );
+ },
+
+ 'div' : function( element )
+ {
+ // Aligned table with no text surrounded is represented by a wrapper div, from which
+ // table cells inherit as text-align styles, which is wrong.
+ // Instead we use a clear-float div after the table to properly achieve the same layout.
+ var singleChild = element.onlyChild();
+ if ( singleChild && singleChild.name == 'table' )
+ {
+ var attrs = element.attributes;
+ singleChild.attributes = CKEDITOR.tools.extend( singleChild.attributes, attrs );
+ attrs.style && singleChild.addStyle( attrs.style );
+
+ var clearFloatDiv = new CKEDITOR.htmlParser.element( 'div' );
+ clearFloatDiv.addStyle( 'clear' ,'both' );
+ element.add( clearFloatDiv );
+ delete element.name;
+ }
+ },
+
+ 'td' : function ( element )
+ {
+ // 'td' in 'thead' is actually .
+ if ( element.getAncestor( 'thead') )
+ element.name = 'th';
+ },
+
+ // MS-Word sometimes present list as a mixing of normal list
+ // and pseudo-list, normalize the previous ones into pseudo form.
+ 'ol' : flattenList,
+ 'ul' : flattenList,
+ 'dl' : flattenList,
+
+ 'font' : function( element )
+ {
+ // Drop the font tag if it comes from list bullet text.
+ if ( isListBulletIndicator( element.parent ) )
+ {
+ delete element.name;
+ return;
+ }
+
+ element.filterChildren();
+
+ var attrs = element.attributes,
+ styleText = attrs.style,
+ parent = element.parent;
+
+ if ( 'font' == parent.name ) // Merge nested tags.
+ {
+ CKEDITOR.tools.extend( parent.attributes,
+ element.attributes );
+ styleText && parent.addStyle( styleText );
+ delete element.name;
+ }
+ // Convert the merged into a span with all attributes preserved.
+ else
+ {
+ styleText = styleText || '';
+ // IE's having those deprecated attributes, normalize them.
+ if ( attrs.color )
+ {
+ attrs.color != '#000000' && ( styleText += 'color:' + attrs.color + ';' );
+ delete attrs.color;
+ }
+ if ( attrs.face )
+ {
+ styleText += 'font-family:' + attrs.face + ';';
+ delete attrs.face;
+ }
+ // TODO: Mapping size in ranges of xx-small,
+ // x-small, small, medium, large, x-large, xx-large.
+ if ( attrs.size )
+ {
+ styleText += 'font-size:' +
+ ( attrs.size > 3 ? 'large'
+ : ( attrs.size < 3 ? 'small' : 'medium' ) ) + ';';
+ delete attrs.size;
+ }
+
+ element.name = 'span';
+ element.addStyle( styleText );
+ }
+ },
+
+ 'span' : function( element )
+ {
+ // Remove the span if it comes from list bullet text.
+ if ( isListBulletIndicator( element.parent ) )
+ return false;
+
+ element.filterChildren();
+ if ( containsNothingButSpaces( element ) )
+ {
+ delete element.name;
+ return null;
+ }
+
+ // List item bullet type is supposed to be indicated by
+ // the text of a span with style 'mso-list : Ignore' or an image.
+ if ( isListBulletIndicator( element ) )
+ {
+ var listSymbolNode = element.firstChild( function( node )
+ {
+ return node.value || node.name == 'img';
+ });
+
+ var listSymbol = listSymbolNode && ( listSymbolNode.value || 'l.' ),
+ listType = listSymbol && listSymbol.match( /^(?:[(]?)([^\s]+?)([.)]?)$/ );
+
+ if ( listType )
+ {
+ var marker = createListBulletMarker( listType, listSymbol );
+ // Some non-existed list items might be carried by an inconsequential list, indicate by "mso-hide:all/display:none",
+ // those are to be removed later, now mark it with "cke:ignored".
+ var ancestor = element.getAncestor( 'span' );
+ if ( ancestor && (/ mso-hide:\s*all|display:\s*none /).test( ancestor.attributes.style ) )
+ marker.attributes[ 'cke:ignored' ] = 1;
+ return marker;
+ }
+ }
+
+ // Update the src attribute of image element with href.
+ var children = element.children,
+ attrs = element.attributes,
+ styleText = attrs && attrs.style,
+ firstChild = children && children[ 0 ];
+
+ // Assume MS-Word mostly carry font related styles on ,
+ // adapting them to editor's convention.
+ if ( styleText )
+ {
+ attrs.style = stylesFilter(
+ [
+ // Drop 'inline-height' style which make lines overlapping.
+ [ 'line-height' ],
+ [ ( /^font-family$/ ), null, !removeFontStyles ? styleMigrateFilter( config[ 'font_style' ], 'family' ) : null ] ,
+ [ ( /^font-size$/ ), null, !removeFontStyles ? styleMigrateFilter( config[ 'fontSize_style' ], 'size' ) : null ] ,
+ [ ( /^color$/ ), null, !removeFontStyles ? styleMigrateFilter( config[ 'colorButton_foreStyle' ], 'color' ) : null ] ,
+ [ ( /^background-color$/ ), null, !removeFontStyles ? styleMigrateFilter( config[ 'colorButton_backStyle' ], 'color' ) : null ]
+ ] )( styleText, element ) || '';
+ }
+
+ return null;
+ },
+
+ // Migrate basic style formats to editor configured ones.
+ 'b' : elementMigrateFilter( config[ 'coreStyles_bold' ] ),
+ 'i' : elementMigrateFilter( config[ 'coreStyles_italic' ] ),
+ 'u' : elementMigrateFilter( config[ 'coreStyles_underline' ] ),
+ 's' : elementMigrateFilter( config[ 'coreStyles_strike' ] ),
+ 'sup' : elementMigrateFilter( config[ 'coreStyles_superscript' ] ),
+ 'sub' : elementMigrateFilter( config[ 'coreStyles_subscript' ] ),
+ // Editor doesn't support anchor with content currently (#3582),
+ // drop such anchors with content preserved.
+ 'a' : function( element )
+ {
+ var attrs = element.attributes;
+ if ( attrs && !attrs.href && attrs.name )
+ delete element.name;
+ },
+ 'cke:listbullet' : function( element )
+ {
+ if ( element.getAncestor( /h\d/ ) && !config.pasteFromWordNumberedHeadingToList )
+ delete element.name;
+ }
+ },
+
+ attributeNames :
+ [
+ // Remove onmouseover and onmouseout events (from MS Word comments effect)
+ [ ( /^onmouse(:?out|over)/ ), '' ],
+ // Onload on image element.
+ [ ( /^onload$/ ), '' ],
+ // Remove office and vml attribute from elements.
+ [ ( /(?:v|o):\w+/ ), '' ],
+ // Remove lang/language attributes.
+ [ ( /^lang/ ), '' ]
+ ],
+
+ attributes :
+ {
+ 'style' : stylesFilter(
+ removeStyles ?
+ // Provide a white-list of styles that we preserve, those should
+ // be the ones that could later be altered with editor tools.
+ [
+ // Leave list-style-type
+ [ ( /^list-style-type$/ ), null ],
+
+ // Preserve margin-left/right which used as default indent style in the editor.
+ [ ( /^margin$|^margin-(?!bottom|top)/ ), null, function( value, element, name )
+ {
+ if ( element.name in { p : 1, div : 1 } )
+ {
+ var indentStyleName = config.contentsLangDirection == 'ltr' ?
+ 'margin-left' : 'margin-right';
+
+ // Extract component value from 'margin' shorthand.
+ if ( name == 'margin' )
+ {
+ value = getStyleComponents( name, value,
+ [ indentStyleName ] )[ indentStyleName ];
+ }
+ else if ( name != indentStyleName )
+ return null;
+
+ if ( value && !emptyMarginRegex.test( value ) )
+ return [ indentStyleName, value ];
+ }
+
+ return null;
+ } ],
+
+ // Preserve clear float style.
+ [ ( /^clear$/ ) ],
+
+ [ ( /^border.*|margin.*|vertical-align|float$/ ), null,
+ function( value, element )
+ {
+ if ( element.name == 'img' )
+ return value;
+ } ],
+
+ [ (/^width|height$/ ), null,
+ function( value, element )
+ {
+ if ( element.name in { table : 1, td : 1, th : 1, img : 1 } )
+ return value;
+ } ]
+ ] :
+ // Otherwise provide a black-list of styles that we remove.
+ [
+ [ ( /^mso-/ ) ],
+ // Fixing color values.
+ [ ( /-color$/ ), null, function( value )
+ {
+ if ( value == 'transparent' )
+ return false;
+ if ( CKEDITOR.env.gecko )
+ return value.replace( /-moz-use-text-color/g, 'transparent' );
+ } ],
+ // Remove empty margin values, e.g. 0.00001pt 0em 0pt
+ [ ( /^margin$/ ), emptyMarginRegex ],
+ [ 'text-indent', '0cm' ],
+ [ 'page-break-before' ],
+ [ 'tab-stops' ],
+ [ 'display', 'none' ],
+ removeFontStyles ? [ ( /font-?/ ) ] : null
+ ], removeStyles ),
+
+ // Prefer width styles over 'width' attributes.
+ 'width' : function( value, element )
+ {
+ if ( element.name in dtd.$tableContent )
+ return false;
+ },
+ // Prefer border styles over table 'border' attributes.
+ 'border' : function( value, element )
+ {
+ if ( element.name in dtd.$tableContent )
+ return false;
+ },
+
+ // Only Firefox carry style sheet from MS-Word, which
+ // will be applied by us manually. For other browsers
+ // the css className is useless.
+ 'class' : falsyFilter,
+
+ // MS-Word always generate 'background-color' along with 'bgcolor',
+ // simply drop the deprecated attributes.
+ 'bgcolor' : falsyFilter,
+
+ // Deprecate 'valign' attribute in favor of 'vertical-align'.
+ 'valign' : removeStyles ? falsyFilter : function( value, element )
+ {
+ element.addStyle( 'vertical-align', value );
+ return false;
+ }
+ },
+
+ // Fore none-IE, some useful data might be buried under these IE-conditional
+ // comments where RegExp were the right approach to dig them out where usual approach
+ // is transform it into a fake element node which hold the desired data.
+ comment :
+ !CKEDITOR.env.ie ?
+ function( value, node )
+ {
+ var imageInfo = value.match( // ),
+ listInfo = value.match( /^\[if !supportLists\]([\s\S]*?)\[endif\]$/ );
+
+ // Seek for list bullet indicator.
+ if ( listInfo )
+ {
+ // Bullet symbol could be either text or an image.
+ var listSymbol = listInfo[ 1 ] || ( imageInfo && 'l.' ),
+ listType = listSymbol && listSymbol.match( />(?:[(]?)([^\s]+?)([.)]?) );
+ return createListBulletMarker( listType, listSymbol );
+ }
+
+ // Reveal the element in conditional comments for Firefox.
+ if ( CKEDITOR.env.gecko && imageInfo )
+ {
+ var img = CKEDITOR.htmlParser.fragment.fromHtml( imageInfo[ 0 ] ).children[ 0 ],
+ previousComment = node.previous,
+ // Try to dig the real image link from vml markup from previous comment text.
+ imgSrcInfo = previousComment && previousComment.value.match( /]*o:href=['"](.*?)['"]/ ),
+ imgSrc = imgSrcInfo && imgSrcInfo[ 1 ];
+
+ // Is there a real 'src' url to be used?
+ imgSrc && ( img.attributes.src = imgSrc );
+ return img;
+ }
+
+ return false;
+ }
+ : falsyFilter
+ };
+ }
+ });
+
+ // The paste processor here is just a reduced copy of html data processor.
+ var pasteProcessor = function()
+ {
+ this.dataFilter = new CKEDITOR.htmlParser.filter();
+ };
+
+ pasteProcessor.prototype =
+ {
+ toHtml : function( data )
+ {
+ var fragment = CKEDITOR.htmlParser.fragment.fromHtml( data, false ),
+ writer = new CKEDITOR.htmlParser.basicWriter();
+
+ fragment.writeHtml( writer, this.dataFilter );
+ return writer.getHtml( true );
+ }
+ };
+
+ CKEDITOR.cleanWord = function( data, editor )
+ {
+ // Firefox will be confused by those downlevel-revealed IE conditional
+ // comments, fixing them first( convert it to upperlevel-revealed one ).
+ // e.g. ...
+ if ( CKEDITOR.env.gecko )
+ data = data.replace( /(([\S\s]*?))/gi, '$1$2$3' );
+
+ var dataProcessor = new pasteProcessor(),
+ dataFilter = dataProcessor.dataFilter;
+
+ // These rules will have higher priorities than default ones.
+ dataFilter.addRules( CKEDITOR.plugins.pastefromword.getRules( editor ) );
+
+ // Allow extending data filter rules.
+ editor.fire( 'beforeCleanWord', { filter : dataFilter } );
+
+ try
+ {
+ data = dataProcessor.toHtml( data, false );
+ }
+ catch ( e )
+ {
+ alert( editor.lang.pastefromword.error );
+ }
+
+ /* Below post processing those things that are unable to delivered by filter rules. */
+
+ // Remove 'cke' namespaced attribute used in filter rules as marker.
+ data = data.replace( /cke:.*?".*?"/g, '' );
+
+ // Remove empty style attribute.
+ data = data.replace( /style=""/g, '' );
+
+ // Remove the dummy spans ( having no inline style ).
+ data = data.replace( //g, '' );
+
+ return data;
+ };
+})();
+
+/**
+ * Whether to ignore all font related formatting styles, including:
+ * font size;
+ * font family;
+ * font foreground/background color.
+ * @name CKEDITOR.config.pasteFromWordRemoveFontStyles
+ * @since 3.1
+ * @type Boolean
+ * @default true
+ * @example
+ * config.pasteFromWordRemoveFontStyles = false;
+ */
+
+/**
+ * Whether to transform MS Word outline numbered headings into lists.
+ * @name CKEDITOR.config.pasteFromWordNumberedHeadingToList
+ * @since 3.1
+ * @type Boolean
+ * @default false
+ * @example
+ * config.pasteFromWordNumberedHeadingToList = true;
+ */
+
+/**
+ * Whether to remove element styles that can't be managed with the editor. Note
+ * that this doesn't handle the font specific styles, which depends on the
+ * {@link CKEDITOR.config.pasteFromWordRemoveFontStyles} setting instead.
+ * @name CKEDITOR.config.pasteFromWordRemoveStyles
+ * @since 3.1
+ * @type Boolean
+ * @default true
+ * @example
+ * config.pasteFromWordRemoveStyles = false;
+ */
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/pastefromword/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/pastefromword/plugin.js
new file mode 100644
index 00000000..15853b41
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/pastefromword/plugin.js
@@ -0,0 +1,135 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+(function()
+{
+ CKEDITOR.plugins.add( 'pastefromword',
+ {
+ init : function( editor )
+ {
+
+ // Flag indicate this command is actually been asked instead of a generic
+ // pasting.
+ var forceFromWord = 0;
+ var resetFromWord = function( evt )
+ {
+ evt && evt.removeListener();
+ forceFromWord && setTimeout( function() { forceFromWord = 0; }, 0 );
+ };
+
+ // Features bring by this command beside the normal process:
+ // 1. No more bothering of user about the clean-up.
+ // 2. Perform the clean-up even if content is not from MS-Word.
+ // (e.g. from a MS-Word similar application.)
+ editor.addCommand( 'pastefromword',
+ {
+ canUndo : false,
+ exec : function()
+ {
+ forceFromWord = 1;
+ if ( editor.execCommand( 'paste' ) === false )
+ {
+ editor.on( 'dialogShow', function ( evt )
+ {
+ evt.removeListener();
+ evt.data.on( 'cancel', resetFromWord );
+ });
+
+ editor.on( 'dialogHide', function( evt )
+ {
+ evt.data.removeListener( 'cancel', resetFromWord );
+ } );
+ }
+
+ editor.on( 'afterPaste', resetFromWord );
+ }
+ });
+
+ // Register the toolbar button.
+ editor.ui.addButton( 'PasteFromWord',
+ {
+ label : editor.lang.pastefromword.toolbar,
+ command : 'pastefromword'
+ });
+
+ editor.on( 'pasteState', function( evt )
+ {
+ editor.getCommand( 'pastefromword' ).setState( evt.data );
+ });
+
+ editor.on( 'paste', function( evt )
+ {
+ var data = evt.data,
+ mswordHtml;
+
+ // MS-WORD format sniffing.
+ if ( ( mswordHtml = data[ 'html' ] )
+ && ( forceFromWord || ( /(class=\"?Mso|style=\"[^\"]*\bmso\-|w:WordDocument)/ ).test( mswordHtml ) ) )
+ {
+ var isLazyLoad = this.loadFilterRules( function()
+ {
+ // Event continuation with the original data.
+ if ( isLazyLoad )
+ editor.fire( 'paste', data );
+ else if ( !editor.config.pasteFromWordPromptCleanup
+ || ( forceFromWord || confirm( editor.lang.pastefromword.confirmCleanup ) ) )
+ {
+ data[ 'html' ] = CKEDITOR.cleanWord( mswordHtml, editor );
+ }
+ });
+
+ // The cleanup rules are to be loaded, we should just cancel
+ // this event.
+ isLazyLoad && evt.cancel();
+ }
+ }, this );
+ },
+
+ loadFilterRules : function( callback )
+ {
+
+ var isLoaded = CKEDITOR.cleanWord;
+
+ if ( isLoaded )
+ callback();
+ else
+ {
+ var filterFilePath = CKEDITOR.getUrl(
+ CKEDITOR.config.pasteFromWordCleanupFile
+ || ( this.path + 'filter/default.js' ) );
+
+ // Load with busy indicator.
+ CKEDITOR.scriptLoader.load( filterFilePath, callback, null, true );
+ }
+
+ return !isLoaded;
+ },
+
+ requires : [ 'clipboard' ]
+ });
+})();
+
+/**
+ * Whether to prompt the user about the clean up of content being pasted from
+ * MS Word.
+ * @name CKEDITOR.config.pasteFromWordPromptCleanup
+ * @since 3.1
+ * @type Boolean
+ * @default undefined
+ * @example
+ * config.pasteFromWordPromptCleanup = true;
+ */
+
+/**
+ * The file that provides the MS Word cleanup function for pasting operations.
+ * Note: This is a global configuration shared by all editor instances present
+ * in the page.
+ * @name CKEDITOR.config.pasteFromWordCleanupFile
+ * @since 3.1
+ * @type String
+ * @default 'default'
+ * @example
+ * // Load from 'pastefromword' plugin 'filter' sub folder (custom.js file).
+ * CKEDITOR.config.pasteFromWordCleanupFile = 'custom';
+ */
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/pastetext/dialogs/pastetext.js b/app/assets/javascripts/ckeditor/_source/plugins/pastetext/dialogs/pastetext.js
new file mode 100644
index 00000000..42a021fe
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/pastetext/dialogs/pastetext.js
@@ -0,0 +1,70 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+(function()
+{
+ CKEDITOR.dialog.add( 'pastetext', function( editor )
+ {
+ return {
+ title : editor.lang.pasteText.title,
+
+ minWidth : CKEDITOR.env.ie && CKEDITOR.env.quirks ? 368 : 350,
+ minHeight : 240,
+
+ onShow : function()
+ {
+ // Reset the textarea value.
+ this.getContentElement( 'general', 'content' ).getInputElement().setValue( '' );
+ },
+
+ onOk : function()
+ {
+ // Get the textarea value.
+ var text = this.getContentElement( 'general', 'content' ).getInputElement().getValue(),
+ editor = this.getParentEditor();
+
+ setTimeout( function()
+ {
+ editor.fire( 'paste', { 'text' : text } );
+ }, 0 );
+ },
+
+ contents :
+ [
+ {
+ label : editor.lang.common.generalTab,
+ id : 'general',
+ elements :
+ [
+ {
+ type : 'html',
+ id : 'pasteMsg',
+ html : '' + editor.lang.clipboard.pasteMsg + '
'
+ },
+ {
+ type : 'textarea',
+ id : 'content',
+ className : 'cke_pastetext',
+
+ onLoad : function()
+ {
+ var label = this.getDialog().getContentElement( 'general', 'pasteMsg' ).getElement(),
+ input = this.getElement().getElementsByTag( 'textarea' ).getItem( 0 );
+
+ input.setAttribute( 'aria-labelledby', label.$.id );
+ input.setStyle( 'direction', editor.config.contentsLangDirection );
+ },
+
+ focus : function()
+ {
+ this.getElement().focus();
+ }
+ }
+ ]
+ }
+ ]
+ };
+ });
+})();
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/pastetext/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/pastetext/plugin.js
new file mode 100644
index 00000000..a523549a
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/pastetext/plugin.js
@@ -0,0 +1,90 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+/**
+ * @file Paste as plain text plugin
+ */
+
+(function()
+{
+ // The pastetext command definition.
+ var pasteTextCmd =
+ {
+ exec : function( editor )
+ {
+ var clipboardText = CKEDITOR.tools.tryThese(
+ function()
+ {
+ var clipboardText = window.clipboardData.getData( 'Text' );
+ if ( !clipboardText )
+ throw 0;
+ return clipboardText;
+ }
+ // Any other approach that's working...
+ );
+
+ if ( !clipboardText ) // Clipboard access privilege is not granted.
+ {
+ editor.openDialog( 'pastetext' );
+ return false;
+ }
+ else
+ editor.fire( 'paste', { 'text' : clipboardText } );
+
+ return true;
+ }
+ };
+
+ // Register the plugin.
+ CKEDITOR.plugins.add( 'pastetext',
+ {
+ init : function( editor )
+ {
+ var commandName = 'pastetext',
+ command = editor.addCommand( commandName, pasteTextCmd );
+
+ editor.ui.addButton( 'PasteText',
+ {
+ label : editor.lang.pasteText.button,
+ command : commandName
+ });
+
+ CKEDITOR.dialog.add( commandName, CKEDITOR.getUrl( this.path + 'dialogs/pastetext.js' ) );
+
+ if ( editor.config.forcePasteAsPlainText )
+ {
+ // Intercept the default pasting process.
+ editor.on( 'beforeCommandExec', function ( evt )
+ {
+ if ( evt.data.name == 'paste' )
+ {
+ editor.execCommand( 'pastetext' );
+ evt.cancel();
+ }
+ }, null, null, 0 );
+ }
+
+ editor.on( 'pasteState', function( evt )
+ {
+ editor.getCommand( 'pastetext' ).setState( evt.data );
+ });
+ },
+
+ requires : [ 'clipboard' ]
+ });
+
+})();
+
+
+/**
+ * Whether to force all pasting operations to insert on plain text into the
+ * editor, loosing any formatting information possibly available in the source
+ * text.
+ * @name CKEDITOR.config.forcePasteAsPlainText
+ * @type Boolean
+ * @default false
+ * @example
+ * config.forcePasteAsPlainText = true;
+ */
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/placeholder/dialogs/placeholder.js b/app/assets/javascripts/ckeditor/_source/plugins/placeholder/dialogs/placeholder.js
new file mode 100644
index 00000000..d709b38e
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/placeholder/dialogs/placeholder.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.html or http://ckeditor.com/license
+ */
+
+(function()
+{
+ function placeholderDialog( editor, isEdit )
+ {
+
+ var lang = editor.lang.placeholder,
+ generalLabel = editor.lang.common.generalTab;
+ return {
+ title : lang.title,
+ minWidth : 300,
+ minHeight : 80,
+ contents :
+ [
+ {
+ id : 'info',
+ label : generalLabel,
+ title : generalLabel,
+ elements :
+ [
+ {
+ id : 'text',
+ type : 'text',
+ style : 'width: 100%;',
+ label : lang.text,
+ 'default' : '',
+ required : true,
+ validate : CKEDITOR.dialog.validate.notEmpty( lang.textMissing ),
+ setup : function( element )
+ {
+ if ( isEdit )
+ this.setValue( element.getText().slice( 2, -2 ) );
+ },
+ commit : function( element )
+ {
+ var text = '[[' + this.getValue() + ']]';
+ // The placeholder must be recreated.
+ CKEDITOR.plugins.placeholder.createPlaceholder( editor, element, text );
+ }
+ }
+ ]
+ }
+ ],
+ onShow : function()
+ {
+ if ( isEdit )
+ this._element = CKEDITOR.plugins.placeholder.getSelectedPlaceHoder( editor );
+
+ this.setupContent( this._element );
+ },
+ onOk : function()
+ {
+ this.commitContent( this._element );
+ delete this._element;
+ }
+ };
+ }
+
+ CKEDITOR.dialog.add( 'createplaceholder', function( editor )
+ {
+ return placeholderDialog( editor );
+ });
+ CKEDITOR.dialog.add( 'editplaceholder', function( editor )
+ {
+ return placeholderDialog( editor, 1 );
+ });
+} )();
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/placeholder/lang/en.js b/app/assets/javascripts/ckeditor/_source/plugins/placeholder/lang/en.js
new file mode 100644
index 00000000..3cff992b
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/placeholder/lang/en.js
@@ -0,0 +1,16 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.plugins.setLang( 'placeholder', 'en',
+{
+ placeholder :
+ {
+ title : 'Placeholder Properties',
+ toolbar : 'Create Placeholder',
+ text : 'Placeholder Text',
+ edit : 'Edit Placeholder',
+ textMissing : 'The placeholder must contain text.'
+ }
+});
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/placeholder/lang/he.js b/app/assets/javascripts/ckeditor/_source/plugins/placeholder/lang/he.js
new file mode 100644
index 00000000..14814e82
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/placeholder/lang/he.js
@@ -0,0 +1,16 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.plugins.setLang( 'placeholder', 'he',
+{
+ placeholder :
+ {
+ title : 'מאפייני שומר מקום',
+ toolbar : 'צור שומר מקום',
+ text : 'תוכן שומר המקום',
+ edit : 'ערוך שומר מקום',
+ textMissing : 'שומר המקום חייב להכיל טקסט.'
+ }
+});
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/placeholder/placeholder.gif b/app/assets/javascripts/ckeditor/_source/plugins/placeholder/placeholder.gif
new file mode 100644
index 00000000..c07078c1
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/placeholder/placeholder.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/placeholder/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/placeholder/plugin.js
new file mode 100644
index 00000000..503f08ac
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/placeholder/plugin.js
@@ -0,0 +1,171 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+/**
+ * @fileOverview The "placeholder" plugin.
+ *
+ */
+
+(function()
+{
+ var placeholderReplaceRegex = /\[\[[^\]]+\]\]/g;
+ CKEDITOR.plugins.add( 'placeholder',
+ {
+ requires : [ 'dialog' ],
+ lang : [ 'en', 'he' ],
+ init : function( editor )
+ {
+ var lang = editor.lang.placeholder;
+
+ editor.addCommand( 'createplaceholder', new CKEDITOR.dialogCommand( 'createplaceholder' ) );
+ editor.addCommand( 'editplaceholder', new CKEDITOR.dialogCommand( 'editplaceholder' ) );
+
+ editor.ui.addButton( 'CreatePlaceholder',
+ {
+ label : lang.toolbar,
+ command :'createplaceholder',
+ icon : this.path + 'placeholder.gif'
+ });
+
+ if ( editor.addMenuItems )
+ {
+ editor.addMenuGroup( 'placeholder', 20 );
+ editor.addMenuItems(
+ {
+ editplaceholder :
+ {
+ label : lang.edit,
+ command : 'editplaceholder',
+ group : 'placeholder',
+ order : 1,
+ icon : this.path + 'placeholder.gif'
+ }
+ } );
+
+ if ( editor.contextMenu )
+ {
+ editor.contextMenu.addListener( function( element, selection )
+ {
+ if ( !element || !element.data( 'cke-placeholder' ) )
+ return null;
+
+ return { editplaceholder : CKEDITOR.TRISTATE_OFF };
+ } );
+ }
+ }
+
+ editor.on( 'doubleclick', function( evt )
+ {
+ if ( CKEDITOR.plugins.placeholder.getSelectedPlaceHoder( editor ) )
+ evt.data.dialog = 'editplaceholder';
+ });
+
+ editor.addCss(
+ '.cke_placeholder' +
+ '{' +
+ 'background-color: #ffff00;' +
+ ( CKEDITOR.env.gecko ? 'cursor: default;' : '' ) +
+ '}'
+ );
+
+ editor.on( 'contentDom', function()
+ {
+ editor.document.getBody().on( 'resizestart', function( evt )
+ {
+ if ( editor.getSelection().getSelectedElement().data( 'cke-placeholder' ) )
+ evt.data.preventDefault();
+ });
+ });
+
+ CKEDITOR.dialog.add( 'createplaceholder', this.path + 'dialogs/placeholder.js' );
+ CKEDITOR.dialog.add( 'editplaceholder', this.path + 'dialogs/placeholder.js' );
+ },
+ afterInit : function( editor )
+ {
+ var dataProcessor = editor.dataProcessor,
+ dataFilter = dataProcessor && dataProcessor.dataFilter,
+ htmlFilter = dataProcessor && dataProcessor.htmlFilter;
+
+ if ( dataFilter )
+ {
+ dataFilter.addRules(
+ {
+ text : function( text )
+ {
+ return text.replace( placeholderReplaceRegex, function( match )
+ {
+ return CKEDITOR.plugins.placeholder.createPlaceholder( editor, null, match, 1 );
+ });
+ }
+ });
+ }
+
+ if ( htmlFilter )
+ {
+ htmlFilter.addRules(
+ {
+ elements :
+ {
+ 'span' : function( element )
+ {
+ if ( element.attributes && element.attributes[ 'data-cke-placeholder' ] )
+ delete element.name;
+ }
+ }
+ });
+ }
+ }
+ });
+})();
+
+CKEDITOR.plugins.placeholder =
+{
+ createPlaceholder : function( editor, oldElement, text, isGet )
+ {
+ var element = new CKEDITOR.dom.element( 'span', editor.document );
+ element.setAttributes(
+ {
+ contentEditable : 'false',
+ 'data-cke-placeholder' : 1,
+ 'class' : 'cke_placeholder'
+ }
+ );
+
+ text && element.setText( text );
+
+ if ( isGet )
+ return element.getOuterHtml();
+
+ if ( oldElement )
+ {
+ if ( CKEDITOR.env.ie )
+ {
+ element.insertAfter( oldElement );
+ // Some time is required for IE before the element is removed.
+ setTimeout( function()
+ {
+ oldElement.remove();
+ element.focus();
+ }, 10 );
+ }
+ else
+ element.replace( oldElement );
+ }
+ else
+ editor.insertElement( element );
+
+ return null;
+ },
+
+ getSelectedPlaceHoder : function( editor )
+ {
+ var range = editor.getSelection().getRanges()[ 0 ];
+ range.shrink( CKEDITOR.SHRINK_TEXT );
+ var node = range.startContainer;
+ while( node && !( node.type == CKEDITOR.NODE_ELEMENT && node.data( 'cke-placeholder' ) ) )
+ node = node.getParent();
+ return node;
+ }
+};
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/popup/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/popup/plugin.js
new file mode 100644
index 00000000..a2e1f334
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/popup/plugin.js
@@ -0,0 +1,64 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.plugins.add( 'popup' );
+
+CKEDITOR.tools.extend( CKEDITOR.editor.prototype,
+{
+ /**
+ * Opens Browser in a popup. The "width" and "height" parameters accept
+ * numbers (pixels) or percent (of screen size) values.
+ * @param {String} url The url of the external file browser.
+ * @param {String} width Popup window width.
+ * @param {String} height Popup window height.
+ * @param {String} options Popup window features.
+ */
+ popup : function( url, width, height, options )
+ {
+ width = width || '80%';
+ height = height || '70%';
+
+ if ( typeof width == 'string' && width.length > 1 && width.substr( width.length - 1, 1 ) == '%' )
+ width = parseInt( window.screen.width * parseInt( width, 10 ) / 100, 10 );
+
+ if ( typeof height == 'string' && height.length > 1 && height.substr( height.length - 1, 1 ) == '%' )
+ height = parseInt( window.screen.height * parseInt( height, 10 ) / 100, 10 );
+
+ if ( width < 640 )
+ width = 640;
+
+ if ( height < 420 )
+ height = 420;
+
+ var top = parseInt( ( window.screen.height - height ) / 2, 10 ),
+ left = parseInt( ( window.screen.width - width ) / 2, 10 );
+
+ options = ( options || 'location=no,menubar=no,toolbar=no,dependent=yes,minimizable=no,modal=yes,alwaysRaised=yes,resizable=yes,scrollbars=yes' ) +
+ ',width=' + width +
+ ',height=' + height +
+ ',top=' + top +
+ ',left=' + left;
+
+ var popupWindow = window.open( '', null, options, true );
+
+ // Blocked by a popup blocker.
+ if ( !popupWindow )
+ return false;
+
+ try
+ {
+ popupWindow.moveTo( left, top );
+ popupWindow.resizeTo( width, height );
+ popupWindow.focus();
+ popupWindow.location.href = url;
+ }
+ catch ( e )
+ {
+ popupWindow = window.open( url, null, options, true );
+ }
+
+ return true;
+ }
+});
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/preview/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/preview/plugin.js
new file mode 100644
index 00000000..12a1683e
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/preview/plugin.js
@@ -0,0 +1,108 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+/**
+ * @file Preview plugin.
+ */
+
+(function()
+{
+ var previewCmd =
+ {
+ modes : { wysiwyg:1, source:1 },
+ canUndo : false,
+ exec : function( editor )
+ {
+ var sHTML,
+ config = editor.config,
+ baseTag = config.baseHref ? ' ' : '',
+ isCustomDomain = CKEDITOR.env.isCustomDomain();
+
+ if ( config.fullPage )
+ {
+ sHTML = editor.getData()
+ .replace( //, '$&' + baseTag )
+ .replace( /[^>]*(?=<\/title>)/, '$& — ' + editor.lang.preview );
+ }
+ else
+ {
+ var bodyHtml = '';
+
+ sHTML =
+ editor.config.docType +
+ '' +
+ '' +
+ baseTag +
+ '' + editor.lang.preview + ' ' +
+ CKEDITOR.tools.buildStyleHtml( editor.config.contentsCss ) +
+ '' + bodyHtml +
+ editor.getData() +
+ '';
+ }
+
+ var iWidth = 640, // 800 * 0.8,
+ iHeight = 420, // 600 * 0.7,
+ iLeft = 80; // (800 - 0.8 * 800) /2 = 800 * 0.1.
+ try
+ {
+ var screen = window.screen;
+ iWidth = Math.round( screen.width * 0.8 );
+ iHeight = Math.round( screen.height * 0.7 );
+ iLeft = Math.round( screen.width * 0.1 );
+ }
+ catch ( e ){}
+
+ var sOpenUrl = '';
+ if ( isCustomDomain )
+ {
+ window._cke_htmlToLoad = sHTML;
+ sOpenUrl = 'javascript:void( (function(){' +
+ 'document.open();' +
+ 'document.domain="' + document.domain + '";' +
+ 'document.write( window.opener._cke_htmlToLoad );' +
+ 'document.close();' +
+ 'window.opener._cke_htmlToLoad = null;' +
+ '})() )';
+ }
+
+ var oWindow = window.open( sOpenUrl, null, 'toolbar=yes,location=no,status=yes,menubar=yes,scrollbars=yes,resizable=yes,width=' +
+ iWidth + ',height=' + iHeight + ',left=' + iLeft );
+
+ if ( !isCustomDomain )
+ {
+ oWindow.document.open();
+ oWindow.document.write( sHTML );
+ oWindow.document.close();
+ }
+ }
+ };
+
+ var pluginName = 'preview';
+
+ // Register a plugin named "preview".
+ CKEDITOR.plugins.add( pluginName,
+ {
+ init : function( editor )
+ {
+ editor.addCommand( pluginName, previewCmd );
+ editor.ui.addButton( 'Preview',
+ {
+ label : editor.lang.preview,
+ command : pluginName
+ });
+ }
+ });
+})();
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/print/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/print/plugin.js
new file mode 100644
index 00000000..dc7cc389
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/print/plugin.js
@@ -0,0 +1,41 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+/**
+ * @file Print Plugin
+ */
+
+CKEDITOR.plugins.add( 'print',
+{
+ init : function( editor )
+ {
+ var pluginName = 'print';
+
+ // Register the command.
+ var command = editor.addCommand( pluginName, CKEDITOR.plugins.print );
+
+ // Register the toolbar button.
+ editor.ui.addButton( 'Print',
+ {
+ label : editor.lang.print,
+ command : pluginName
+ });
+ }
+} );
+
+CKEDITOR.plugins.print =
+{
+ exec : function( editor )
+ {
+ if ( CKEDITOR.env.opera )
+ return;
+ else if ( CKEDITOR.env.gecko )
+ editor.window.$.print();
+ else
+ editor.document.$.execCommand( "Print" );
+ },
+ canUndo : false,
+ modes : { wysiwyg : !( CKEDITOR.env.opera ) } // It is imposible to print the inner document in Opera.
+};
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/removeformat/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/removeformat/plugin.js
new file mode 100644
index 00000000..066fce6c
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/removeformat/plugin.js
@@ -0,0 +1,185 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.plugins.add( 'removeformat',
+{
+ requires : [ 'selection' ],
+
+ init : function( editor )
+ {
+ editor.addCommand( 'removeFormat', CKEDITOR.plugins.removeformat.commands.removeformat );
+ editor.ui.addButton( 'RemoveFormat',
+ {
+ label : editor.lang.removeFormat,
+ command : 'removeFormat'
+ });
+
+ editor._.removeFormat = { filters: [] };
+ }
+});
+
+CKEDITOR.plugins.removeformat =
+{
+ commands :
+ {
+ removeformat :
+ {
+ exec : function( editor )
+ {
+ var tagsRegex = editor._.removeFormatRegex ||
+ ( editor._.removeFormatRegex = new RegExp( '^(?:' + editor.config.removeFormatTags.replace( /,/g,'|' ) + ')$', 'i' ) );
+
+ var removeAttributes = editor._.removeAttributes ||
+ ( editor._.removeAttributes = editor.config.removeFormatAttributes.split( ',' ) );
+
+ var filter = CKEDITOR.plugins.removeformat.filter;
+ var ranges = editor.getSelection().getRanges( 1 ),
+ iterator = ranges.createIterator(),
+ range;
+
+ while ( ( range = iterator.getNextRange() ) )
+ {
+ if ( ! range.collapsed )
+ range.enlarge( CKEDITOR.ENLARGE_ELEMENT );
+
+ // Bookmark the range so we can re-select it after processing.
+ var bookmark = range.createBookmark(),
+ // The style will be applied within the bookmark boundaries.
+ startNode = bookmark.startNode,
+ endNode = bookmark.endNode,
+ currentNode;
+
+ // We need to check the selection boundaries (bookmark spans) to break
+ // the code in a way that we can properly remove partially selected nodes.
+ // For example, removing a style from
+ // This is [some text to show the] problem
+ // ... where [ and ] represent the selection, must result:
+ // This is [some text to show the] problem
+ // The strategy is simple, we just break the partial nodes before the
+ // removal logic, having something that could be represented this way:
+ // This is [some text to show the ] problem
+
+ var breakParent = function( node )
+ {
+ // Let's start checking the start boundary.
+ var path = new CKEDITOR.dom.elementPath( node ),
+ pathElements = path.elements;
+
+ for ( var i = 1, pathElement ; pathElement = pathElements[ i ] ; i++ )
+ {
+ if ( pathElement.equals( path.block ) || pathElement.equals( path.blockLimit ) )
+ break;
+
+ // If this element can be removed (even partially).
+ if ( tagsRegex.test( pathElement.getName() ) && filter( editor, pathElement ) )
+ node.breakParent( pathElement );
+ }
+ };
+
+ breakParent( startNode );
+ if ( endNode )
+ {
+ breakParent( endNode );
+
+ // Navigate through all nodes between the bookmarks.
+ currentNode = startNode.getNextSourceNode( true, CKEDITOR.NODE_ELEMENT );
+
+ while ( currentNode )
+ {
+ // If we have reached the end of the selection, stop looping.
+ if ( currentNode.equals( endNode ) )
+ break;
+
+ // Cache the next node to be processed. Do it now, because
+ // currentNode may be removed.
+ var nextNode = currentNode.getNextSourceNode( false, CKEDITOR.NODE_ELEMENT );
+
+ // This node must not be a fake element.
+ if ( !( currentNode.getName() == 'img'
+ && currentNode.data( 'cke-realelement' ) )
+ && filter( editor, currentNode ) )
+ {
+ // Remove elements nodes that match with this style rules.
+ if ( tagsRegex.test( currentNode.getName() ) )
+ currentNode.remove( 1 );
+ else
+ {
+ currentNode.removeAttributes( removeAttributes );
+ editor.fire( 'removeFormatCleanup', currentNode );
+ }
+ }
+
+ currentNode = nextNode;
+ }
+ }
+
+ range.moveToBookmark( bookmark );
+ }
+
+ editor.getSelection().selectRanges( ranges );
+ }
+ }
+ },
+
+ /**
+ * Perform the remove format filters on the passed element.
+ * @param {CKEDITOR.editor} editor
+ * @param {CKEDITOR.dom.element} element
+ */
+ filter : function ( editor, element )
+ {
+ var filters = editor._.removeFormat.filters;
+ for ( var i = 0; i < filters.length; i++ )
+ {
+ if ( filters[ i ]( element ) === false )
+ return false;
+ }
+ return true;
+ }
+};
+
+/**
+ * Add to a collection of functions to decide whether a specific
+ * element should be considered as formatting element and thus
+ * could be removed during removeFormat command,
+ * Note: Only available with the existence of 'removeformat' plugin.
+ * @since 3.3
+ * @param {Function} func The function to be called, which will be passed a {CKEDITOR.dom.element} element to test.
+ * @example
+ * // Don't remove empty span
+ * editor.addRemoveFormatFilter.push( function( element )
+ * {
+ * return !( element.is( 'span' ) && CKEDITOR.tools.isEmpty( element.getAttributes() ) );
+ * });
+ */
+CKEDITOR.editor.prototype.addRemoveFormatFilter = function( func )
+{
+ this._.removeFormat.filters.push( func );
+};
+
+/**
+ * A comma separated list of elements to be removed when executing the "remove
+ " format" command. Note that only inline elements are allowed.
+ * @type String
+ * @default 'b,big,code,del,dfn,em,font,i,ins,kbd,q,samp,small,span,strike,strong,sub,sup,tt,u,var'
+ * @example
+ */
+CKEDITOR.config.removeFormatTags = 'b,big,code,del,dfn,em,font,i,ins,kbd,q,samp,small,span,strike,strong,sub,sup,tt,u,var';
+
+/**
+ * A comma separated list of elements attributes to be removed when executing
+ * the "remove format" command.
+ * @type String
+ * @default 'class,style,lang,width,height,align,hspace,valign'
+ * @example
+ */
+CKEDITOR.config.removeFormatAttributes = 'class,style,lang,width,height,align,hspace,valign';
+
+/**
+ * Fired after an element was cleaned by the removeFormat plugin.
+ * @name CKEDITOR.editor#removeFormatCleanup
+ * @event
+ * @param {Object} data.element The element that was cleaned up.
+ */
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/resize/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/resize/plugin.js
new file mode 100644
index 00000000..24ba09ca
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/resize/plugin.js
@@ -0,0 +1,168 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.plugins.add( 'resize',
+{
+ init : function( editor )
+ {
+ var config = editor.config;
+
+ // Resize in the same direction of chrome,
+ // which is identical to dir of editor element. (#6614)
+ var resizeDir = editor.element.getDirection( 1 );
+
+ !config.resize_dir && ( config.resize_dir = 'both' );
+ ( config.resize_maxWidth == undefined ) && ( config.resize_maxWidth = 3000 );
+ ( config.resize_maxHeight == undefined ) && ( config.resize_maxHeight = 3000 );
+ ( config.resize_minWidth == undefined ) && ( config.resize_minWidth = 750 );
+ ( config.resize_minHeight == undefined ) && ( config.resize_minHeight = 250 );
+
+ if ( config.resize_enabled !== false )
+ {
+ var container = null,
+ origin,
+ startSize,
+ resizeHorizontal = ( config.resize_dir == 'both' || config.resize_dir == 'horizontal' ) &&
+ ( config.resize_minWidth != config.resize_maxWidth ),
+ resizeVertical = ( config.resize_dir == 'both' || config.resize_dir == 'vertical' ) &&
+ ( config.resize_minHeight != config.resize_maxHeight );
+
+ function dragHandler( evt )
+ {
+ var dx = evt.data.$.screenX - origin.x,
+ dy = evt.data.$.screenY - origin.y,
+ width = startSize.width,
+ height = startSize.height,
+ internalWidth = width + dx * ( resizeDir == 'rtl' ? -1 : 1 ),
+ internalHeight = height + dy;
+
+ if ( resizeHorizontal )
+ width = Math.max( config.resize_minWidth, Math.min( internalWidth, config.resize_maxWidth ) );
+
+ if ( resizeVertical )
+ height = Math.max( config.resize_minHeight, Math.min( internalHeight, config.resize_maxHeight ) );
+
+ editor.resize( width, height );
+ }
+
+ function dragEndHandler ( evt )
+ {
+ CKEDITOR.document.removeListener( 'mousemove', dragHandler );
+ CKEDITOR.document.removeListener( 'mouseup', dragEndHandler );
+
+ if ( editor.document )
+ {
+ editor.document.removeListener( 'mousemove', dragHandler );
+ editor.document.removeListener( 'mouseup', dragEndHandler );
+ }
+ }
+
+ var mouseDownFn = CKEDITOR.tools.addFunction( function( $event )
+ {
+ if ( !container )
+ container = editor.getResizable();
+
+ startSize = { width : container.$.offsetWidth || 0, height : container.$.offsetHeight || 0 };
+ origin = { x : $event.screenX, y : $event.screenY };
+
+ config.resize_minWidth > startSize.width && ( config.resize_minWidth = startSize.width );
+ config.resize_minHeight > startSize.height && ( config.resize_minHeight = startSize.height );
+
+ CKEDITOR.document.on( 'mousemove', dragHandler );
+ CKEDITOR.document.on( 'mouseup', dragEndHandler );
+
+ if ( editor.document )
+ {
+ editor.document.on( 'mousemove', dragHandler );
+ editor.document.on( 'mouseup', dragEndHandler );
+ }
+ });
+
+ editor.on( 'destroy', function() { CKEDITOR.tools.removeFunction( mouseDownFn ); } );
+
+ editor.on( 'themeSpace', function( event )
+ {
+ if ( event.data.space == 'bottom' )
+ {
+ var direction = '';
+ if ( resizeHorizontal && !resizeVertical )
+ direction = ' cke_resizer_horizontal';
+ if ( !resizeHorizontal && resizeVertical )
+ direction = ' cke_resizer_vertical';
+
+ var resizerHtml =
+ '
';
+
+ // Always sticks the corner of botttom space.
+ resizeDir == 'ltr' && direction == 'ltr' ?
+ event.data.html += resizerHtml :
+ event.data.html = resizerHtml + event.data.html;
+ }
+ }, editor, null, 100 );
+ }
+ }
+} );
+
+/**
+ * The minimum editor width, in pixels, when resizing it with the resize handle.
+ * Note: It fallbacks to editor's actual width if that's smaller than the default value.
+ * @name CKEDITOR.config.resize_minWidth
+ * @type Number
+ * @default 750
+ * @example
+ * config.resize_minWidth = 500;
+ */
+
+/**
+ * The minimum editor height, in pixels, when resizing it with the resize handle.
+ * Note: It fallbacks to editor's actual height if that's smaller than the default value.
+ * @name CKEDITOR.config.resize_minHeight
+ * @type Number
+ * @default 250
+ * @example
+ * config.resize_minHeight = 600;
+ */
+
+/**
+ * The maximum editor width, in pixels, when resizing it with the resize handle.
+ * @name CKEDITOR.config.resize_maxWidth
+ * @type Number
+ * @default 3000
+ * @example
+ * config.resize_maxWidth = 750;
+ */
+
+/**
+ * The maximum editor height, in pixels, when resizing it with the resize handle.
+ * @name CKEDITOR.config.resize_maxHeight
+ * @type Number
+ * @default 3000
+ * @example
+ * config.resize_maxHeight = 600;
+ */
+
+/**
+ * Whether to enable the resizing feature. If disabled the resize handler will not be visible.
+ * @name CKEDITOR.config.resize_enabled
+ * @type Boolean
+ * @default true
+ * @example
+ * config.resize_enabled = false;
+ */
+
+/**
+ * The directions to which the editor resizing is enabled. Possible values
+ * are "both", "vertical" and "horizontal".
+ * @name CKEDITOR.config.resize_dir
+ * @type String
+ * @default 'both'
+ * @since 3.3
+ * @example
+ * config.resize_dir = 'vertical';
+ */
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/richcombo/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/richcombo/plugin.js
new file mode 100644
index 00000000..8974fca0
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/richcombo/plugin.js
@@ -0,0 +1,374 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.plugins.add( 'richcombo',
+{
+ requires : [ 'floatpanel', 'listblock', 'button' ],
+
+ beforeInit : function( editor )
+ {
+ editor.ui.addHandler( CKEDITOR.UI_RICHCOMBO, CKEDITOR.ui.richCombo.handler );
+ }
+});
+
+/**
+ * Button UI element.
+ * @constant
+ * @example
+ */
+CKEDITOR.UI_RICHCOMBO = 3;
+
+CKEDITOR.ui.richCombo = CKEDITOR.tools.createClass(
+{
+ $ : function( definition )
+ {
+ // Copy all definition properties to this object.
+ CKEDITOR.tools.extend( this, definition,
+ // Set defaults.
+ {
+ title : definition.label,
+ modes : { wysiwyg : 1 }
+ });
+
+ // We don't want the panel definition in this object.
+ var panelDefinition = this.panel || {};
+ delete this.panel;
+
+ this.id = CKEDITOR.tools.getNextNumber();
+
+ this.document = ( panelDefinition
+ && panelDefinition.parent
+ && panelDefinition.parent.getDocument() )
+ || CKEDITOR.document;
+
+ panelDefinition.className = ( panelDefinition.className || '' ) + ' cke_rcombopanel';
+ panelDefinition.block =
+ {
+ multiSelect : panelDefinition.multiSelect,
+ attributes : panelDefinition.attributes
+ };
+
+ this._ =
+ {
+ panelDefinition : panelDefinition,
+ items : {},
+ state : CKEDITOR.TRISTATE_OFF
+ };
+ },
+
+ statics :
+ {
+ handler :
+ {
+ create : function( definition )
+ {
+ return new CKEDITOR.ui.richCombo( definition );
+ }
+ }
+ },
+
+ proto :
+ {
+ renderHtml : function( editor )
+ {
+ var output = [];
+ this.render( editor, output );
+ return output.join( '' );
+ },
+
+ /**
+ * Renders the combo.
+ * @param {CKEDITOR.editor} editor The editor instance which this button is
+ * to be used by.
+ * @param {Array} output The output array to which append the HTML relative
+ * to this button.
+ * @example
+ */
+ render : function( editor, output )
+ {
+ var env = CKEDITOR.env;
+
+ var id = 'cke_' + this.id;
+ var clickFn = CKEDITOR.tools.addFunction( function( $element )
+ {
+ var _ = this._;
+
+ if ( _.state == CKEDITOR.TRISTATE_DISABLED )
+ return;
+
+ this.createPanel( editor );
+
+ if ( _.on )
+ {
+ _.panel.hide();
+ return;
+ }
+
+ this.commit();
+ var value = this.getValue();
+ if ( value )
+ _.list.mark( value );
+ else
+ _.list.unmarkAll();
+
+ _.panel.showBlock( this.id, new CKEDITOR.dom.element( $element ), 4 );
+ },
+ this );
+
+ var instance = {
+ id : id,
+ combo : this,
+ focus : function()
+ {
+ var element = CKEDITOR.document.getById( id ).getChild( 1 );
+ element.focus();
+ },
+ clickFn : clickFn
+ };
+
+ editor.on( 'mode', function()
+ {
+ this.setState( this.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
+ this.setValue( '' );
+ },
+ this );
+
+ var keyDownFn = CKEDITOR.tools.addFunction( function( ev, element )
+ {
+ ev = new CKEDITOR.dom.event( ev );
+
+ var keystroke = ev.getKeystroke();
+ switch ( keystroke )
+ {
+ case 13 : // ENTER
+ case 32 : // SPACE
+ case 40 : // ARROW-DOWN
+ // Show panel
+ CKEDITOR.tools.callFunction( clickFn, element );
+ break;
+ default :
+ // Delegate the default behavior to toolbar button key handling.
+ instance.onkey( instance, keystroke );
+ }
+
+ // Avoid subsequent focus grab on editor document.
+ ev.preventDefault();
+ });
+
+ // For clean up
+ instance.keyDownFn = keyDownFn;
+
+ output.push(
+ '',
+ '',
+ '', this.label, ' ',
+ '= 10900 && !env.hc ? '' : ' href="javascript:void(\'' + this.label + '\')"',
+ ' role="button" aria-labelledby="', id , '_label" aria-describedby="', id, '_text" aria-haspopup="true"' );
+
+ // Some browsers don't cancel key events in the keydown but in the
+ // keypress.
+ // TODO: Check if really needed for Gecko+Mac.
+ if ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.mac ) )
+ {
+ output.push(
+ ' onkeypress="return false;"' );
+ }
+
+ // With Firefox, we need to force it to redraw, otherwise it
+ // will remain in the focus state.
+ if ( CKEDITOR.env.gecko )
+ {
+ output.push(
+ ' onblur="this.style.cssText = this.style.cssText;"' );
+ }
+
+ output.push(
+ ' onkeydown="CKEDITOR.tools.callFunction( ', keyDownFn, ', event, this );"' +
+ ' onclick="CKEDITOR.tools.callFunction(', clickFn, ', this); return false;">' +
+ '' +
+ '' + this.label + ' ' +
+ ' ' +
+ '' + ( CKEDITOR.env.hc ? '▼' : CKEDITOR.env.air ? ' ' : '' ) + ' ' + // BLACK DOWN-POINTING TRIANGLE
+ ' ' +
+ ' ' +
+ ' ' );
+
+ if ( this.onRender )
+ this.onRender();
+
+ return instance;
+ },
+
+ createPanel : function( editor )
+ {
+ if ( this._.panel )
+ return;
+
+ var panelDefinition = this._.panelDefinition,
+ panelBlockDefinition = this._.panelDefinition.block,
+ panelParentElement = panelDefinition.parent || CKEDITOR.document.getBody(),
+ panel = new CKEDITOR.ui.floatPanel( editor, panelParentElement, panelDefinition ),
+ list = panel.addListBlock( this.id, panelBlockDefinition ),
+ me = this;
+
+ panel.onShow = function()
+ {
+ if ( me.className )
+ this.element.getFirst().addClass( me.className + '_panel' );
+
+ me.setState( CKEDITOR.TRISTATE_ON );
+
+ list.focus( !me.multiSelect && me.getValue() );
+
+ me._.on = 1;
+
+ if ( me.onOpen )
+ me.onOpen();
+ };
+
+ panel.onHide = function( preventOnClose )
+ {
+ if ( me.className )
+ this.element.getFirst().removeClass( me.className + '_panel' );
+
+ me.setState( me.modes && me.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
+
+ me._.on = 0;
+
+ if ( !preventOnClose && me.onClose )
+ me.onClose();
+ };
+
+ panel.onEscape = function()
+ {
+ panel.hide();
+ me.document.getById( 'cke_' + me.id ).getFirst().getNext().focus();
+ };
+
+ list.onClick = function( value, marked )
+ {
+ // Move the focus to the main windows, otherwise it will stay
+ // into the floating panel, even if invisible, and Safari and
+ // Opera will go a bit crazy.
+ me.document.getWindow().focus();
+
+ if ( me.onClick )
+ me.onClick.call( me, value, marked );
+
+ if ( marked )
+ me.setValue( value, me._.items[ value ] );
+ else
+ me.setValue( '' );
+
+ panel.hide();
+ };
+
+ this._.panel = panel;
+ this._.list = list;
+
+ panel.getBlock( this.id ).onHide = function()
+ {
+ me._.on = 0;
+ me.setState( CKEDITOR.TRISTATE_OFF );
+ };
+
+ if ( this.init )
+ this.init();
+ },
+
+ setValue : function( value, text )
+ {
+ this._.value = value;
+
+ var textElement = this.document.getById( 'cke_' + this.id + '_text' );
+ if ( textElement )
+ {
+ if ( !( value || text ) )
+ {
+ text = this.label;
+ textElement.addClass( 'cke_inline_label' );
+ }
+ else
+ textElement.removeClass( 'cke_inline_label' );
+
+ textElement.setHtml( typeof text != 'undefined' ? text : value );
+ }
+ },
+
+ getValue : function()
+ {
+ return this._.value || '';
+ },
+
+ unmarkAll : function()
+ {
+ this._.list.unmarkAll();
+ },
+
+ mark : function( value )
+ {
+ this._.list.mark( value );
+ },
+
+ hideItem : function( value )
+ {
+ this._.list.hideItem( value );
+ },
+
+ hideGroup : function( groupTitle )
+ {
+ this._.list.hideGroup( groupTitle );
+ },
+
+ showAll : function()
+ {
+ this._.list.showAll();
+ },
+
+ add : function( value, html, text )
+ {
+ this._.items[ value ] = text || value;
+ this._.list.add( value, html, text );
+ },
+
+ startGroup : function( title )
+ {
+ this._.list.startGroup( title );
+ },
+
+ commit : function()
+ {
+ if ( !this._.committed )
+ {
+ this._.list.commit();
+ this._.committed = 1;
+ CKEDITOR.ui.fire( 'ready', this );
+ }
+ this._.committed = 1;
+ },
+
+ setState : function( state )
+ {
+ if ( this._.state == state )
+ return;
+
+ this.document.getById( 'cke_' + this.id ).setState( state );
+
+ this._.state = state;
+ }
+ }
+});
+
+CKEDITOR.ui.prototype.addRichCombo = function( name, definition )
+{
+ this.add( name, CKEDITOR.UI_RICHCOMBO, definition );
+};
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/save/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/save/plugin.js
new file mode 100644
index 00000000..5cf2b91b
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/save/plugin.js
@@ -0,0 +1,55 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+/**
+ * @fileSave plugin.
+ */
+
+(function()
+{
+ var saveCmd =
+ {
+ modes : { wysiwyg:1, source:1 },
+
+ exec : function( editor )
+ {
+ var $form = editor.element.$.form;
+
+ if ( $form )
+ {
+ try
+ {
+ $form.submit();
+ }
+ catch( e )
+ {
+ // If there's a button named "submit" then the form.submit
+ // function is masked and can't be called in IE/FF, so we
+ // call the click() method of that button.
+ if ( $form.submit.click )
+ $form.submit.click();
+ }
+ }
+ }
+ };
+
+ var pluginName = 'save';
+
+ // Register a plugin named "save".
+ CKEDITOR.plugins.add( pluginName,
+ {
+ init : function( editor )
+ {
+ var command = editor.addCommand( pluginName, saveCmd );
+ command.modes = { wysiwyg : !!( editor.element.$.form ) };
+
+ editor.ui.addButton( 'Save',
+ {
+ label : editor.lang.save,
+ command : pluginName
+ });
+ }
+ });
+})();
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/scayt/dialogs/options.js b/app/assets/javascripts/ckeditor/_source/plugins/scayt/dialogs/options.js
new file mode 100644
index 00000000..3bd99163
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/scayt/dialogs/options.js
@@ -0,0 +1,537 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.dialog.add( 'scaytcheck', function( editor )
+{
+ var firstLoad = true,
+ captions,
+ doc = CKEDITOR.document,
+ editorName = editor.name,
+ tags = CKEDITOR.plugins.scayt.getUiTabs( editor ),
+ i,
+ contents = [],
+ userDicActive = 0,
+ dic_buttons = [
+ // [0] contains buttons for creating
+ "dic_create_" + editorName + ",dic_restore_" + editorName,
+ // [1] contains buton for manipulation
+ "dic_rename_" + editorName + ",dic_delete_" + editorName
+ ],
+ optionsIds = [ 'mixedCase', 'mixedWithDigits', 'allCaps', 'ignoreDomainNames' ];
+
+ // common operations
+
+ function getBOMAllOptions()
+ {
+ if (typeof document.forms["optionsbar_" + editorName] != "undefined")
+ return document.forms["optionsbar_" + editorName]["options"];
+ return [];
+ }
+ function getBOMAllLangs()
+ {
+ if (typeof document.forms["languagesbar_" + editorName] != "undefined")
+ return document.forms["languagesbar_" + editorName]["scayt_lang"];
+ return [];
+ }
+
+ function setCheckedValue( radioObj, newValue )
+ {
+ if ( !radioObj )
+ return;
+ var radioLength = radioObj.length;
+ if ( radioLength == undefined )
+ {
+ radioObj.checked = radioObj.value == newValue.toString();
+ return;
+ }
+ for ( var i = 0; i < radioLength; i++ )
+ {
+ radioObj[i].checked = false;
+ if ( radioObj[i].value == newValue.toString() )
+ radioObj[i].checked = true;
+ }
+ }
+
+ var lang = editor.lang.scayt;
+ var tags_contents = [
+ {
+ id : 'options',
+ label : lang.optionsTab,
+ elements : [
+ {
+ type : 'html',
+ id : 'options',
+ html : ''
+ }
+ ]
+ },
+ {
+ id : 'langs',
+ label : lang.languagesTab,
+ elements : [
+ {
+ type : 'html',
+ id : 'langs',
+ html : ''
+ }
+ ]
+ },
+ {
+ id : 'dictionaries',
+ label : lang.dictionariesTab,
+ elements : [
+ {
+ type : 'html',
+ style: '',
+ id : 'dictionaries',
+ html : ''
+ }
+ ]
+ },
+ {
+ id : 'about',
+ label : lang.aboutTab,
+ elements : [
+ {
+ type : 'html',
+ id : 'about',
+ style : 'margin: 5px 5px;',
+ html : '
'
+ }
+ ]
+ }
+ ];
+
+ var dialogDefiniton = {
+ title : lang.title,
+ minWidth : 360,
+ minHeight : 220,
+ onShow : function()
+ {
+ var dialog = this;
+ dialog.data = editor.fire( 'scaytDialog', {} );
+ dialog.options = dialog.data.scayt_control.option();
+ dialog.chosed_lang = dialog.sLang = dialog.data.scayt_control.sLang;
+
+ if ( !dialog.data || !dialog.data.scayt || !dialog.data.scayt_control )
+ {
+ alert( 'Error loading application service' );
+ dialog.hide();
+ return;
+ }
+
+ var stop = 0;
+ if ( firstLoad )
+ {
+ dialog.data.scayt.getCaption( editor.langCode || 'en', function( caps )
+ {
+ if ( stop++ > 0 ) // Once only
+ return;
+ captions = caps;
+ init_with_captions.apply( dialog );
+ reload.apply( dialog );
+ firstLoad = false;
+ });
+ }
+ else
+ reload.apply( dialog );
+
+ dialog.selectPage( dialog.data.tab );
+ },
+ onOk : function()
+ {
+ var scayt_control = this.data.scayt_control;
+ scayt_control.option( this.options );
+ // Setup language if it was changed.
+ var csLang = this.chosed_lang;
+ scayt_control.setLang( csLang );
+ scayt_control.refresh();
+ },
+ onCancel: function()
+ {
+ var o = getBOMAllOptions();
+ for ( var i in o )
+ o[i].checked = false;
+
+ setCheckedValue( getBOMAllLangs(), "" );
+ },
+ contents : contents
+ };
+
+ var scayt_control = CKEDITOR.plugins.scayt.getScayt( editor );
+
+ for ( i = 0; i < tags.length; i++ )
+ {
+ if ( tags[ i ] == 1 )
+ contents[ contents.length ] = tags_contents[ i ];
+ }
+ if ( tags[2] == 1 )
+ userDicActive = 1;
+
+ var init_with_captions = function()
+ {
+ var dialog = this,
+ lang_list = dialog.data.scayt.getLangList(),
+ buttonCaptions = [ 'dic_create', 'dic_delete', 'dic_rename', 'dic_restore' ],
+ buttonIds = [],
+ langList = [],
+ labels = optionsIds,
+ i;
+
+ // Add buttons titles
+ if ( userDicActive )
+ {
+ for ( i = 0; i < buttonCaptions.length; i++ )
+ {
+ buttonIds[ i ] = buttonCaptions[ i ] + "_" + editorName;
+ doc.getById( buttonIds[ i ] ).setHtml( '' + captions[ 'button_' + buttonCaptions[ i ]] +' ' );
+ }
+ doc.getById( 'dic_info_' + editorName ).setHtml( captions[ 'dic_info' ] );
+ }
+
+ // Fill options and dictionary labels.
+ if ( tags[0] == 1 )
+ {
+ for ( i in labels )
+ {
+ var labelCaption = 'label_' + labels[ i ],
+ labelId = labelCaption + '_' + editorName,
+ labelElement = doc.getById( labelId );
+
+ if ( 'undefined' != typeof labelElement
+ && 'undefined' != typeof captions[ labelCaption ]
+ && 'undefined' != typeof dialog.options[labels[ i ]] )
+ {
+ labelElement.setHtml( captions[ labelCaption ] );
+ var labelParent = labelElement.getParent();
+ labelParent.$.style.display = "block";
+ }
+ }
+ }
+
+ var about = '
' +
+ '' + captions[ 'version' ] + window.scayt.getAboutInfo().version.toString() + '
' +
+ '' + captions[ 'about_throwt_copy' ] + '
';
+
+ doc.getById( 'scayt_about_' + editorName ).setHtml( about );
+
+ // Create languages tab.
+ var createOption = function( option, list )
+ {
+ var label = doc.createElement( 'label' );
+ label.setAttribute( 'for', 'cke_option' + option );
+ label.setHtml( list[ option ] );
+
+ if ( dialog.sLang == option ) // Current.
+ dialog.chosed_lang = option;
+
+ var div = doc.createElement( 'div' );
+ var radio = CKEDITOR.dom.element.createFromHtml( ' ' );
+
+ radio.on( 'click', function()
+ {
+ this.$.checked = true;
+ dialog.chosed_lang = option;
+ });
+
+ div.append( radio );
+ div.append( label );
+
+ return {
+ lang : list[ option ],
+ code : option,
+ radio : div
+ };
+ };
+
+ if ( tags[1] ==1 )
+ {
+ for ( i in lang_list.rtl )
+ langList[ langList.length ] = createOption( i, lang_list.ltr );
+
+ for ( i in lang_list.ltr )
+ langList[ langList.length ] = createOption( i, lang_list.ltr );
+
+ langList.sort( function( lang1, lang2 )
+ {
+ return ( lang2.lang > lang1.lang ) ? -1 : 1 ;
+ });
+
+ var fieldL = doc.getById( 'scayt_lcol_' + editorName ),
+ fieldR = doc.getById( 'scayt_rcol_' + editorName );
+ for ( i=0; i < langList.length; i++ )
+ {
+ var field = ( i < langList.length / 2 ) ? fieldL : fieldR;
+ field.append( langList[ i ].radio );
+ }
+ }
+
+ // user dictionary handlers
+ var dic = {};
+ dic.dic_create = function( el, dic_name , dic_buttons )
+ {
+ // comma separated button's ids include repeats if exists
+ var all_buttons = dic_buttons[0] + ',' + dic_buttons[1];
+
+ var err_massage = captions["err_dic_create"];
+ var suc_massage = captions["succ_dic_create"];
+
+ window.scayt.createUserDictionary( dic_name,
+ function( arg )
+ {
+ hide_dic_buttons ( all_buttons );
+ display_dic_buttons ( dic_buttons[1] );
+ suc_massage = suc_massage.replace("%s" , arg.dname );
+ dic_success_message (suc_massage);
+ },
+ function( arg )
+ {
+ err_massage = err_massage.replace("%s" ,arg.dname );
+ dic_error_message ( err_massage + "( "+ (arg.message || "") +")");
+ });
+
+ };
+
+ dic.dic_rename = function( el, dic_name )
+ {
+ //
+ // try to rename dictionary
+ var err_massage = captions["err_dic_rename"] || "";
+ var suc_massage = captions["succ_dic_rename"] || "";
+ window.scayt.renameUserDictionary( dic_name,
+ function( arg )
+ {
+ suc_massage = suc_massage.replace("%s" , arg.dname );
+ set_dic_name( dic_name );
+ dic_success_message ( suc_massage );
+ },
+ function( arg )
+ {
+ err_massage = err_massage.replace("%s" , arg.dname );
+ set_dic_name( dic_name );
+ dic_error_message( err_massage + "( " + ( arg.message || "" ) + " )" );
+ });
+ };
+
+ dic.dic_delete = function( el, dic_name , dic_buttons )
+ {
+ var all_buttons = dic_buttons[0] + ',' + dic_buttons[1];
+ var err_massage = captions["err_dic_delete"];
+ var suc_massage = captions["succ_dic_delete"];
+
+ // try to delete dictionary
+ window.scayt.deleteUserDictionary(
+ function( arg )
+ {
+ suc_massage = suc_massage.replace("%s" , arg.dname );
+ hide_dic_buttons ( all_buttons );
+ display_dic_buttons ( dic_buttons[0] );
+ set_dic_name( "" ); // empty input field
+ dic_success_message( suc_massage );
+ },
+ function( arg )
+ {
+ err_massage = err_massage.replace("%s" , arg.dname );
+ dic_error_message(err_massage);
+ });
+ };
+
+ dic.dic_restore = dialog.dic_restore || function( el, dic_name , dic_buttons )
+ {
+ // try to restore existing dictionary
+ var all_buttons = dic_buttons[0] + ',' + dic_buttons[1];
+ var err_massage = captions["err_dic_restore"];
+ var suc_massage = captions["succ_dic_restore"];
+
+ window.scayt.restoreUserDictionary(dic_name,
+ function( arg )
+ {
+ suc_massage = suc_massage.replace("%s" , arg.dname );
+ hide_dic_buttons ( all_buttons );
+ display_dic_buttons(dic_buttons[1]);
+ dic_success_message( suc_massage );
+ },
+ function( arg )
+ {
+ err_massage = err_massage.replace("%s" , arg.dname );
+ dic_error_message( err_massage );
+ });
+ };
+
+ function onDicButtonClick( ev )
+ {
+ var dic_name = doc.getById('dic_name_' + editorName).getValue();
+ if ( !dic_name )
+ {
+ dic_error_message(" Dictionary name should not be empty. ");
+ return false;
+ }
+ try{
+ var el = ev.data.getTarget().getParent();
+ var id = /(dic_\w+)_[\w\d]+/.exec(el.getId())[1];
+ dic[ id ].apply( null, [ el, dic_name, dic_buttons ] );
+ }
+ catch(err)
+ {
+ dic_error_message(" Dictionary error. ");
+ }
+
+ return true;
+ }
+
+ // ** bind event listeners
+ var arr_buttons = ( dic_buttons[0] + ',' + dic_buttons[1] ).split( ',' ),
+ l;
+
+ for ( i = 0, l = arr_buttons.length ; i < l ; i += 1 )
+ {
+ var dic_button = doc.getById(arr_buttons[i]);
+ if ( dic_button )
+ dic_button.on( 'click', onDicButtonClick, this );
+ }
+ };
+
+ var reload = function()
+ {
+ var dialog = this;
+ // for enabled options tab
+ if ( tags[0] == 1 ){
+ var opto = getBOMAllOptions();
+
+ // Animate options.
+ for ( var k=0,l = opto.length; k' + m + ' ' );
+ }
+ function dic_success_message( m )
+ {
+ doc.getById('dic_message_' + editorName).setHtml('' + m + ' ') ;
+ }
+ function display_dic_buttons( sIds )
+ {
+ sIds = String( sIds );
+ var aIds = sIds.split(',');
+ for ( var i=0, l = aIds.length; i < l ; i+=1)
+ doc.getById( aIds[i] ).$.style.display = "inline";
+ }
+ function hide_dic_buttons( sIds )
+ {
+ sIds = String( sIds );
+ var aIds = sIds.split(',');
+ for ( var i = 0, l = aIds.length; i < l ; i += 1 )
+ doc.getById( aIds[i] ).$.style.display = "none";
+ }
+ function set_dic_name( dic_name )
+ {
+ doc.getById('dic_name_' + editorName).$.value= dic_name;
+ }
+
+ return dialogDefiniton;
+});
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/scayt/dialogs/toolbar.css b/app/assets/javascripts/ckeditor/_source/plugins/scayt/dialogs/toolbar.css
new file mode 100644
index 00000000..ecabdac9
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/scayt/dialogs/toolbar.css
@@ -0,0 +1,71 @@
+a
+{
+ text-decoration:none;
+ padding: 2px 4px 4px 6px;
+ display : block;
+ border-width: 1px;
+ border-style: solid;
+ margin : 0px;
+}
+
+a.cke_scayt_toogle:hover,
+a.cke_scayt_toogle:focus,
+a.cke_scayt_toogle:active
+{
+ border-color: #316ac5;
+ background-color: #dff1ff;
+ color : #000;
+ cursor: pointer;
+ margin : 0px;
+}
+a.cke_scayt_toogle {
+ color : #316ac5;
+ border-color: #fff;
+}
+.scayt_enabled a.cke_scayt_item {
+ color : #316ac5;
+ border-color: #fff;
+ margin : 0px;
+}
+.scayt_disabled a.cke_scayt_item {
+ color : gray;
+ border-color : #fff;
+}
+.scayt_enabled a.cke_scayt_item:hover,
+.scayt_enabled a.cke_scayt_item:focus,
+.scayt_enabled a.cke_scayt_item:active
+{
+ border-color: #316ac5;
+ background-color: #dff1ff;
+ color : #000;
+ cursor: pointer;
+}
+.scayt_disabled a.cke_scayt_item:hover,
+.scayt_disabled a.cke_scayt_item:focus,
+.scayt_disabled a.cke_scayt_item:active
+{
+ border-color: gray;
+ background-color: #dff1ff;
+ color : gray;
+ cursor: no-drop;
+}
+.cke_scayt_set_on, .cke_scayt_set_off
+{
+ display: none;
+}
+.scayt_enabled .cke_scayt_set_on
+{
+ display: none;
+}
+.scayt_disabled .cke_scayt_set_on
+{
+ display: inline;
+}
+.scayt_disabled .cke_scayt_set_off
+{
+ display: none;
+}
+.scayt_enabled .cke_scayt_set_off
+{
+ display: inline;
+}
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/scayt/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/scayt/plugin.js
new file mode 100644
index 00000000..967767fd
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/scayt/plugin.js
@@ -0,0 +1,964 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+/**
+ * @fileOverview Spell Check As You Type (SCAYT).
+ * Button name : Scayt.
+ */
+
+(function()
+{
+ var commandName = 'scaytcheck',
+ openPage = '';
+
+ // Checks if a value exists in an array
+ function in_array( needle, haystack )
+ {
+ var found = 0,
+ key;
+ for ( key in haystack )
+ {
+ if ( haystack[ key ] == needle )
+ {
+ found = 1;
+ break;
+ }
+ }
+ return found;
+ }
+
+ var onEngineLoad = function()
+ {
+ var editor = this;
+
+ var createInstance = function() // Create new instance every time Document is created.
+ {
+ var config = editor.config;
+ // Initialise Scayt instance.
+ var oParams = {};
+ // Get the iframe.
+ oParams.srcNodeRef = editor.document.getWindow().$.frameElement;
+ // syntax : AppName.AppVersion@AppRevision
+ oParams.assocApp = 'CKEDITOR.' + CKEDITOR.version + '@' + CKEDITOR.revision;
+ oParams.customerid = config.scayt_customerid || '1:WvF0D4-UtPqN1-43nkD4-NKvUm2-daQqk3-LmNiI-z7Ysb4-mwry24-T8YrS3-Q2tpq2';
+ oParams.customDictionaryIds = config.scayt_customDictionaryIds || '';
+ oParams.userDictionaryName = config.scayt_userDictionaryName || '';
+ oParams.sLang = config.scayt_sLang || 'en_US';
+
+ // Introduce SCAYT onLoad callback. (#5632)
+ oParams.onLoad = function()
+ {
+ // Draw down word marker to avoid being covered by background-color style.(#5466)
+ if ( !( CKEDITOR.env.ie && CKEDITOR.env.version < 8 ) )
+ this.addStyle( this.selectorCss(), 'padding-bottom: 2px !important;' );
+
+ // Call scayt_control.focus when SCAYT loaded
+ // and only if editor has focus and scayt control creates at first time (#5720)
+ if ( editor.focusManager.hasFocus && !plugin.isControlRestored( editor ) )
+ this.focus();
+
+ };
+
+ oParams.onBeforeChange = function()
+ {
+ if ( plugin.getScayt( editor ) && !editor.checkDirty() )
+ setTimeout( function(){ editor.resetDirty(); }, 0 );
+ };
+
+ var scayt_custom_params = window.scayt_custom_params;
+ if ( typeof scayt_custom_params == 'object' )
+ {
+ for ( var k in scayt_custom_params )
+ oParams[ k ] = scayt_custom_params[ k ];
+ }
+ // needs for restoring a specific scayt control settings
+ if ( plugin.getControlId( editor ) )
+ oParams.id = plugin.getControlId( editor );
+
+ var scayt_control = new window.scayt( oParams );
+
+ scayt_control.afterMarkupRemove.push( function( node )
+ {
+ ( new CKEDITOR.dom.element( node, scayt_control.document ) ).mergeSiblings();
+ } );
+
+ // Copy config.
+ var lastInstance = plugin.instances[ editor.name ];
+ if ( lastInstance )
+ {
+ scayt_control.sLang = lastInstance.sLang;
+ scayt_control.option( lastInstance.option() );
+ scayt_control.paused = lastInstance.paused;
+ }
+
+ plugin.instances[ editor.name ] = scayt_control;
+
+ try {
+ scayt_control.setDisabled( plugin.isPaused( editor ) === false );
+ } catch (e) {}
+
+ editor.fire( 'showScaytState' );
+ };
+
+ editor.on( 'contentDom', createInstance );
+ editor.on( 'contentDomUnload', function()
+ {
+ // Remove scripts.
+ var scripts = CKEDITOR.document.getElementsByTag( 'script' ),
+ scaytIdRegex = /^dojoIoScript(\d+)$/i,
+ scaytSrcRegex = /^https?:\/\/svc\.spellchecker\.net\/spellcheck\/script\/ssrv\.cgi/i;
+
+ for ( var i=0; i < scripts.count(); i++ )
+ {
+ var script = scripts.getItem( i ),
+ id = script.getId(),
+ src = script.getAttribute( 'src' );
+
+ if ( id && src && id.match( scaytIdRegex ) && src.match( scaytSrcRegex ))
+ script.remove();
+ }
+ });
+
+ editor.on( 'beforeCommandExec', function( ev ) // Disable SCAYT before Source command execution.
+ {
+ if ( ( ev.data.name == 'source' || ev.data.name == 'newpage' ) && editor.mode == 'wysiwyg' )
+ {
+ var scayt_instance = plugin.getScayt( editor );
+ if ( scayt_instance )
+ {
+ plugin.setPaused( editor, !scayt_instance.disabled );
+ // store a control id for restore a specific scayt control settings
+ plugin.setControlId( editor, scayt_instance.id );
+ scayt_instance.destroy( true );
+ delete plugin.instances[ editor.name ];
+ }
+ }
+ // Catch on source mode switch off (#5720)
+ else if ( ev.data.name == 'source' && editor.mode == 'source' )
+ plugin.markControlRestore( editor );
+ });
+
+ editor.on( 'afterCommandExec', function( ev )
+ {
+ if ( !plugin.isScaytEnabled( editor ) )
+ return;
+
+ if ( editor.mode == 'wysiwyg' && ( ev.data.name == 'undo' || ev.data.name == 'redo' ) )
+ window.setTimeout( function() { plugin.getScayt( editor ).refresh(); }, 10 );
+ });
+
+ editor.on( 'destroy', function( ev )
+ {
+ var editor = ev.editor,
+ scayt_instance = plugin.getScayt( editor );
+
+ // SCAYT instance might already get destroyed by mode switch (#5744).
+ if ( !scayt_instance )
+ return;
+
+ delete plugin.instances[ editor.name ];
+ // store a control id for restore a specific scayt control settings
+ plugin.setControlId( editor, scayt_instance.id );
+ scayt_instance.destroy( true );
+ });
+
+ // Listen to data manipulation to reflect scayt markup.
+ editor.on( 'afterSetData', function()
+ {
+ if ( plugin.isScaytEnabled( editor ) ) {
+ window.setTimeout( function()
+ {
+ var instance = plugin.getScayt( editor );
+ instance && instance.refresh();
+ }, 10 );
+ }
+ });
+
+ // Reload spell-checking for current word after insertion completed.
+ editor.on( 'insertElement', function()
+ {
+ var scayt_instance = plugin.getScayt( editor );
+ if ( plugin.isScaytEnabled( editor ) )
+ {
+ // Unlock the selection before reload, SCAYT will take
+ // care selection update.
+ if ( CKEDITOR.env.ie )
+ editor.getSelection().unlock( true );
+
+ // Return focus to the editor and refresh SCAYT markup (#5573).
+ window.setTimeout( function()
+ {
+ scayt_instance.focus();
+ scayt_instance.refresh();
+ }, 10 );
+ }
+ }, this, null, 50 );
+
+ editor.on( 'insertHtml', function()
+ {
+ var scayt_instance = plugin.getScayt( editor );
+ if ( plugin.isScaytEnabled( editor ) )
+ {
+ // Unlock the selection before reload, SCAYT will take
+ // care selection update.
+ if ( CKEDITOR.env.ie )
+ editor.getSelection().unlock( true );
+
+ // Return focus to the editor (#5573)
+ // Refresh SCAYT markup
+ window.setTimeout( function()
+ {
+ scayt_instance.focus();
+ scayt_instance.refresh();
+ }, 10 );
+ }
+ }, this, null, 50 );
+
+ editor.on( 'scaytDialog', function( ev ) // Communication with dialog.
+ {
+ ev.data.djConfig = window.djConfig;
+ ev.data.scayt_control = plugin.getScayt( editor );
+ ev.data.tab = openPage;
+ ev.data.scayt = window.scayt;
+ });
+
+ var dataProcessor = editor.dataProcessor,
+ htmlFilter = dataProcessor && dataProcessor.htmlFilter;
+
+ if ( htmlFilter )
+ {
+ htmlFilter.addRules(
+ {
+ elements :
+ {
+ span : function( element )
+ {
+ if ( element.attributes[ 'data-scayt_word' ]
+ && element.attributes[ 'data-scaytid' ] )
+ {
+ delete element.name; // Write children, but don't write this node.
+ return element;
+ }
+ }
+ }
+ }
+ );
+ }
+
+ // Override Image.equals method avoid CK snapshot module to add SCAYT markup to snapshots. (#5546)
+ var undoImagePrototype = CKEDITOR.plugins.undo.Image.prototype;
+ undoImagePrototype.equals = CKEDITOR.tools.override( undoImagePrototype.equals, function( org )
+ {
+ return function( otherImage )
+ {
+ var thisContents = this.contents,
+ otherContents = otherImage.contents;
+ var scayt_instance = plugin.getScayt( this.editor );
+ // Making the comparison based on content without SCAYT word markers.
+ if ( scayt_instance && plugin.isScaytReady( this.editor ) )
+ {
+ // scayt::reset might return value undefined. (#5742)
+ this.contents = scayt_instance.reset( thisContents ) || '';
+ otherImage.contents = scayt_instance.reset( otherContents ) || '';
+ }
+
+ var retval = org.apply( this, arguments );
+
+ this.contents = thisContents;
+ otherImage.contents = otherContents;
+ return retval;
+ };
+ });
+
+ if ( editor.document )
+ createInstance();
+ };
+
+CKEDITOR.plugins.scayt =
+ {
+ engineLoaded : false,
+ instances : {},
+ // Data storage for SCAYT control, based on editor instances
+ controlInfo : {},
+ setControlInfo : function( editor, o )
+ {
+ if ( editor && editor.name && typeof ( this.controlInfo[ editor.name ] ) != 'object' )
+ this.controlInfo[ editor.name ] = {};
+
+ for ( var infoOpt in o )
+ this.controlInfo[ editor.name ][ infoOpt ] = o[ infoOpt ];
+ },
+ isControlRestored : function( editor )
+ {
+ if ( editor &&
+ editor.name &&
+ this.controlInfo[ editor.name ] )
+ {
+ return this.controlInfo[ editor.name ].restored ;
+ }
+ return false;
+ },
+ markControlRestore : function( editor )
+ {
+ this.setControlInfo( editor, { restored:true } );
+ },
+ setControlId: function( editor, id )
+ {
+ this.setControlInfo( editor, { id:id } );
+ },
+ getControlId: function( editor )
+ {
+ if ( editor &&
+ editor.name &&
+ this.controlInfo[ editor.name ] &&
+ this.controlInfo[ editor.name ].id )
+ {
+ return this.controlInfo[ editor.name ].id;
+ }
+ return null;
+ },
+ setPaused: function( editor , bool )
+ {
+ this.setControlInfo( editor, { paused:bool } );
+ },
+ isPaused: function( editor )
+ {
+ if ( editor &&
+ editor.name &&
+ this.controlInfo[editor.name] )
+ {
+ return this.controlInfo[editor.name].paused;
+ }
+ return undefined;
+ },
+ getScayt : function( editor )
+ {
+ return this.instances[ editor.name ];
+ },
+ isScaytReady : function( editor )
+ {
+ return this.engineLoaded === true &&
+ 'undefined' !== typeof window.scayt && this.getScayt( editor );
+ },
+ isScaytEnabled : function( editor )
+ {
+ var scayt_instance = this.getScayt( editor );
+ return ( scayt_instance ) ? scayt_instance.disabled === false : false;
+ },
+ getUiTabs : function( editor )
+ {
+ var uiTabs = [];
+
+ // read UI tabs value from config
+ var configUiTabs = editor.config.scayt_uiTabs || "1,1,1";
+
+ // convert string to array
+ configUiTabs = configUiTabs.split( ',' );
+
+ // "About us" should be always shown for standard config
+ configUiTabs[3] = "1";
+
+ for ( var i = 0; i < 4; i++ ) {
+ uiTabs[i] = (typeof window.scayt != "undefined" && typeof window.scayt.uiTags != "undefined")
+ ? (parseInt(configUiTabs[i],10) && window.scayt.uiTags[i])
+ : parseInt(configUiTabs[i],10);
+ }
+ return uiTabs;
+ },
+ loadEngine : function( editor )
+ {
+ // SCAYT doesn't work with Firefox2, Opera and AIR.
+ if ( CKEDITOR.env.gecko && CKEDITOR.env.version < 10900 || CKEDITOR.env.opera || CKEDITOR.env.air )
+ return editor.fire( 'showScaytState' );
+
+ if ( this.engineLoaded === true )
+ return onEngineLoad.apply( editor ); // Add new instance.
+ else if ( this.engineLoaded == -1 ) // We are waiting.
+ return CKEDITOR.on( 'scaytReady', function(){ onEngineLoad.apply( editor ); } ); // Use function(){} to avoid rejection as duplicate.
+
+ CKEDITOR.on( 'scaytReady', onEngineLoad, editor );
+ CKEDITOR.on( 'scaytReady', function()
+ {
+ this.engineLoaded = true;
+ },
+ this,
+ null,
+ 0
+ ); // First to run.
+
+ this.engineLoaded = -1; // Loading in progress.
+
+ // compose scayt url
+ var protocol = document.location.protocol;
+ // Default to 'http' for unknown.
+ protocol = protocol.search( /https?:/) != -1? protocol : 'http:';
+ var baseUrl = 'svc.spellchecker.net/scayt26/loader__base.js';
+
+ var scaytUrl = editor.config.scayt_srcUrl || ( protocol + '//' + baseUrl );
+ var scaytConfigBaseUrl = plugin.parseUrl( scaytUrl ).path + '/';
+
+ if( window.scayt == undefined )
+ {
+ CKEDITOR._djScaytConfig =
+ {
+ baseUrl: scaytConfigBaseUrl,
+ addOnLoad:
+ [
+ function()
+ {
+ CKEDITOR.fireOnce( 'scaytReady' );
+ }
+ ],
+ isDebug: false
+ };
+ // Append javascript code.
+ CKEDITOR.document.getHead().append(
+ CKEDITOR.document.createElement( 'script',
+ {
+ attributes :
+ {
+ type : 'text/javascript',
+ async : 'true',
+ src : scaytUrl
+ }
+ })
+ );
+ }
+ else
+ CKEDITOR.fireOnce( 'scaytReady' );
+
+ return null;
+ },
+ parseUrl : function ( data )
+ {
+ var match;
+ if ( data.match && ( match = data.match(/(.*)[\/\\](.*?\.\w+)$/) ) )
+ return { path: match[1], file: match[2] };
+ else
+ return data;
+ }
+ };
+
+ var plugin = CKEDITOR.plugins.scayt;
+
+ // Context menu constructing.
+ var addButtonCommand = function( editor, buttonName, buttonLabel, commandName, command, menugroup, menuOrder )
+ {
+ editor.addCommand( commandName, command );
+
+ // If the "menu" plugin is loaded, register the menu item.
+ editor.addMenuItem( commandName,
+ {
+ label : buttonLabel,
+ command : commandName,
+ group : menugroup,
+ order : menuOrder
+ });
+ };
+
+ var commandDefinition =
+ {
+ preserveState : true,
+ editorFocus : false,
+ canUndo : false,
+
+ exec: function( editor )
+ {
+ if ( plugin.isScaytReady( editor ) )
+ {
+ var isEnabled = plugin.isScaytEnabled( editor );
+
+ this.setState( isEnabled ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_ON );
+
+ var scayt_control = plugin.getScayt( editor );
+ // the place where the status of editor focus should be restored
+ // after there will be ability to store its state before SCAYT button click
+ // if (storedFocusState is focused )
+ // scayt_control.focus();
+ //
+ // now focus is set certainly
+ scayt_control.focus();
+ scayt_control.setDisabled( isEnabled );
+ }
+ else if ( !editor.config.scayt_autoStartup && plugin.engineLoaded >= 0 ) // Load first time
+ {
+ this.setState( CKEDITOR.TRISTATE_DISABLED );
+ plugin.loadEngine( editor );
+ }
+ }
+ };
+
+ // Add scayt plugin.
+ CKEDITOR.plugins.add( 'scayt',
+ {
+ requires : [ 'menubutton' ],
+
+ beforeInit : function( editor )
+ {
+ var items_order = editor.config.scayt_contextMenuItemsOrder
+ || 'suggest|moresuggest|control',
+ items_order_str = "";
+
+ items_order = items_order.split( '|' );
+
+ if ( items_order && items_order.length )
+ {
+ for ( var pos = 0 ; pos < items_order.length ; pos++ )
+ items_order_str += 'scayt_' + items_order[ pos ] + ( items_order.length != parseInt( pos, 10 ) + 1 ? ',' : '' );
+ }
+
+ // Put it on top of all context menu items (#5717)
+ editor.config.menu_groups = items_order_str + ',' + editor.config.menu_groups;
+ },
+
+ init : function( editor )
+ {
+ // Delete span[data-scaytid] when text pasting in editor (#6921)
+ var dataFilter = editor.dataProcessor && editor.dataProcessor.dataFilter;
+ var dataFilterRules =
+ {
+ elements :
+ {
+ span : function( element )
+ {
+ var attrs = element.attributes;
+ if ( attrs && attrs[ 'data-scaytid' ] )
+ delete element.name;
+ }
+ }
+ };
+ dataFilter && dataFilter.addRules( dataFilterRules );
+
+ var moreSuggestions = {},
+ mainSuggestions = {};
+
+ // Scayt command.
+ var command = editor.addCommand( commandName, commandDefinition );
+
+ // Add Options dialog.
+ CKEDITOR.dialog.add( commandName, CKEDITOR.getUrl( this.path + 'dialogs/options.js' ) );
+
+ var uiTabs = plugin.getUiTabs( editor );
+
+ var menuGroup = 'scaytButton';
+ editor.addMenuGroup( menuGroup );
+ // combine menu items to render
+ var uiMenuItems = {};
+
+ var lang = editor.lang.scayt;
+
+ // always added
+ uiMenuItems.scaytToggle =
+ {
+ label : lang.enable,
+ command : commandName,
+ group : menuGroup
+ };
+
+ if ( uiTabs[0] == 1 )
+ uiMenuItems.scaytOptions =
+ {
+ label : lang.options,
+ group : menuGroup,
+ onClick : function()
+ {
+ openPage = 'options';
+ editor.openDialog( commandName );
+ }
+ };
+
+ if ( uiTabs[1] == 1 )
+ uiMenuItems.scaytLangs =
+ {
+ label : lang.langs,
+ group : menuGroup,
+ onClick : function()
+ {
+ openPage = 'langs';
+ editor.openDialog( commandName );
+ }
+ };
+ if ( uiTabs[2] == 1 )
+ uiMenuItems.scaytDict =
+ {
+ label : lang.dictionariesTab,
+ group : menuGroup,
+ onClick : function()
+ {
+ openPage = 'dictionaries';
+ editor.openDialog( commandName );
+ }
+ };
+ // always added
+ uiMenuItems.scaytAbout =
+ {
+ label : editor.lang.scayt.about,
+ group : menuGroup,
+ onClick : function()
+ {
+ openPage = 'about';
+ editor.openDialog( commandName );
+ }
+ };
+
+ editor.addMenuItems( uiMenuItems );
+
+ editor.ui.add( 'Scayt', CKEDITOR.UI_MENUBUTTON,
+ {
+ label : lang.title,
+ title : CKEDITOR.env.opera ? lang.opera_title : lang.title,
+ className : 'cke_button_scayt',
+ modes : { wysiwyg : 1 },
+ onRender: function()
+ {
+ command.on( 'state', function()
+ {
+ this.setState( command.state );
+ },
+ this);
+ },
+ onMenu : function()
+ {
+ var isEnabled = plugin.isScaytEnabled( editor );
+
+ editor.getMenuItem( 'scaytToggle' ).label = lang[ isEnabled ? 'disable' : 'enable' ];
+
+ var uiTabs = plugin.getUiTabs( editor );
+
+ return {
+ scaytToggle : CKEDITOR.TRISTATE_OFF,
+ scaytOptions : isEnabled && uiTabs[0] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
+ scaytLangs : isEnabled && uiTabs[1] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
+ scaytDict : isEnabled && uiTabs[2] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
+ scaytAbout : isEnabled && uiTabs[3] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED
+ };
+ }
+ });
+
+ // If the "contextmenu" plugin is loaded, register the listeners.
+ if ( editor.contextMenu && editor.addMenuItems )
+ {
+ editor.contextMenu.addListener( function( element, selection )
+ {
+ if ( !plugin.isScaytEnabled( editor )
+ || selection.getRanges()[ 0 ].checkReadOnly() )
+ return null;
+
+ var scayt_control = plugin.getScayt( editor ),
+ node = scayt_control.getScaytNode();
+
+ if ( !node )
+ return null;
+
+ var word = scayt_control.getWord( node );
+
+ if ( !word )
+ return null;
+
+ var sLang = scayt_control.getLang(),
+ _r = {},
+ items_suggestion = window.scayt.getSuggestion( word, sLang );
+ if ( !items_suggestion || !items_suggestion.length )
+ return null;
+ // Remove unused commands and menuitems
+ for ( i in moreSuggestions )
+ {
+ delete editor._.menuItems[ i ];
+ delete editor._.commands[ i ];
+ }
+ for ( i in mainSuggestions )
+ {
+ delete editor._.menuItems[ i ];
+ delete editor._.commands[ i ];
+ }
+ moreSuggestions = {}; // Reset items.
+ mainSuggestions = {};
+
+ var moreSuggestionsUnable = editor.config.scayt_moreSuggestions || 'on';
+ var moreSuggestionsUnableAdded = false;
+
+ var maxSuggestions = editor.config.scayt_maxSuggestions;
+ ( typeof maxSuggestions != 'number' ) && ( maxSuggestions = 5 );
+ !maxSuggestions && ( maxSuggestions = items_suggestion.length );
+
+ var contextCommands = editor.config.scayt_contextCommands || 'all';
+ contextCommands = contextCommands.split( '|' );
+
+ for ( var i = 0, l = items_suggestion.length; i < l; i += 1 )
+ {
+ var commandName = 'scayt_suggestion_' + items_suggestion[i].replace( ' ', '_' );
+ var exec = ( function( el, s )
+ {
+ return {
+ exec: function()
+ {
+ scayt_control.replace( el, s );
+ }
+ };
+ })( node, items_suggestion[i] );
+
+ if ( i < maxSuggestions )
+ {
+ addButtonCommand( editor, 'button_' + commandName, items_suggestion[i],
+ commandName, exec, 'scayt_suggest', i + 1 );
+ _r[ commandName ] = CKEDITOR.TRISTATE_OFF;
+ mainSuggestions[ commandName ] = CKEDITOR.TRISTATE_OFF;
+ }
+ else if ( moreSuggestionsUnable == 'on' )
+ {
+ addButtonCommand( editor, 'button_' + commandName, items_suggestion[i],
+ commandName, exec, 'scayt_moresuggest', i + 1 );
+ moreSuggestions[ commandName ] = CKEDITOR.TRISTATE_OFF;
+ moreSuggestionsUnableAdded = true;
+ }
+ }
+
+ if ( moreSuggestionsUnableAdded )
+ {
+ // Register the More suggestions group;
+ editor.addMenuItem( 'scayt_moresuggest',
+ {
+ label : lang.moreSuggestions,
+ group : 'scayt_moresuggest',
+ order : 10,
+ getItems : function()
+ {
+ return moreSuggestions;
+ }
+ });
+ mainSuggestions[ 'scayt_moresuggest' ] = CKEDITOR.TRISTATE_OFF;
+ }
+
+ if ( in_array( 'all', contextCommands ) || in_array( 'ignore', contextCommands) )
+ {
+ var ignore_command = {
+ exec: function(){
+ scayt_control.ignore( node );
+ }
+ };
+ addButtonCommand( editor, 'ignore', lang.ignore, 'scayt_ignore', ignore_command, 'scayt_control', 1 );
+ mainSuggestions[ 'scayt_ignore' ] = CKEDITOR.TRISTATE_OFF;
+ }
+
+ if ( in_array( 'all', contextCommands ) || in_array( 'ignoreall', contextCommands ) )
+ {
+ var ignore_all_command = {
+ exec: function(){
+ scayt_control.ignoreAll( node );
+ }
+ };
+ addButtonCommand(editor, 'ignore_all', lang.ignoreAll, 'scayt_ignore_all', ignore_all_command, 'scayt_control', 2);
+ mainSuggestions['scayt_ignore_all'] = CKEDITOR.TRISTATE_OFF;
+ }
+
+ if ( in_array( 'all', contextCommands ) || in_array( 'add', contextCommands ) )
+ {
+ var addword_command = {
+ exec: function(){
+ window.scayt.addWordToUserDictionary( node );
+ }
+ };
+ addButtonCommand(editor, 'add_word', lang.addWord, 'scayt_add_word', addword_command, 'scayt_control', 3);
+ mainSuggestions['scayt_add_word'] = CKEDITOR.TRISTATE_OFF;
+ }
+
+ if ( scayt_control.fireOnContextMenu )
+ scayt_control.fireOnContextMenu( editor );
+
+ return mainSuggestions;
+ });
+ }
+
+ var showInitialState = function()
+ {
+ editor.removeListener( 'showScaytState', showInitialState );
+
+ if ( !CKEDITOR.env.opera && !CKEDITOR.env.air )
+ command.setState( plugin.isScaytEnabled( editor ) ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
+ else
+ command.setState( CKEDITOR.TRISTATE_DISABLED );
+ };
+
+ editor.on( 'showScaytState', showInitialState );
+
+ if ( CKEDITOR.env.opera || CKEDITOR.env.air )
+ {
+ editor.on( 'instanceReady', function()
+ {
+ showInitialState();
+ });
+ }
+
+ // Start plugin
+ if ( editor.config.scayt_autoStartup )
+ {
+ editor.on( 'instanceReady', function()
+ {
+ plugin.loadEngine( editor );
+ });
+ }
+ },
+
+ afterInit : function( editor )
+ {
+ // Prevent word marker line from displaying in elements path and been removed when cleaning format. (#3570) (#4125)
+ var elementsPathFilters,
+ scaytFilter = function( element )
+ {
+ if ( element.hasAttribute( 'data-scaytid' ) )
+ return false;
+ };
+
+ if ( editor._.elementsPath && ( elementsPathFilters = editor._.elementsPath.filters ) )
+ elementsPathFilters.push( scaytFilter );
+
+ editor.addRemoveFormatFilter && editor.addRemoveFormatFilter( scaytFilter );
+
+ }
+ });
+})();
+
+/**
+ * If enabled (true), turns on SCAYT automatically after loading the editor.
+ * @name CKEDITOR.config.scayt_autoStartup
+ * @type Boolean
+ * @default false
+ * @example
+ * config.scayt_autoStartup = true;
+ */
+
+/**
+ * Defines the number of SCAYT suggestions to show in the main context menu.
+ * The possible values are:
+ *
+ * 0 (zero): All suggestions are displayed in the main context menu.
+ * Positive number: The maximum number of suggestions to shown in context
+ * menu. Other entries will be shown in "More Suggestions" sub-menu.
+ * Negative number: No suggestions are shown in the main context menu. All
+ * entries will be listed in the "Suggestions" sub-menu.
+ *
+ * @name CKEDITOR.config.scayt_maxSuggestions
+ * @type Number
+ * @default 5
+ * @example
+ * // Display only three suggestions in the main context menu.
+ * config.scayt_maxSuggestions = 3;
+ * @example
+ * // Do not show the suggestions directly.
+ * config.scayt_maxSuggestions = -1;
+ */
+
+/**
+ * Sets the customer ID for SCAYT. Required for migration from free version
+ * with banner to paid version.
+ * @name CKEDITOR.config.scayt_customerid
+ * @type String
+ * @default ''
+ * @example
+ * // Load SCAYT using my customer ID.
+ * config.scayt_customerid = 'your-encrypted-customer-id';
+ */
+
+/**
+ * Enables/disables the "More Suggestions" sub-menu in the context menu.
+ * The possible values are "on" or "off".
+ * @name CKEDITOR.config.scayt_moreSuggestions
+ * @type String
+ * @default 'on'
+ * @example
+ * // Disables the "More Suggestions" sub-menu.
+ * config.scayt_moreSuggestions = 'off';
+ */
+
+/**
+ * Customizes the display of SCAYT context menu commands ("Add Word", "Ignore"
+ * and "Ignore All"). It must be a string with one or more of the following
+ * words separated by a pipe ("|"):
+ *
+ * "off": disables all options.
+ * "all": enables all options.
+ * "ignore": enables the "Ignore" option.
+ * "ignoreall": enables the "Ignore All" option.
+ * "add": enables the "Add Word" option.
+ *
+ * @name CKEDITOR.config.scayt_contextCommands
+ * @type String
+ * @default 'all'
+ * @example
+ * // Show only "Add Word" and "Ignore All" in the context menu.
+ * config.scayt_contextCommands = 'add|ignoreall';
+ */
+
+/**
+ * Sets the default spellchecking language for SCAYT.
+ * @name CKEDITOR.config.scayt_sLang
+ * @type String
+ * @default 'en_US'
+ * @example
+ * // Sets SCAYT to German.
+ * config.scayt_sLang = 'de_DE';
+ */
+
+/**
+ * Sets the visibility of the SCAYT tabs in the settings dialog and toolbar
+ * button. The value must contain a "1" (enabled) or "0" (disabled) number for
+ * each of the following entries, in this precise order, separated by a
+ * comma (","): "Options", "Languages" and "Dictionary".
+ * @name CKEDITOR.config.scayt_uiTabs
+ * @type String
+ * @default '1,1,1'
+ * @example
+ * // Hide the "Languages" tab.
+ * config.scayt_uiTabs = '1,0,1';
+ */
+
+
+/**
+ * Set the URL to SCAYT core. Required to switch to licensed version of SCAYT application.
+ * Further details at http://wiki.spellchecker.net/doku.php?id=3rd:wysiwyg:fckeditor:wscckf3l .
+ * @name CKEDITOR.config.scayt_srcUrl
+ * @type String
+ * @default ''
+ * @example
+ * config.scayt_srcUrl = "http://my-host/spellcheck/lf/scayt/scayt.js";
+ */
+
+/**
+ * Links SCAYT to custom dictionaries. It's a string containing dictionary ids
+ * separared by commas (","). Available only for licensed version.
+ * Further details at http://wiki.spellchecker.net/doku.php?id=custom_dictionary_support .
+ * @name CKEDITOR.config.scayt_customDictionaryIds
+ * @type String
+ * @default ''
+ * @example
+ * config.scayt_customDictionaryIds = '3021,3456,3478"';
+ */
+
+/**
+ * Makes it possible to activate a custom dictionary on SCAYT. The user
+ * dictionary name must be used. Available only for licensed version.
+ * @name CKEDITOR.config.scayt_userDictionaryName
+ * @type String
+ * @default ''
+ * @example
+ * config.scayt_userDictionaryName = 'MyDictionary';
+ */
+
+/**
+ * Define order of placing of SCAYT context menu items by groups.
+ * It must be a string with one or more of the following
+ * words separated by a pipe ("|"):
+ *
+ * 'suggest' - main suggestion word list,
+ * 'moresuggest' - more suggestions word list,
+ * 'control' - SCAYT commands, such as 'Ignore' and 'Add Word'
+ *
+ *
+ * @name CKEDITOR.config.scayt_contextMenuItemsOrder
+ * @type String
+ * @default 'suggest|moresuggest|control'
+ * @example
+ * config.scayt_contextMenuItemsOrder = 'moresuggest|control|suggest';
+ */
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/selection/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/selection/plugin.js
new file mode 100644
index 00000000..66161f3b
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/selection/plugin.js
@@ -0,0 +1,1562 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+(function()
+{
+ // #### checkSelectionChange : START
+
+ // The selection change check basically saves the element parent tree of
+ // the current node and check it on successive requests. If there is any
+ // change on the tree, then the selectionChange event gets fired.
+ function checkSelectionChange()
+ {
+ try
+ {
+ // In IE, the "selectionchange" event may still get thrown when
+ // releasing the WYSIWYG mode, so we need to check it first.
+ var sel = this.getSelection();
+ if ( !sel || !sel.document.getWindow().$ )
+ return;
+
+ var firstElement = sel.getStartElement();
+ var currentPath = new CKEDITOR.dom.elementPath( firstElement );
+
+ if ( !currentPath.compare( this._.selectionPreviousPath ) )
+ {
+ this._.selectionPreviousPath = currentPath;
+ this.fire( 'selectionChange', { selection : sel, path : currentPath, element : firstElement } );
+ }
+ }
+ catch (e)
+ {}
+ }
+
+ var checkSelectionChangeTimer,
+ checkSelectionChangeTimeoutPending;
+
+ function checkSelectionChangeTimeout()
+ {
+ // Firing the "OnSelectionChange" event on every key press started to
+ // be too slow. This function guarantees that there will be at least
+ // 200ms delay between selection checks.
+
+ checkSelectionChangeTimeoutPending = true;
+
+ if ( checkSelectionChangeTimer )
+ return;
+
+ checkSelectionChangeTimeoutExec.call( this );
+
+ checkSelectionChangeTimer = CKEDITOR.tools.setTimeout( checkSelectionChangeTimeoutExec, 200, this );
+ }
+
+ function checkSelectionChangeTimeoutExec()
+ {
+ checkSelectionChangeTimer = null;
+
+ if ( checkSelectionChangeTimeoutPending )
+ {
+ // Call this with a timeout so the browser properly moves the
+ // selection after the mouseup. It happened that the selection was
+ // being moved after the mouseup when clicking inside selected text
+ // with Firefox.
+ CKEDITOR.tools.setTimeout( checkSelectionChange, 0, this );
+
+ checkSelectionChangeTimeoutPending = false;
+ }
+ }
+
+ // #### checkSelectionChange : END
+
+ function rangeRequiresFix( range )
+ {
+ function isInlineCt( node )
+ {
+ return node && node.type == CKEDITOR.NODE_ELEMENT
+ && node.getName() in CKEDITOR.dtd.$removeEmpty;
+ }
+
+ var start = range.startContainer,
+ offset = range.startOffset;
+
+ if ( start.type == CKEDITOR.NODE_TEXT )
+ return false;
+
+ // 1. Empty inline element. ^
+ // 2. Adjoin to inline element. text ^
+ return !CKEDITOR.tools.trim( start.getHtml() ) ? isInlineCt( start ) : isInlineCt( start.getChild( offset - 1 ) ) || isInlineCt( start.getChild( offset ) );
+ }
+
+ var selectAllCmd =
+ {
+ modes : { wysiwyg : 1, source : 1 },
+ exec : function( editor )
+ {
+ switch ( editor.mode )
+ {
+ case 'wysiwyg' :
+ editor.document.$.execCommand( 'SelectAll', false, null );
+ // Force triggering selectionChange (#7008)
+ editor.forceNextSelectionCheck();
+ editor.selectionChange();
+ break;
+ case 'source' :
+ // Select the contents of the textarea
+ var textarea = editor.textarea.$;
+ if ( CKEDITOR.env.ie )
+ textarea.createTextRange().execCommand( 'SelectAll' );
+ else
+ {
+ textarea.selectionStart = 0;
+ textarea.selectionEnd = textarea.value.length;
+ }
+ textarea.focus();
+ }
+ },
+ canUndo : false
+ };
+
+ function createFillingChar( doc )
+ {
+ removeFillingChar( doc );
+
+ var fillingChar = doc.createText( '\u200B' );
+ doc.setCustomData( 'cke-fillingChar', fillingChar );
+
+ return fillingChar;
+ }
+
+ function getFillingChar( doc )
+ {
+ return doc && doc.getCustomData( 'cke-fillingChar' );
+ }
+
+ // Checks if a filling char has been used, eventualy removing it (#1272).
+ function checkFillingChar( doc )
+ {
+ var fillingChar = doc && getFillingChar( doc );
+ if ( fillingChar )
+ {
+ // Use this flag to avoid removing the filling char right after
+ // creating it.
+ if ( fillingChar.getCustomData( 'ready' ) )
+ removeFillingChar( doc );
+ else
+ fillingChar.setCustomData( 'ready', 1 );
+ }
+ }
+
+ function removeFillingChar( doc )
+ {
+ var fillingChar = doc && doc.removeCustomData( 'cke-fillingChar' );
+ if ( fillingChar )
+ {
+ // We can't simply remove the filling node because the user
+ // will actually enlarge it when typing, so we just remove the
+ // invisible char from it.
+ fillingChar.setText( fillingChar.getText().replace( /\u200B/g, '' ) );
+ fillingChar = 0;
+ }
+ }
+
+ CKEDITOR.plugins.add( 'selection',
+ {
+ init : function( editor )
+ {
+ // On WebKit only, we need a special "filling" char on some situations
+ // (#1272). Here we set the events that should invalidate that char.
+ if ( CKEDITOR.env.webkit )
+ {
+ editor.on( 'selectionChange', function() { checkFillingChar( editor.document ); } );
+ editor.on( 'beforeSetMode', function() { removeFillingChar( editor.document ); } );
+ editor.on( 'key', function( e )
+ {
+ // Remove the filling char before some keys get
+ // executed, so they'll not get blocked by it.
+ switch ( e.data.keyCode )
+ {
+ case 13 : // ENTER
+ case CKEDITOR.SHIFT + 13 : // SHIFT-ENTER
+ case 37 : // LEFT-ARROW
+ case 39 : // RIGHT-ARROW
+ case 8 : // BACKSPACE
+ removeFillingChar( editor.document );
+ }
+ }, null, null, 10 );
+
+ var fillingCharBefore,
+ resetSelection;
+
+ function beforeData()
+ {
+ var doc = editor.document,
+ fillingChar = getFillingChar( doc );
+
+ if ( fillingChar )
+ {
+ // If cursor is right blinking by side of the filler node, save it for restoring,
+ // as the following text substitution will blind it. (#7437)
+ var sel = doc.$.defaultView.getSelection();
+ if ( sel.type == 'Caret' && sel.anchorNode == fillingChar.$ )
+ resetSelection = 1;
+
+ fillingCharBefore = fillingChar.getText();
+ fillingChar.setText( fillingCharBefore.replace( /\u200B/g, '' ) );
+ }
+ }
+ function afterData()
+ {
+ var doc = editor.document,
+ fillingChar = getFillingChar( doc );
+
+ if ( fillingChar )
+ {
+ fillingChar.setText( fillingCharBefore );
+
+ if ( resetSelection )
+ {
+ doc.$.defaultView.getSelection().setPosition( fillingChar.$,fillingChar.getLength() );
+ resetSelection = 0;
+ }
+ }
+ }
+ editor.on( 'beforeUndoImage', beforeData );
+ editor.on( 'afterUndoImage', afterData );
+ editor.on( 'beforeGetData', beforeData, null, null, 0 );
+ editor.on( 'getData', afterData );
+ }
+
+ editor.on( 'contentDom', function()
+ {
+ var doc = editor.document,
+ body = doc.getBody(),
+ html = doc.getDocumentElement();
+
+ if ( CKEDITOR.env.ie )
+ {
+ // Other browsers don't loose the selection if the
+ // editor document loose the focus. In IE, we don't
+ // have support for it, so we reproduce it here, other
+ // than firing the selection change event.
+
+ var savedRange,
+ saveEnabled,
+ restoreEnabled = 1;
+
+ // "onfocusin" is fired before "onfocus". It makes it
+ // possible to restore the selection before click
+ // events get executed.
+ body.on( 'focusin', function( evt )
+ {
+ // If there are elements with layout they fire this event but
+ // it must be ignored to allow edit its contents #4682
+ if ( evt.data.$.srcElement.nodeName != 'BODY' )
+ return;
+
+ // If we have saved a range, restore it at this
+ // point.
+ if ( savedRange )
+ {
+ if ( restoreEnabled )
+ {
+ // Well not break because of this.
+ try
+ {
+ savedRange.select();
+ }
+ catch (e)
+ {}
+
+ // Update locked selection because of the normalized text nodes. (#6083, #6987)
+ var lockedSelection = doc.getCustomData( 'cke_locked_selection' );
+ if ( lockedSelection )
+ {
+ lockedSelection.unlock();
+ lockedSelection.lock();
+ }
+ }
+
+ savedRange = null;
+ }
+ });
+
+ body.on( 'focus', function()
+ {
+ // Enable selections to be saved.
+ saveEnabled = 1;
+
+ saveSelection();
+ });
+
+ body.on( 'beforedeactivate', function( evt )
+ {
+ // Ignore this event if it's caused by focus switch between
+ // internal editable control type elements, e.g. layouted paragraph. (#4682)
+ if ( evt.data.$.toElement )
+ return;
+
+ // Disable selections from being saved.
+ saveEnabled = 0;
+ restoreEnabled = 1;
+ });
+
+ // IE before version 8 will leave cursor blinking inside the document after
+ // editor blurred unless we clean up the selection. (#4716)
+ if ( CKEDITOR.env.ie && CKEDITOR.env.version < 8 )
+ {
+ editor.on( 'blur', function( evt )
+ {
+ // Try/Catch to avoid errors if the editor is hidden. (#6375)
+ try
+ {
+ editor.document && editor.document.$.selection.empty();
+ }
+ catch (e) {}
+ });
+ }
+
+ // Listening on document element ensures that
+ // scrollbar is included. (#5280)
+ html.on( 'mousedown', function()
+ {
+ // Lock restore selection now, as we have
+ // a followed 'click' event which introduce
+ // new selection. (#5735)
+ restoreEnabled = 0;
+ });
+
+ html.on( 'mouseup', function()
+ {
+ restoreEnabled = 1;
+ });
+
+ // In IE6/7 the blinking cursor appears, but contents are
+ // not editable. (#5634)
+ if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.version < 8 || CKEDITOR.env.quirks ) )
+ {
+ // The 'click' event is not fired when clicking the
+ // scrollbars, so we can use it to check whether
+ // the empty space following has been clicked.
+ html.on( 'click', function( evt )
+ {
+ if ( evt.data.getTarget().getName() == 'html' )
+ editor.getSelection().getRanges()[ 0 ].select();
+ });
+ }
+
+ var scroll;
+ // IE fires the "selectionchange" event when clicking
+ // inside a selection. We don't want to capture that.
+ body.on( 'mousedown', function( evt )
+ {
+ // IE scrolls document to top on right mousedown
+ // when editor has no focus, remember this scroll
+ // position and revert it before context menu opens. (#5778)
+ if ( evt.data.$.button == 2 )
+ {
+ var sel = editor.document.$.selection;
+ if ( sel.type == 'None' )
+ scroll = editor.window.getScrollPosition();
+ }
+ disableSave();
+ });
+
+ body.on( 'mouseup',
+ function( evt )
+ {
+ // Restore recorded scroll position when needed on right mouseup.
+ if ( evt.data.$.button == 2 && scroll )
+ {
+ editor.document.$.documentElement.scrollLeft = scroll.x;
+ editor.document.$.documentElement.scrollTop = scroll.y;
+ }
+ scroll = null;
+
+ saveEnabled = 1;
+ setTimeout( function()
+ {
+ saveSelection( true );
+ },
+ 0 );
+ });
+
+ body.on( 'keydown', disableSave );
+ body.on( 'keyup',
+ function()
+ {
+ saveEnabled = 1;
+ saveSelection();
+ });
+
+
+ // IE is the only to provide the "selectionchange"
+ // event.
+ doc.on( 'selectionchange', saveSelection );
+
+ function disableSave()
+ {
+ saveEnabled = 0;
+ }
+
+ function saveSelection( testIt )
+ {
+ if ( saveEnabled )
+ {
+ var doc = editor.document,
+ sel = editor.getSelection(),
+ nativeSel = sel && sel.getNative();
+
+ // There is a very specific case, when clicking
+ // inside a text selection. In that case, the
+ // selection collapses at the clicking point,
+ // but the selection object remains in an
+ // unknown state, making createRange return a
+ // range at the very start of the document. In
+ // such situation we have to test the range, to
+ // be sure it's valid.
+ if ( testIt && nativeSel && nativeSel.type == 'None' )
+ {
+ // The "InsertImage" command can be used to
+ // test whether the selection is good or not.
+ // If not, it's enough to give some time to
+ // IE to put things in order for us.
+ if ( !doc.$.queryCommandEnabled( 'InsertImage' ) )
+ {
+ CKEDITOR.tools.setTimeout( saveSelection, 50, this, true );
+ return;
+ }
+ }
+
+ // Avoid saving selection from within text input. (#5747)
+ var parentTag;
+ if ( nativeSel && nativeSel.type && nativeSel.type != 'Control'
+ && ( parentTag = nativeSel.createRange() )
+ && ( parentTag = parentTag.parentElement() )
+ && ( parentTag = parentTag.nodeName )
+ && parentTag.toLowerCase() in { input: 1, textarea : 1 } )
+ {
+ return;
+ }
+
+ savedRange = nativeSel && sel.getRanges()[ 0 ];
+
+ checkSelectionChangeTimeout.call( editor );
+ }
+ }
+ }
+ else
+ {
+ // In other browsers, we make the selection change
+ // check based on other events, like clicks or keys
+ // press.
+
+ doc.on( 'mouseup', checkSelectionChangeTimeout, editor );
+ doc.on( 'keyup', checkSelectionChangeTimeout, editor );
+ }
+ });
+
+ // Clear the cached range path before unload. (#7174)
+ editor.on( 'contentDomUnload', editor.forceNextSelectionCheck, editor );
+
+ editor.addCommand( 'selectAll', selectAllCmd );
+ editor.ui.addButton( 'SelectAll',
+ {
+ label : editor.lang.selectAll,
+ command : 'selectAll'
+ });
+
+ editor.selectionChange = checkSelectionChangeTimeout;
+ }
+ });
+
+ /**
+ * Gets the current selection from the editing area when in WYSIWYG mode.
+ * @returns {CKEDITOR.dom.selection} A selection object or null if not on
+ * WYSIWYG mode or no selection is available.
+ * @example
+ * var selection = CKEDITOR.instances.editor1.getSelection() ;
+ * alert( selection.getType() );
+ */
+ CKEDITOR.editor.prototype.getSelection = function()
+ {
+ return this.document && this.document.getSelection();
+ };
+
+ CKEDITOR.editor.prototype.forceNextSelectionCheck = function()
+ {
+ delete this._.selectionPreviousPath;
+ };
+
+ /**
+ * Gets the current selection from the document.
+ * @returns {CKEDITOR.dom.selection} A selection object.
+ * @example
+ * var selection = CKEDITOR.instances.editor1.document.getSelection() ;
+ * alert( selection.getType() );
+ */
+ CKEDITOR.dom.document.prototype.getSelection = function()
+ {
+ var sel = new CKEDITOR.dom.selection( this );
+ return ( !sel || sel.isInvalid ) ? null : sel;
+ };
+
+ /**
+ * No selection.
+ * @constant
+ * @example
+ * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_NONE )
+ * alert( 'Nothing is selected' );
+ */
+ CKEDITOR.SELECTION_NONE = 1;
+
+ /**
+ * Text or collapsed selection.
+ * @constant
+ * @example
+ * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
+ * alert( 'Text is selected' );
+ */
+ CKEDITOR.SELECTION_TEXT = 2;
+
+ /**
+ * Element selection.
+ * @constant
+ * @example
+ * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_ELEMENT )
+ * alert( 'An element is selected' );
+ */
+ CKEDITOR.SELECTION_ELEMENT = 3;
+
+ /**
+ * Manipulates the selection in a DOM document.
+ * @constructor
+ * @example
+ */
+ CKEDITOR.dom.selection = function( document )
+ {
+ var lockedSelection = document.getCustomData( 'cke_locked_selection' );
+
+ if ( lockedSelection )
+ return lockedSelection;
+
+ this.document = document;
+ this.isLocked = 0;
+ this._ =
+ {
+ cache : {}
+ };
+
+ /**
+ * IE BUG: The selection's document may be a different document than the
+ * editor document. Return null if that's the case.
+ */
+ if ( CKEDITOR.env.ie )
+ {
+ var range = this.getNative().createRange();
+ if ( !range
+ || ( range.item && range.item(0).ownerDocument != this.document.$ )
+ || ( range.parentElement && range.parentElement().ownerDocument != this.document.$ ) )
+ {
+ this.isInvalid = true;
+ }
+ }
+
+ return this;
+ };
+
+ var styleObjectElements =
+ {
+ img:1,hr:1,li:1,table:1,tr:1,td:1,th:1,embed:1,object:1,ol:1,ul:1,
+ a:1, input:1, form:1, select:1, textarea:1, button:1, fieldset:1, th:1, thead:1, tfoot:1
+ };
+
+ CKEDITOR.dom.selection.prototype =
+ {
+ /**
+ * Gets the native selection object from the browser.
+ * @function
+ * @returns {Object} The native selection object.
+ * @example
+ * var selection = editor.getSelection().getNative() ;
+ */
+ getNative :
+ CKEDITOR.env.ie ?
+ function()
+ {
+ return this._.cache.nativeSel || ( this._.cache.nativeSel = this.document.$.selection );
+ }
+ :
+ function()
+ {
+ return this._.cache.nativeSel || ( this._.cache.nativeSel = this.document.getWindow().$.getSelection() );
+ },
+
+ /**
+ * Gets the type of the current selection. The following values are
+ * available:
+ *
+ * {@link CKEDITOR.SELECTION_NONE} (1): No selection.
+ * {@link CKEDITOR.SELECTION_TEXT} (2): Text is selected or
+ * collapsed selection.
+ * {@link CKEDITOR.SELECTION_ELEMENT} (3): A element
+ * selection.
+ *
+ * @function
+ * @returns {Number} One of the following constant values:
+ * {@link CKEDITOR.SELECTION_NONE}, {@link CKEDITOR.SELECTION_TEXT} or
+ * {@link CKEDITOR.SELECTION_ELEMENT}.
+ * @example
+ * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
+ * alert( 'Text is selected' );
+ */
+ getType :
+ CKEDITOR.env.ie ?
+ function()
+ {
+ var cache = this._.cache;
+ if ( cache.type )
+ return cache.type;
+
+ var type = CKEDITOR.SELECTION_NONE;
+
+ try
+ {
+ var sel = this.getNative(),
+ ieType = sel.type;
+
+ if ( ieType == 'Text' )
+ type = CKEDITOR.SELECTION_TEXT;
+
+ if ( ieType == 'Control' )
+ type = CKEDITOR.SELECTION_ELEMENT;
+
+ // It is possible that we can still get a text range
+ // object even when type == 'None' is returned by IE.
+ // So we'd better check the object returned by
+ // createRange() rather than by looking at the type.
+ if ( sel.createRange().parentElement )
+ type = CKEDITOR.SELECTION_TEXT;
+ }
+ catch(e) {}
+
+ return ( cache.type = type );
+ }
+ :
+ function()
+ {
+ var cache = this._.cache;
+ if ( cache.type )
+ return cache.type;
+
+ var type = CKEDITOR.SELECTION_TEXT;
+
+ var sel = this.getNative();
+
+ if ( !sel )
+ type = CKEDITOR.SELECTION_NONE;
+ else if ( sel.rangeCount == 1 )
+ {
+ // Check if the actual selection is a control (IMG,
+ // TABLE, HR, etc...).
+
+ var range = sel.getRangeAt(0),
+ startContainer = range.startContainer;
+
+ if ( startContainer == range.endContainer
+ && startContainer.nodeType == 1
+ && ( range.endOffset - range.startOffset ) == 1
+ && styleObjectElements[ startContainer.childNodes[ range.startOffset ].nodeName.toLowerCase() ] )
+ {
+ type = CKEDITOR.SELECTION_ELEMENT;
+ }
+ }
+
+ return ( cache.type = type );
+ },
+
+ /**
+ * Retrieve the {@link CKEDITOR.dom.range} instances that represent the current selection.
+ * Note: Some browsers returns multiple ranges even on a sequent selection, e.g. Firefox returns
+ * one range for each table cell when one or more table row is selected.
+ * @return {Array}
+ * @example
+ * var ranges = selection.getRanges();
+ * alert(ranges.length);
+ */
+ getRanges : (function()
+ {
+ var func = CKEDITOR.env.ie ?
+ ( function()
+ {
+ function getNodeIndex( node ) { return new CKEDITOR.dom.node( node ).getIndex(); }
+
+ // Finds the container and offset for a specific boundary
+ // of an IE range.
+ var getBoundaryInformation = function( range, start )
+ {
+ // Creates a collapsed range at the requested boundary.
+ range = range.duplicate();
+ range.collapse( start );
+
+ // Gets the element that encloses the range entirely.
+ var parent = range.parentElement(),
+ doc = parent.ownerDocument;
+
+ // Empty parent element, e.g. ^
+ if ( !parent.hasChildNodes() )
+ return { container : parent, offset : 0 };
+
+ var siblings = parent.children,
+ child,
+ sibling,
+ testRange = range.duplicate(),
+ startIndex = 0,
+ endIndex = siblings.length - 1,
+ index = -1,
+ position,
+ distance;
+
+ // Binary search over all element childs to test the range to see whether
+ // range is right on the boundary of one element.
+ while ( startIndex <= endIndex )
+ {
+ index = Math.floor( ( startIndex + endIndex ) / 2 );
+ child = siblings[ index ];
+ testRange.moveToElementText( child );
+ position = testRange.compareEndPoints( 'StartToStart', range );
+
+ if ( position > 0 )
+ endIndex = index - 1;
+ else if ( position < 0 )
+ startIndex = index + 1;
+ else
+ {
+ // IE9 report wrong measurement with compareEndPoints when range anchors between two BRs.
+ // e.g. text ^
(#7433)
+ if ( CKEDITOR.env.ie9Compat && child.tagName == 'BR' )
+ {
+ var bmId = 'cke_range_marker';
+ range.execCommand( 'CreateBookmark', false, bmId );
+ child = doc.getElementsByName( bmId )[ 0 ];
+ var offset = getNodeIndex( child );
+ parent.removeChild( child );
+ return { container : parent, offset : offset };
+ }
+ else
+ return { container : parent, offset : getNodeIndex( child ) };
+ }
+ }
+
+ // All childs are text nodes,
+ // or to the right hand of test range are all text nodes. (#6992)
+ if ( index == -1 || index == siblings.length - 1 && position < 0 )
+ {
+ // Adapt test range to embrace the entire parent contents.
+ testRange.moveToElementText( parent );
+ testRange.setEndPoint( 'StartToStart', range );
+
+ // IE report line break as CRLF with range.text but
+ // only LF with textnode.nodeValue, normalize them to avoid
+ // breaking character counting logic below. (#3949)
+ distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
+
+ siblings = parent.childNodes;
+
+ // Actual range anchor right beside test range at the boundary of text node.
+ if ( !distance )
+ {
+ child = siblings[ siblings.length - 1 ];
+
+ if ( child.nodeType == CKEDITOR.NODE_ELEMENT )
+ return { container : parent, offset : siblings.length };
+ else
+ return { container : child, offset : child.nodeValue.length };
+ }
+
+ // Start the measuring until distance overflows, meanwhile count the text nodes.
+ var i = siblings.length;
+ while ( distance > 0 )
+ distance -= siblings[ --i ].nodeValue.length;
+
+ return { container : siblings[ i ], offset : -distance };
+ }
+ // Test range was one offset beyond OR behind the anchored text node.
+ else
+ {
+ // Adapt one side of test range to the actual range
+ // for measuring the offset between them.
+ testRange.collapse( position > 0 ? true : false );
+ testRange.setEndPoint( position > 0 ? 'StartToStart' : 'EndToStart', range );
+
+ // IE report line break as CRLF with range.text but
+ // only LF with textnode.nodeValue, normalize them to avoid
+ // breaking character counting logic below. (#3949)
+ distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
+
+ // Actual range anchor right beside test range at the inner boundary of text node.
+ if ( !distance )
+ return { container : parent, offset : getNodeIndex( child ) + ( position > 0 ? 0 : 1 ) };
+
+ // Start the measuring until distance overflows, meanwhile count the text nodes.
+ while ( distance > 0 )
+ {
+ try
+ {
+ sibling = child[ position > 0 ? 'previousSibling' : 'nextSibling' ];
+ distance -= sibling.nodeValue.length;
+ child = sibling;
+ }
+ // Measurement in IE could be somtimes wrong because of element. (#4611)
+ catch( e )
+ {
+ return { container : parent, offset : getNodeIndex( child ) };
+ }
+ }
+
+ return { container : child, offset : position > 0 ? -distance : child.nodeValue.length + distance };
+ }
+ };
+
+ return function()
+ {
+ // IE doesn't have range support (in the W3C way), so we
+ // need to do some magic to transform selections into
+ // CKEDITOR.dom.range instances.
+
+ var sel = this.getNative(),
+ nativeRange = sel && sel.createRange(),
+ type = this.getType(),
+ range;
+
+ if ( !sel )
+ return [];
+
+ if ( type == CKEDITOR.SELECTION_TEXT )
+ {
+ range = new CKEDITOR.dom.range( this.document );
+
+ var boundaryInfo = getBoundaryInformation( nativeRange, true );
+ range.setStart( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
+
+ boundaryInfo = getBoundaryInformation( nativeRange );
+ range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
+
+ // Correct an invalid IE range case on empty list item. (#5850)
+ if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING
+ && range.endOffset <= range.startContainer.getIndex() )
+ {
+ range.collapse();
+ }
+
+ return [ range ];
+ }
+ else if ( type == CKEDITOR.SELECTION_ELEMENT )
+ {
+ var retval = [];
+
+ for ( var i = 0 ; i < nativeRange.length ; i++ )
+ {
+ var element = nativeRange.item( i ),
+ parentElement = element.parentNode,
+ j = 0;
+
+ range = new CKEDITOR.dom.range( this.document );
+
+ for (; j < parentElement.childNodes.length && parentElement.childNodes[j] != element ; j++ )
+ { /*jsl:pass*/ }
+
+ range.setStart( new CKEDITOR.dom.node( parentElement ), j );
+ range.setEnd( new CKEDITOR.dom.node( parentElement ), j + 1 );
+ retval.push( range );
+ }
+
+ return retval;
+ }
+
+ return [];
+ };
+ })()
+ :
+ function()
+ {
+
+ // On browsers implementing the W3C range, we simply
+ // tranform the native ranges in CKEDITOR.dom.range
+ // instances.
+
+ var ranges = [],
+ range,
+ doc = this.document,
+ sel = this.getNative();
+
+ if ( !sel )
+ return ranges;
+
+ // On WebKit, it may happen that we'll have no selection
+ // available. We normalize it here by replicating the
+ // behavior of other browsers.
+ if ( !sel.rangeCount )
+ {
+ range = new CKEDITOR.dom.range( doc );
+ range.moveToElementEditStart( doc.getBody() );
+ ranges.push( range );
+ }
+
+ for ( var i = 0 ; i < sel.rangeCount ; i++ )
+ {
+ var nativeRange = sel.getRangeAt( i );
+
+ range = new CKEDITOR.dom.range( doc );
+
+ range.setStart( new CKEDITOR.dom.node( nativeRange.startContainer ), nativeRange.startOffset );
+ range.setEnd( new CKEDITOR.dom.node( nativeRange.endContainer ), nativeRange.endOffset );
+ ranges.push( range );
+ }
+ return ranges;
+ };
+
+ return function( onlyEditables )
+ {
+ var cache = this._.cache;
+ if ( cache.ranges && !onlyEditables )
+ return cache.ranges;
+ else if ( !cache.ranges )
+ cache.ranges = new CKEDITOR.dom.rangeList( func.call( this ) );
+
+ // Split range into multiple by read-only nodes.
+ if ( onlyEditables )
+ {
+ var ranges = cache.ranges;
+ for ( var i = 0; i < ranges.length; i++ )
+ {
+ var range = ranges[ i ];
+
+ // Drop range spans inside one ready-only node.
+ var parent = range.getCommonAncestor();
+ if ( parent.isReadOnly() )
+ ranges.splice( i, 1 );
+
+ if ( range.collapsed )
+ continue;
+
+ var startContainer = range.startContainer,
+ endContainer = range.endContainer,
+ startOffset = range.startOffset,
+ endOffset = range.endOffset,
+ walkerRange = range.clone();
+
+ // Range may start inside a non-editable element, restart range
+ // by the end of it.
+ var readOnly;
+ if ( ( readOnly = startContainer.isReadOnly() ) )
+ range.setStartAfter( readOnly );
+
+ // Enlarge range start/end with text node to avoid walker
+ // being DOM destructive, it doesn't interfere our checking
+ // of elements below as well.
+ if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
+ {
+ if ( startOffset >= startContainer.getLength() )
+ walkerRange.setStartAfter( startContainer );
+ else
+ walkerRange.setStartBefore( startContainer );
+ }
+
+ if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
+ {
+ if ( !endOffset )
+ walkerRange.setEndBefore( endContainer );
+ else
+ walkerRange.setEndAfter( endContainer );
+ }
+
+ // Looking for non-editable element inside the range.
+ var walker = new CKEDITOR.dom.walker( walkerRange );
+ walker.evaluator = function( node )
+ {
+ if ( node.type == CKEDITOR.NODE_ELEMENT
+ && node.isReadOnly() )
+ {
+ var newRange = range.clone();
+ range.setEndBefore( node );
+
+ // Drop collapsed range around read-only elements,
+ // it make sure the range list empty when selecting
+ // only non-editable elements.
+ if ( range.collapsed )
+ ranges.splice( i--, 1 );
+
+ // Avoid creating invalid range.
+ if ( !( node.getPosition( walkerRange.endContainer ) & CKEDITOR.POSITION_CONTAINS ) )
+ {
+ newRange.setStartAfter( node );
+ if ( !newRange.collapsed )
+ ranges.splice( i + 1, 0, newRange );
+ }
+
+ return true;
+ }
+
+ return false;
+ };
+
+ walker.next();
+ }
+ }
+
+ return cache.ranges;
+ };
+ })(),
+
+ /**
+ * Gets the DOM element in which the selection starts.
+ * @returns {CKEDITOR.dom.element} The element at the beginning of the
+ * selection.
+ * @example
+ * var element = editor.getSelection().getStartElement() ;
+ * alert( element.getName() );
+ */
+ getStartElement : function()
+ {
+ var cache = this._.cache;
+ if ( cache.startElement !== undefined )
+ return cache.startElement;
+
+ var node,
+ sel = this.getNative();
+
+ switch ( this.getType() )
+ {
+ case CKEDITOR.SELECTION_ELEMENT :
+ return this.getSelectedElement();
+
+ case CKEDITOR.SELECTION_TEXT :
+
+ var range = this.getRanges()[0];
+
+ if ( range )
+ {
+ if ( !range.collapsed )
+ {
+ range.optimize();
+
+ // Decrease the range content to exclude particial
+ // selected node on the start which doesn't have
+ // visual impact. ( #3231 )
+ while ( 1 )
+ {
+ var startContainer = range.startContainer,
+ startOffset = range.startOffset;
+ // Limit the fix only to non-block elements.(#3950)
+ if ( startOffset == ( startContainer.getChildCount ?
+ startContainer.getChildCount() : startContainer.getLength() )
+ && !startContainer.isBlockBoundary() )
+ range.setStartAfter( startContainer );
+ else break;
+ }
+
+ node = range.startContainer;
+
+ if ( node.type != CKEDITOR.NODE_ELEMENT )
+ return node.getParent();
+
+ node = node.getChild( range.startOffset );
+
+ if ( !node || node.type != CKEDITOR.NODE_ELEMENT )
+ node = range.startContainer;
+ else
+ {
+ var child = node.getFirst();
+ while ( child && child.type == CKEDITOR.NODE_ELEMENT )
+ {
+ node = child;
+ child = child.getFirst();
+ }
+ }
+ }
+ else
+ {
+ node = range.startContainer;
+ if ( node.type != CKEDITOR.NODE_ELEMENT )
+ node = node.getParent();
+ }
+
+ node = node.$;
+ }
+ }
+
+ return cache.startElement = ( node ? new CKEDITOR.dom.element( node ) : null );
+ },
+
+ /**
+ * Gets the current selected element.
+ * @returns {CKEDITOR.dom.element} The selected element. Null if no
+ * selection is available or the selection type is not
+ * {@link CKEDITOR.SELECTION_ELEMENT}.
+ * @example
+ * var element = editor.getSelection().getSelectedElement() ;
+ * alert( element.getName() );
+ */
+ getSelectedElement : function()
+ {
+ var cache = this._.cache;
+ if ( cache.selectedElement !== undefined )
+ return cache.selectedElement;
+
+ var self = this;
+
+ var node = CKEDITOR.tools.tryThese(
+ // Is it native IE control type selection?
+ function()
+ {
+ return self.getNative().createRange().item( 0 );
+ },
+ // Figure it out by checking if there's a single enclosed
+ // node of the range.
+ function()
+ {
+ var range = self.getRanges()[ 0 ],
+ enclosed,
+ selected;
+
+ // Check first any enclosed element, e.g.
+ for ( var i = 2; i && !( ( enclosed = range.getEnclosedNode() )
+ && ( enclosed.type == CKEDITOR.NODE_ELEMENT )
+ && styleObjectElements[ enclosed.getName() ]
+ && ( selected = enclosed ) ); i-- )
+ {
+ // Then check any deep wrapped element, e.g. [ ]
+ range.shrink( CKEDITOR.SHRINK_ELEMENT );
+ }
+
+ return selected.$;
+ });
+
+ return cache.selectedElement = ( node ? new CKEDITOR.dom.element( node ) : null );
+ },
+
+ lock : function()
+ {
+ // Call all cacheable function.
+ this.getRanges();
+ this.getStartElement();
+ this.getSelectedElement();
+
+ // The native selection is not available when locked.
+ this._.cache.nativeSel = {};
+
+ this.isLocked = 1;
+
+ // Save this selection inside the DOM document.
+ this.document.setCustomData( 'cke_locked_selection', this );
+ },
+
+ unlock : function( restore )
+ {
+ var doc = this.document,
+ lockedSelection = doc.getCustomData( 'cke_locked_selection' );
+
+ if ( lockedSelection )
+ {
+ doc.setCustomData( 'cke_locked_selection', null );
+
+ if ( restore )
+ {
+ var selectedElement = lockedSelection.getSelectedElement(),
+ ranges = !selectedElement && lockedSelection.getRanges();
+
+ this.isLocked = 0;
+ this.reset();
+
+ doc.getBody().focus();
+
+ if ( selectedElement )
+ this.selectElement( selectedElement );
+ else
+ this.selectRanges( ranges );
+ }
+ }
+
+ if ( !lockedSelection || !restore )
+ {
+ this.isLocked = 0;
+ this.reset();
+ }
+ },
+
+ reset : function()
+ {
+ this._.cache = {};
+ },
+
+ /**
+ * Make the current selection of type {@link CKEDITOR.SELECTION_ELEMENT} by enclosing the specified element.
+ * @param element
+ */
+ selectElement : function( element )
+ {
+ if ( this.isLocked )
+ {
+ var range = new CKEDITOR.dom.range( this.document );
+ range.setStartBefore( element );
+ range.setEndAfter( element );
+
+ this._.cache.selectedElement = element;
+ this._.cache.startElement = element;
+ this._.cache.ranges = new CKEDITOR.dom.rangeList( range );
+ this._.cache.type = CKEDITOR.SELECTION_ELEMENT;
+
+ return;
+ }
+
+ range = new CKEDITOR.dom.range( element.getDocument() );
+ range.setStartBefore( element );
+ range.setEndAfter( element );
+ range.select();
+
+ this.document.fire( 'selectionchange' );
+ this.reset();
+
+ },
+
+ /**
+ * Adding the specified ranges to document selection preceding
+ * by clearing up the original selection.
+ * @param {CKEDITOR.dom.range} ranges
+ */
+ selectRanges : function( ranges )
+ {
+ if ( this.isLocked )
+ {
+ this._.cache.selectedElement = null;
+ this._.cache.startElement = ranges[ 0 ] && ranges[ 0 ].getTouchedStartNode();
+ this._.cache.ranges = new CKEDITOR.dom.rangeList( ranges );
+ this._.cache.type = CKEDITOR.SELECTION_TEXT;
+
+ return;
+ }
+
+ if ( CKEDITOR.env.ie )
+ {
+ if ( ranges.length > 1 )
+ {
+ // IE doesn't accept multiple ranges selection, so we join all into one.
+ var last = ranges[ ranges.length -1 ] ;
+ ranges[ 0 ].setEnd( last.endContainer, last.endOffset );
+ ranges.length = 1;
+ }
+
+ if ( ranges[ 0 ] )
+ ranges[ 0 ].select();
+
+ this.reset();
+ }
+ else
+ {
+ var sel = this.getNative();
+
+ // getNative() returns null if iframe is "display:none" in FF. (#6577)
+ if ( !sel )
+ return;
+
+ if ( ranges.length )
+ {
+ sel.removeAllRanges();
+ // Remove any existing filling char first.
+ CKEDITOR.env.webkit && removeFillingChar( this.document );
+ }
+
+ for ( var i = 0 ; i < ranges.length ; i++ )
+ {
+ // Joining sequential ranges introduced by
+ // readonly elements protection.
+ if ( i < ranges.length -1 )
+ {
+ var left = ranges[ i ], right = ranges[ i +1 ],
+ between = left.clone();
+ between.setStart( left.endContainer, left.endOffset );
+ between.setEnd( right.startContainer, right.startOffset );
+
+ // Don't confused by Firefox adjancent multi-ranges
+ // introduced by table cells selection.
+ if ( !between.collapsed )
+ {
+ between.shrink( CKEDITOR.NODE_ELEMENT, true );
+ var ancestor = between.getCommonAncestor(),
+ enclosed = between.getEnclosedNode();
+
+ // The following cases has to be considered:
+ // 1. [placeholder]
+ // 2. (#6621)
+ if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() )
+ {
+ right.setStart( left.startContainer, left.startOffset );
+ ranges.splice( i--, 1 );
+ continue;
+ }
+ }
+ }
+
+ var range = ranges[ i ];
+ var nativeRange = this.document.$.createRange();
+ var startContainer = range.startContainer;
+
+ // In FF2, if we have a collapsed range, inside an empty
+ // element, we must add something to it otherwise the caret
+ // will not be visible.
+ // In Opera instead, the selection will be moved out of the
+ // element. (#4657)
+ if ( range.collapsed &&
+ ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.version < 10900 ) ) &&
+ startContainer.type == CKEDITOR.NODE_ELEMENT &&
+ !startContainer.getChildCount() )
+ {
+ startContainer.appendText( '' );
+ }
+
+ if ( range.collapsed
+ && CKEDITOR.env.webkit
+ && rangeRequiresFix( range ) )
+ {
+ // Append a zero-width space so WebKit will not try to
+ // move the selection by itself (#1272).
+ var fillingChar = createFillingChar( this.document );
+ range.insertNode( fillingChar ) ;
+
+ var next = fillingChar.getNext();
+
+ // If the filling char is followed by a , whithout
+ // having something before it, it'll not blink.
+ // Let's remove it in this case.
+ if ( next && !fillingChar.getPrevious() && next.type == CKEDITOR.NODE_ELEMENT && next.getName() == 'br' )
+ {
+ removeFillingChar( this.document );
+ range.moveToPosition( next, CKEDITOR.POSITION_BEFORE_START );
+ }
+ else
+ range.moveToPosition( fillingChar, CKEDITOR.POSITION_AFTER_END );
+ }
+
+ nativeRange.setStart( range.startContainer.$, range.startOffset );
+
+ try
+ {
+ nativeRange.setEnd( range.endContainer.$, range.endOffset );
+ }
+ catch ( e )
+ {
+ // There is a bug in Firefox implementation (it would be too easy
+ // otherwise). The new start can't be after the end (W3C says it can).
+ // So, let's create a new range and collapse it to the desired point.
+ if ( e.toString().indexOf( 'NS_ERROR_ILLEGAL_VALUE' ) >= 0 )
+ {
+ range.collapse( 1 );
+ nativeRange.setEnd( range.endContainer.$, range.endOffset );
+ }
+ else
+ throw e;
+ }
+
+ // Select the range.
+ sel.addRange( nativeRange );
+ }
+
+ this.reset();
+ }
+ },
+
+ /**
+ * Create bookmark for every single of this selection range (from #getRanges)
+ * by calling the {@link CKEDITOR.dom.range.prototype.createBookmark} method,
+ * with extra cares to avoid interferon among those ranges. Same arguments are
+ * received as with the underlay range method.
+ */
+ createBookmarks : function( serializable )
+ {
+ return this.getRanges().createBookmarks( serializable );
+ },
+
+ /**
+ * Create bookmark for every single of this selection range (from #getRanges)
+ * by calling the {@link CKEDITOR.dom.range.prototype.createBookmark2} method,
+ * with extra cares to avoid interferon among those ranges. Same arguments are
+ * received as with the underlay range method.
+ */
+ createBookmarks2 : function( normalized )
+ {
+ return this.getRanges().createBookmarks2( normalized );
+ },
+
+ /**
+ * Select the virtual ranges denote by the bookmarks by calling #selectRanges.
+ * @param bookmarks
+ */
+ selectBookmarks : function( bookmarks )
+ {
+ var ranges = [];
+ for ( var i = 0 ; i < bookmarks.length ; i++ )
+ {
+ var range = new CKEDITOR.dom.range( this.document );
+ range.moveToBookmark( bookmarks[i] );
+ ranges.push( range );
+ }
+ this.selectRanges( ranges );
+ return this;
+ },
+
+ /**
+ * Retrieve the common ancestor node of the first range and the last range.
+ */
+ getCommonAncestor : function()
+ {
+ var ranges = this.getRanges(),
+ startNode = ranges[ 0 ].startContainer,
+ endNode = ranges[ ranges.length - 1 ].endContainer;
+ return startNode.getCommonAncestor( endNode );
+ },
+
+ /**
+ * Moving scroll bar to the current selection's start position.
+ */
+ scrollIntoView : function()
+ {
+ // If we have split the block, adds a temporary span at the
+ // range position and scroll relatively to it.
+ var start = this.getStartElement();
+ start.scrollIntoView();
+ }
+ };
+})();
+
+( function()
+{
+ var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
+ fillerTextRegex = /\ufeff|\u00a0/,
+ nonCells = { table:1,tbody:1,tr:1 };
+
+ CKEDITOR.dom.range.prototype.select =
+ CKEDITOR.env.ie ?
+ // V2
+ function( forceExpand )
+ {
+ var collapsed = this.collapsed,
+ isStartMarkerAlone, dummySpan, ieRange;
+
+ // Try to make a object selection.
+ var selected = this.getEnclosedNode();
+ if ( selected )
+ {
+ try
+ {
+ ieRange = this.document.$.body.createControlRange();
+ ieRange.addElement( selected.$ );
+ ieRange.select();
+ return;
+ }
+ catch( er ) {}
+ }
+
+ // IE doesn't support selecting the entire table row/cell, move the selection into cells, e.g.
+ // [cell ... => [cell ...
+ if ( this.startContainer.type == CKEDITOR.NODE_ELEMENT && this.startContainer.getName() in nonCells
+ || this.endContainer.type == CKEDITOR.NODE_ELEMENT && this.endContainer.getName() in nonCells )
+ {
+ this.shrink( CKEDITOR.NODE_ELEMENT, true );
+ }
+
+ var bookmark = this.createBookmark();
+
+ // Create marker tags for the start and end boundaries.
+ var startNode = bookmark.startNode;
+
+ var endNode;
+ if ( !collapsed )
+ endNode = bookmark.endNode;
+
+ // Create the main range which will be used for the selection.
+ ieRange = this.document.$.body.createTextRange();
+
+ // Position the range at the start boundary.
+ ieRange.moveToElementText( startNode.$ );
+ ieRange.moveStart( 'character', 1 );
+
+ if ( endNode )
+ {
+ // Create a tool range for the end.
+ var ieRangeEnd = this.document.$.body.createTextRange();
+
+ // Position the tool range at the end.
+ ieRangeEnd.moveToElementText( endNode.$ );
+
+ // Move the end boundary of the main range to match the tool range.
+ ieRange.setEndPoint( 'EndToEnd', ieRangeEnd );
+ ieRange.moveEnd( 'character', -1 );
+ }
+ else
+ {
+ // The isStartMarkerAlone logic comes from V2. It guarantees that the lines
+ // will expand and that the cursor will be blinking on the right place.
+ // Actually, we are using this flag just to avoid using this hack in all
+ // situations, but just on those needed.
+ var next = startNode.getNext( notWhitespaces );
+ isStartMarkerAlone = ( !( next && next.getText && next.getText().match( fillerTextRegex ) ) // already a filler there?
+ && ( forceExpand || !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ) ) );
+
+ // Append a temporary before the selection.
+ // This is needed to avoid IE destroying selections inside empty
+ // inline elements, like (#253).
+ // It is also needed when placing the selection right after an inline
+ // element to avoid the selection moving inside of it.
+ dummySpan = this.document.createElement( 'span' );
+ dummySpan.setHtml( '' ); // Zero Width No-Break Space (U+FEFF). See #1359.
+ dummySpan.insertBefore( startNode );
+
+ if ( isStartMarkerAlone )
+ {
+ // To expand empty blocks or line spaces after , we need
+ // instead to have any char, which will be later deleted using the
+ // selection.
+ // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359)
+ this.document.createText( '\ufeff' ).insertBefore( startNode );
+ }
+ }
+
+ // Remove the markers (reset the position, because of the changes in the DOM tree).
+ this.setStartBefore( startNode );
+ startNode.remove();
+
+ if ( collapsed )
+ {
+ if ( isStartMarkerAlone )
+ {
+ // Move the selection start to include the temporary \ufeff.
+ ieRange.moveStart( 'character', -1 );
+
+ ieRange.select();
+
+ // Remove our temporary stuff.
+ this.document.$.selection.clear();
+ }
+ else
+ ieRange.select();
+
+ this.moveToPosition( dummySpan, CKEDITOR.POSITION_BEFORE_START );
+ dummySpan.remove();
+ }
+ else
+ {
+ this.setEndBefore( endNode );
+ endNode.remove();
+ ieRange.select();
+ }
+
+ this.document.fire( 'selectionchange' );
+ }
+ :
+ function()
+ {
+ this.document.getSelection().selectRanges( [ this ] );
+ };
+} )();
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_address.png b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_address.png
new file mode 100644
index 00000000..1ee67aa2
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_address.png differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_blockquote.png b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_blockquote.png
new file mode 100644
index 00000000..cae3aec6
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_blockquote.png differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_div.png b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_div.png
new file mode 100644
index 00000000..c71f3978
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_div.png differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h1.png b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h1.png
new file mode 100644
index 00000000..3a643473
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h1.png differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h2.png b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h2.png
new file mode 100644
index 00000000..8062ebe8
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h2.png differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h3.png b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h3.png
new file mode 100644
index 00000000..5b6a4030
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h3.png differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h4.png b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h4.png
new file mode 100644
index 00000000..6c7f7951
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h4.png differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h5.png b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h5.png
new file mode 100644
index 00000000..e153de09
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h5.png differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h6.png b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h6.png
new file mode 100644
index 00000000..c8d993a9
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_h6.png differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_p.png b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_p.png
new file mode 100644
index 00000000..4e6035d2
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_p.png differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_pre.png b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_pre.png
new file mode 100644
index 00000000..d11a0fff
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/images/block_pre.png differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showblocks/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/plugin.js
new file mode 100644
index 00000000..cbec3141
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/showblocks/plugin.js
@@ -0,0 +1,156 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+/**
+ * @fileOverview The "showblocks" plugin. Enable it will make all block level
+ * elements being decorated with a border and the element name
+ * displayed on the left-right corner.
+ */
+
+(function()
+{
+ var cssTemplate = '.%2 p,'+
+ '.%2 div,'+
+ '.%2 pre,'+
+ '.%2 address,'+
+ '.%2 blockquote,'+
+ '.%2 h1,'+
+ '.%2 h2,'+
+ '.%2 h3,'+
+ '.%2 h4,'+
+ '.%2 h5,'+
+ '.%2 h6'+
+ '{'+
+ 'background-repeat: no-repeat;'+
+ 'background-position: top %3;'+
+ 'border: 1px dotted gray;'+
+ 'padding-top: 8px;'+
+ 'padding-%3: 8px;'+
+ '}'+
+
+ '.%2 p'+
+ '{'+
+ '%1p.png);'+
+ '}'+
+
+ '.%2 div'+
+ '{'+
+ '%1div.png);'+
+ '}'+
+
+ '.%2 pre'+
+ '{'+
+ '%1pre.png);'+
+ '}'+
+
+ '.%2 address'+
+ '{'+
+ '%1address.png);'+
+ '}'+
+
+ '.%2 blockquote'+
+ '{'+
+ '%1blockquote.png);'+
+ '}'+
+
+ '.%2 h1'+
+ '{'+
+ '%1h1.png);'+
+ '}'+
+
+ '.%2 h2'+
+ '{'+
+ '%1h2.png);'+
+ '}'+
+
+ '.%2 h3'+
+ '{'+
+ '%1h3.png);'+
+ '}'+
+
+ '.%2 h4'+
+ '{'+
+ '%1h4.png);'+
+ '}'+
+
+ '.%2 h5'+
+ '{'+
+ '%1h5.png);'+
+ '}'+
+
+ '.%2 h6'+
+ '{'+
+ '%1h6.png);'+
+ '}';
+
+ var cssTemplateRegex = /%1/g, cssClassRegex = /%2/g, backgroundPositionRegex = /%3/g;
+
+ var commandDefinition =
+ {
+ preserveState : true,
+ editorFocus : false,
+
+ exec : function ( editor )
+ {
+ this.toggleState();
+ this.refresh( editor );
+ },
+
+ refresh : function( editor )
+ {
+ var funcName = ( this.state == CKEDITOR.TRISTATE_ON ) ? 'addClass' : 'removeClass';
+ editor.document.getBody()[ funcName ]( 'cke_show_blocks' );
+ }
+ };
+
+ CKEDITOR.plugins.add( 'showblocks',
+ {
+ requires : [ 'wysiwygarea' ],
+
+ init : function( editor )
+ {
+ var command = editor.addCommand( 'showblocks', commandDefinition );
+ command.canUndo = false;
+
+ if ( editor.config.startupOutlineBlocks )
+ command.setState( CKEDITOR.TRISTATE_ON );
+
+ editor.addCss( cssTemplate
+ .replace( cssTemplateRegex, 'background-image: url(' + CKEDITOR.getUrl( this.path ) + 'images/block_' )
+ .replace( cssClassRegex, 'cke_show_blocks ' )
+ .replace( backgroundPositionRegex, editor.lang.dir == 'rtl' ? 'right' : 'left' ) );
+
+ editor.ui.addButton( 'ShowBlocks',
+ {
+ label : editor.lang.showBlocks,
+ command : 'showblocks'
+ });
+
+ // Refresh the command on setData.
+ editor.on( 'mode', function()
+ {
+ if ( command.state != CKEDITOR.TRISTATE_DISABLED )
+ command.refresh( editor );
+ });
+
+ // Refresh the command on setData.
+ editor.on( 'contentDom', function()
+ {
+ if ( command.state != CKEDITOR.TRISTATE_DISABLED )
+ command.refresh( editor );
+ });
+ }
+ });
+} )();
+
+/**
+ * Whether to automaticaly enable the "show block" command when the editor
+ * loads. (StartupShowBlocks in FCKeditor)
+ * @name CKEDITOR.config.startupOutlineBlocks
+ * @type Boolean
+ * @default false
+ * @example
+ * config.startupOutlineBlocks = true;
+ */
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/showborders/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/showborders/plugin.js
new file mode 100644
index 00000000..6e199e35
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/showborders/plugin.js
@@ -0,0 +1,203 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+/**
+ * @fileOverview The "show border" plugin. The command display visible outline
+ * border line around all table elements if table doesn't have a none-zero 'border' attribute specified.
+ */
+
+(function()
+{
+ var showBorderClassName = 'cke_show_border',
+ cssStyleText,
+ cssTemplate =
+ // TODO: For IE6, we don't have child selector support,
+ // where nested table cells could be incorrect.
+ ( CKEDITOR.env.ie6Compat ?
+ [
+ '.%1 table.%2,',
+ '.%1 table.%2 td, .%1 table.%2 th,',
+ '{',
+ 'border : #d3d3d3 1px dotted',
+ '}'
+ ] :
+ [
+ '.%1 table.%2,',
+ '.%1 table.%2 > tr > td, .%1 table.%2 > tr > th,',
+ '.%1 table.%2 > tbody > tr > td, .%1 table.%2 > tbody > tr > th,',
+ '.%1 table.%2 > thead > tr > td, .%1 table.%2 > thead > tr > th,',
+ '.%1 table.%2 > tfoot > tr > td, .%1 table.%2 > tfoot > tr > th',
+ '{',
+ 'border : #d3d3d3 1px dotted',
+ '}'
+ ] ).join( '' );
+
+ cssStyleText = cssTemplate.replace( /%2/g, showBorderClassName ).replace( /%1/g, 'cke_show_borders ' );
+
+ var commandDefinition =
+ {
+ preserveState : true,
+ editorFocus : false,
+
+ exec : function ( editor )
+ {
+ this.toggleState();
+ this.refresh( editor );
+ },
+
+ refresh : function( editor )
+ {
+ var funcName = ( this.state == CKEDITOR.TRISTATE_ON ) ? 'addClass' : 'removeClass';
+ editor.document.getBody()[ funcName ]( 'cke_show_borders' );
+ }
+ };
+
+ CKEDITOR.plugins.add( 'showborders',
+ {
+ requires : [ 'wysiwygarea' ],
+ modes : { 'wysiwyg' : 1 },
+
+ init : function( editor )
+ {
+
+ var command = editor.addCommand( 'showborders', commandDefinition );
+ command.canUndo = false;
+
+ if ( editor.config.startupShowBorders !== false )
+ command.setState( CKEDITOR.TRISTATE_ON );
+
+ editor.addCss( cssStyleText );
+
+ // Refresh the command on setData.
+ editor.on( 'mode', function()
+ {
+ if ( command.state != CKEDITOR.TRISTATE_DISABLED )
+ command.refresh( editor );
+ }, null, null, 100 );
+
+ // Refresh the command on wysiwyg frame reloads.
+ editor.on( 'contentDom', function()
+ {
+ if ( command.state != CKEDITOR.TRISTATE_DISABLED )
+ command.refresh( editor );
+ });
+
+ editor.on( 'removeFormatCleanup', function( evt )
+ {
+ var element = evt.data;
+ if ( editor.getCommand( 'showborders' ).state == CKEDITOR.TRISTATE_ON &&
+ element.is( 'table' ) && ( !element.hasAttribute( 'border' ) || parseInt( element.getAttribute( 'border' ), 10 ) <= 0 ) )
+ element.addClass( showBorderClassName );
+ });
+ },
+
+ afterInit : function( editor )
+ {
+ var dataProcessor = editor.dataProcessor,
+ dataFilter = dataProcessor && dataProcessor.dataFilter,
+ htmlFilter = dataProcessor && dataProcessor.htmlFilter;
+
+ if ( dataFilter )
+ {
+ dataFilter.addRules(
+ {
+ elements :
+ {
+ 'table' : function( element )
+ {
+ var attributes = element.attributes,
+ cssClass = attributes[ 'class' ],
+ border = parseInt( attributes.border, 10 );
+
+ if ( !border || border <= 0 )
+ attributes[ 'class' ] = ( cssClass || '' ) + ' ' + showBorderClassName;
+ }
+ }
+ } );
+ }
+
+ if ( htmlFilter )
+ {
+ htmlFilter.addRules(
+ {
+ elements :
+ {
+ 'table' : function( table )
+ {
+ var attributes = table.attributes,
+ cssClass = attributes[ 'class' ];
+
+ cssClass && ( attributes[ 'class' ] =
+ cssClass.replace( showBorderClassName, '' )
+ .replace( /\s{2}/, ' ' )
+ .replace( /^\s+|\s+$/, '' ) );
+ }
+ }
+ } );
+ }
+ }
+ });
+
+ // Table dialog must be aware of it.
+ CKEDITOR.on( 'dialogDefinition', function( ev )
+ {
+ var dialogName = ev.data.name;
+
+ if ( dialogName == 'table' || dialogName == 'tableProperties' )
+ {
+ var dialogDefinition = ev.data.definition,
+ infoTab = dialogDefinition.getContents( 'info' ),
+ borderField = infoTab.get( 'txtBorder' ),
+ originalCommit = borderField.commit;
+
+ borderField.commit = CKEDITOR.tools.override( originalCommit, function( org )
+ {
+ return function( data, selectedTable )
+ {
+ org.apply( this, arguments );
+ var value = parseInt( this.getValue(), 10 );
+ selectedTable[ ( !value || value <= 0 ) ? 'addClass' : 'removeClass' ]( showBorderClassName );
+ };
+ } );
+
+ var advTab = dialogDefinition.getContents( 'advanced' ),
+ classField = advTab && advTab.get( 'advCSSClasses' );
+
+ if ( classField )
+ {
+ classField.setup = CKEDITOR.tools.override( classField.setup, function( originalSetup )
+ {
+ return function()
+ {
+ originalSetup.apply( this, arguments );
+ this.setValue( this.getValue().replace( /cke_show_border/, '' ) );
+ };
+ });
+
+ classField.commit = CKEDITOR.tools.override( classField.commit, function( originalCommit )
+ {
+ return function( data, element )
+ {
+ originalCommit.apply( this, arguments );
+
+ if ( !parseInt( element.getAttribute( 'border' ), 10 ) )
+ element.addClass( 'cke_show_border' );
+ };
+ });
+ }
+ }
+ });
+
+} )();
+
+/**
+ * Whether to automatically enable the "show borders" command when the editor loads.
+ * (ShowBorders in FCKeditor)
+ * @name CKEDITOR.config.startupShowBorders
+ * @type Boolean
+ * @default true
+ * @example
+ * config.startupShowBorders = false;
+ */
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/dialogs/smiley.js b/app/assets/javascripts/ckeditor/_source/plugins/smiley/dialogs/smiley.js
new file mode 100644
index 00000000..d6157dee
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/smiley/dialogs/smiley.js
@@ -0,0 +1,223 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.dialog.add( 'smiley', function( editor )
+{
+ var config = editor.config,
+ lang = editor.lang.smiley,
+ images = config.smiley_images,
+ columns = config.smiley_columns || 8,
+ i;
+
+ /**
+ * Simulate "this" of a dialog for non-dialog events.
+ * @type {CKEDITOR.dialog}
+ */
+ var dialog;
+ var onClick = function( evt )
+ {
+ var target = evt.data.getTarget(),
+ targetName = target.getName();
+
+ if ( targetName == 'a' )
+ target = target.getChild( 0 );
+ else if ( targetName != 'img' )
+ return;
+
+ var src = target.getAttribute( 'cke_src' ),
+ title = target.getAttribute( 'title' );
+
+ var img = editor.document.createElement( 'img',
+ {
+ attributes :
+ {
+ src : src,
+ 'data-cke-saved-src' : src,
+ title : title,
+ alt : title,
+ width : target.$.width,
+ height : target.$.height
+ }
+ });
+
+ editor.insertElement( img );
+
+ dialog.hide();
+ evt.data.preventDefault();
+ };
+
+ var onKeydown = CKEDITOR.tools.addFunction( function( ev, element )
+ {
+ ev = new CKEDITOR.dom.event( ev );
+ element = new CKEDITOR.dom.element( element );
+ var relative, nodeToMove;
+
+ var keystroke = ev.getKeystroke(),
+ rtl = editor.lang.dir == 'rtl';
+ switch ( keystroke )
+ {
+ // UP-ARROW
+ case 38 :
+ // relative is TR
+ if ( ( relative = element.getParent().getParent().getPrevious() ) )
+ {
+ nodeToMove = relative.getChild( [element.getParent().getIndex(), 0] );
+ nodeToMove.focus();
+ }
+ ev.preventDefault();
+ break;
+ // DOWN-ARROW
+ case 40 :
+ // relative is TR
+ if ( ( relative = element.getParent().getParent().getNext() ) )
+ {
+ nodeToMove = relative.getChild( [element.getParent().getIndex(), 0] );
+ if ( nodeToMove )
+ nodeToMove.focus();
+ }
+ ev.preventDefault();
+ break;
+ // ENTER
+ // SPACE
+ case 32 :
+ onClick( { data: ev } );
+ ev.preventDefault();
+ break;
+
+ // RIGHT-ARROW
+ case rtl ? 37 : 39 :
+ // TAB
+ case 9 :
+ // relative is TD
+ if ( ( relative = element.getParent().getNext() ) )
+ {
+ nodeToMove = relative.getChild( 0 );
+ nodeToMove.focus();
+ ev.preventDefault(true);
+ }
+ // relative is TR
+ else if ( ( relative = element.getParent().getParent().getNext() ) )
+ {
+ nodeToMove = relative.getChild( [0, 0] );
+ if ( nodeToMove )
+ nodeToMove.focus();
+ ev.preventDefault(true);
+ }
+ break;
+
+ // LEFT-ARROW
+ case rtl ? 39 : 37 :
+ // SHIFT + TAB
+ case CKEDITOR.SHIFT + 9 :
+ // relative is TD
+ if ( ( relative = element.getParent().getPrevious() ) )
+ {
+ nodeToMove = relative.getChild( 0 );
+ nodeToMove.focus();
+ ev.preventDefault(true);
+ }
+ // relative is TR
+ else if ( ( relative = element.getParent().getParent().getPrevious() ) )
+ {
+ nodeToMove = relative.getLast().getChild( 0 );
+ nodeToMove.focus();
+ ev.preventDefault(true);
+ }
+ break;
+ default :
+ // Do not stop not handled events.
+ return;
+ }
+ });
+
+ // Build the HTML for the smiley images table.
+ var labelId = CKEDITOR.tools.getNextId() + '_smiley_emtions_label';
+ var html =
+ [
+ '' +
+ '
' + lang.options +' ',
+ '
'
+ ];
+
+ var size = images.length;
+ for ( i = 0 ; i < size ; i++ )
+ {
+ if ( i % columns === 0 )
+ html.push( '' );
+
+ var smileyLabelId = 'cke_smile_label_' + i + '_' + CKEDITOR.tools.getNextNumber();
+ html.push(
+ '' +
+ '',
+ ' ' +
+ '' +config.smiley_descriptions[ i ] + ' ' +
+ ' ',
+ ' ' );
+
+ if ( i % columns == columns - 1 )
+ html.push( ' ' );
+ }
+
+ if ( i < columns - 1 )
+ {
+ for ( ; i < columns - 1 ; i++ )
+ html.push( ' ' );
+ html.push( '' );
+ }
+
+ html.push( '
' );
+
+ var smileySelector =
+ {
+ type : 'html',
+ html : html.join( '' ),
+ onLoad : function( event )
+ {
+ dialog = event.sender;
+ },
+ focus : function()
+ {
+ var self = this;
+ // IE need a while to move the focus (#6539).
+ setTimeout( function ()
+ {
+ var firstSmile = self.getElement().getElementsByTag( 'a' ).getItem( 0 );
+ firstSmile.focus();
+ }, 0 );
+ },
+ onClick : onClick,
+ style : 'width: 100%; border-collapse: separate;'
+ };
+
+ return {
+ title : editor.lang.smiley.title,
+ minWidth : 270,
+ minHeight : 120,
+ contents : [
+ {
+ id : 'tab1',
+ label : '',
+ title : '',
+ expand : true,
+ padding : 0,
+ elements : [
+ smileySelector
+ ]
+ }
+ ],
+ buttons : [ CKEDITOR.dialog.cancelButton ]
+ };
+} );
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/angel_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/angel_smile.gif
new file mode 100644
index 00000000..2cf48947
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/angel_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/angry_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/angry_smile.gif
new file mode 100644
index 00000000..40b52d3a
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/angry_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/broken_heart.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/broken_heart.gif
new file mode 100644
index 00000000..b6d0166a
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/broken_heart.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/confused_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/confused_smile.gif
new file mode 100644
index 00000000..a52db7a4
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/confused_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/cry_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/cry_smile.gif
new file mode 100644
index 00000000..1ef6ba4f
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/cry_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/devil_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/devil_smile.gif
new file mode 100644
index 00000000..bfb25077
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/devil_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/embaressed_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/embaressed_smile.gif
new file mode 100644
index 00000000..d9cedc56
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/embaressed_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/envelope.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/envelope.gif
new file mode 100644
index 00000000..94e0b1fa
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/envelope.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/heart.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/heart.gif
new file mode 100644
index 00000000..680ae2fc
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/heart.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/kiss.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/kiss.gif
new file mode 100644
index 00000000..70e52551
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/kiss.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/lightbulb.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/lightbulb.gif
new file mode 100644
index 00000000..d44c2ffe
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/lightbulb.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/omg_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/omg_smile.gif
new file mode 100644
index 00000000..ab7ea699
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/omg_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/regular_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/regular_smile.gif
new file mode 100644
index 00000000..e5bc34be
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/regular_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/sad_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/sad_smile.gif
new file mode 100644
index 00000000..ae4cf1e3
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/sad_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/shades_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/shades_smile.gif
new file mode 100644
index 00000000..b4540175
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/shades_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/teeth_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/teeth_smile.gif
new file mode 100644
index 00000000..7d734d15
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/teeth_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/thumbs_down.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/thumbs_down.gif
new file mode 100644
index 00000000..90b9978c
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/thumbs_down.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/thumbs_up.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/thumbs_up.gif
new file mode 100644
index 00000000..b6332848
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/thumbs_up.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/tounge_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/tounge_smile.gif
new file mode 100644
index 00000000..b2e657fa
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/tounge_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/whatchutalkingabout_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/whatchutalkingabout_smile.gif
new file mode 100644
index 00000000..77098821
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/whatchutalkingabout_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/wink_smile.gif b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/wink_smile.gif
new file mode 100644
index 00000000..b2102954
Binary files /dev/null and b/app/assets/javascripts/ckeditor/_source/plugins/smiley/images/wink_smile.gif differ
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/smiley/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/smiley/plugin.js
new file mode 100644
index 00000000..bde383b5
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/smiley/plugin.js
@@ -0,0 +1,94 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.plugins.add( 'smiley',
+{
+ requires : [ 'dialog' ],
+
+ init : function( editor )
+ {
+ editor.config.smiley_path = editor.config.smiley_path || ( this.path + 'images/' );
+ editor.addCommand( 'smiley', new CKEDITOR.dialogCommand( 'smiley' ) );
+ editor.ui.addButton( 'Smiley',
+ {
+ label : editor.lang.smiley.toolbar,
+ command : 'smiley'
+ });
+ CKEDITOR.dialog.add( 'smiley', this.path + 'dialogs/smiley.js' );
+ }
+} );
+
+/**
+ * The base path used to build the URL for the smiley images. It must end with
+ * a slash.
+ * @name CKEDITOR.config.smiley_path
+ * @type String
+ * @default {@link CKEDITOR.basePath} + 'plugins/smiley/images/'
+ * @example
+ * config.smiley_path = 'http://www.example.com/images/smileys/';
+ * @example
+ * config.smiley_path = '/images/smileys/';
+ */
+
+/**
+ * The file names for the smileys to be displayed. These files must be
+ * contained inside the URL path defined with the
+ * {@link CKEDITOR.config.smiley_path} setting.
+ * @type Array
+ * @default (see example)
+ * @example
+ * // This is actually the default value.
+ * config.smiley_images = [
+ * 'regular_smile.gif','sad_smile.gif','wink_smile.gif','teeth_smile.gif','confused_smile.gif','tounge_smile.gif',
+ * 'embaressed_smile.gif','omg_smile.gif','whatchutalkingabout_smile.gif','angry_smile.gif','angel_smile.gif','shades_smile.gif',
+ * 'devil_smile.gif','cry_smile.gif','lightbulb.gif','thumbs_down.gif','thumbs_up.gif','heart.gif',
+ * 'broken_heart.gif','kiss.gif','envelope.gif'];
+ */
+CKEDITOR.config.smiley_images = [
+ 'regular_smile.gif','sad_smile.gif','wink_smile.gif','teeth_smile.gif','confused_smile.gif','tounge_smile.gif',
+ 'embaressed_smile.gif','omg_smile.gif','whatchutalkingabout_smile.gif','angry_smile.gif','angel_smile.gif','shades_smile.gif',
+ 'devil_smile.gif','cry_smile.gif','lightbulb.gif','thumbs_down.gif','thumbs_up.gif','heart.gif',
+ 'broken_heart.gif','kiss.gif','envelope.gif'];
+
+/**
+ * The description to be used for each of the smileys defined in the
+ * {@link CKEDITOR.config.smiley_images} setting. Each entry in this array list
+ * must match its relative pair in the {@link CKEDITOR.config.smiley_images}
+ * setting.
+ * @type Array
+ * @default The textual descriptions of smiley.
+ * @example
+ * // Default settings.
+ * config.smiley_descriptions =
+ * [
+ * 'smiley', 'sad', 'wink', 'laugh', 'frown', 'cheeky', 'blush', 'surprise',
+ * 'indecision', 'angry', 'angel', 'cool', 'devil', 'crying', 'enlightened', 'no',
+ * 'yes', 'heart', 'broken heart', 'kiss', 'mail'
+ * ];
+ * @example
+ * // Use textual emoticons as description.
+ * config.smiley_descriptions =
+ * [
+ * ':)', ':(', ';)', ':D', ':/', ':P', ':*)', ':-o',
+ * ':|', '>:(', 'o:)', '8-)', '>:-)', ';(', '', '', '',
+ * '', '', ':-*', ''
+ * ];
+ */
+CKEDITOR.config.smiley_descriptions =
+ [
+ 'smiley', 'sad', 'wink', 'laugh', 'frown', 'cheeky', 'blush', 'surprise',
+ 'indecision', 'angry', 'angel', 'cool', 'devil', 'crying', 'enlightened', 'no',
+ 'yes', 'heart', 'broken heart', 'kiss', 'mail'
+ ];
+
+/**
+ * The number of columns to be generated by the smilies matrix.
+ * @name CKEDITOR.config.smiley_columns
+ * @type Number
+ * @default 8
+ * @since 3.3.2
+ * @example
+ * config.smiley_columns = 6;
+ */
diff --git a/app/assets/javascripts/ckeditor/_source/plugins/sourcearea/plugin.js b/app/assets/javascripts/ckeditor/_source/plugins/sourcearea/plugin.js
new file mode 100644
index 00000000..ad3b4eac
--- /dev/null
+++ b/app/assets/javascripts/ckeditor/_source/plugins/sourcearea/plugin.js
@@ -0,0 +1,196 @@
+/*
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+/**
+ * @fileOverview The "sourcearea" plugin. It registers the "source" editing
+ * mode, which displays the raw data being edited in the editor.
+ */
+
+CKEDITOR.plugins.add( 'sourcearea',
+{
+ requires : [ 'editingblock' ],
+
+ init : function( editor )
+ {
+ var sourcearea = CKEDITOR.plugins.sourcearea,
+ win = CKEDITOR.document.getWindow();
+
+ editor.on( 'editingBlockReady', function()
+ {
+ var textarea,
+ onResize;
+
+ editor.addMode( 'source',
+ {
+ load : function( holderElement, data )
+ {
+ if ( CKEDITOR.env.ie && CKEDITOR.env.version < 8 )
+ holderElement.setStyle( 'position', 'relative' );
+
+ // Create the source area