Index: _source/plugins/htmldataprocessor/plugin.js
===================================================================
--- _source/plugins/htmldataprocessor/plugin.js (revision 5878)
+++ _source/plugins/htmldataprocessor/plugin.js (revision )
@@ -263,6 +263,14 @@
defaultHtmlFilterRules.elements[ i ] = unprotectReadyOnly;
}
+ // Disallow structural blocks without sub nodes.
+ var removeIfEmptyBlocks = { table:1, thead:1,tfoot:1,tbody:1, tr:1 },
+ removeIfEmptyFilterRules = { elements : {} },
+ removeIfEmtpyFilter = function( element ){ if ( !element.children.length ) return false; };
+
+ for ( i in removeIfEmptyBlocks )
+ removeIfEmptyFilterRules.elements[ i ] = removeIfEmtpyFilter;
+
var protectAttributeRegex = /<((?:a|area|img|input)[\s\S]*?\s)((href|src|name)\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|(?:[^ "'>]+)))([^>]*)>/gi,
findSavedSrcRegex = /\s_cke_saved_src\s*=/;
@@ -397,8 +405,10 @@
dataProcessor.dataFilter.addRules( defaultDataFilterRules );
dataProcessor.dataFilter.addRules( defaultDataBlockFilterRules );
+ dataProcessor.dataFilter.addRules( removeIfEmptyFilterRules );
dataProcessor.htmlFilter.addRules( defaultHtmlFilterRules );
dataProcessor.htmlFilter.addRules( defaultHtmlBlockFilterRules );
+ dataProcessor.htmlFilter.addRules( removeIfEmptyFilterRules );
}
});
Index: _source/core/htmlparser/cdata.js
===================================================================
--- _source/core/htmlparser/cdata.js (revision 5206)
+++ _source/core/htmlparser/cdata.js (revision )
@@ -38,6 +38,11 @@
writeHtml : function( writer )
{
writer.write( this.value );
+ },
+
+ toDom : function( doc )
+ {
+ return new CKEDITOR.dom.text( this.value, doc );
}
};
})();
Index: _source/core/dom/element.js
===================================================================
--- _source/core/dom/element.js (revision 5913)
+++ _source/core/dom/element.js (revision )
@@ -254,6 +254,28 @@
},
/**
+ * Retrieve block element's filler node if existed.
+ */
+ getBogus : function()
+ {
+ if ( !this.isBlockBoundary() )
+ return;
+
+ var lastChild = this.getLast() ;
+
+ // Ignore empty/spaces text.
+ while ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.rtrim( lastChild.getText() ) )
+ lastChild = lastChild.getPrevious();
+
+ if ( lastChild &&
+ ( CKEDITOR.env.ie && lastChild.type == CKEDITOR.NODE_TEXT && CKEDITOR.tools.trim( lastChild.getText() ).match( /^(?: |\xa0)$/ )
+ || CKEDITOR.env.gecko && CKEDITOR.env.webkit && lastChild.is( 'br' ) ) )
+ {
+ return lastChild;
+ }
+ },
+
+ /**
* Breaks one of the ancestor element in the element position, moving
* this element between the broken parts.
* @param {CKEDITOR.dom.element} parent The anscestor element to get broken.
Index: _source/core/htmlparser/fragment.js
===================================================================
--- _source/core/htmlparser/fragment.js (revision 5750)
+++ _source/core/htmlparser/fragment.js (revision )
@@ -492,6 +492,15 @@
{
for ( var i = 0 ; i < this.children.length ; i++ )
this.children[i].writeHtml( writer, filter );
+ },
+
+ toDom : function( doc )
+ {
+ var fragment = new CKEDITOR.dom.documentFragment( doc );
+ for ( var i = 0, count = this.children.length; i < count; i++ )
+ fragment.append( this.children[ i ].toDom( doc ) );
+
+ return fragment;
}
};
})();
Index: _source/core/htmlparser/element.js
===================================================================
--- _source/core/htmlparser/element.js (revision 5206)
+++ _source/core/htmlparser/element.js (revision )
@@ -235,6 +235,17 @@
// Send children.
CKEDITOR.htmlParser.fragment.prototype.writeChildrenHtml.apply( this, arguments );
+ },
+
+ toDom : function( doc )
+ {
+ var element = new CKEDITOR.dom.element( this.name, doc );
+ element.setAttributes( this.attributes );
+
+ for ( var i = 0, count = this.children.length; i < count; i++ )
+ element.append( this.children[ i ].toDom() );
+
+ return element;
}
};
})();
Index: _source/core/htmlparser/comment.js
===================================================================
--- _source/core/htmlparser/comment.js (revision 5206)
+++ _source/core/htmlparser/comment.js (revision )
@@ -56,5 +56,11 @@
}
writer.comment( comment );
+ },
+
+ toDom : function( doc )
+ {
+ return new CKEDITOR.dom.comment( this.value, doc );
}
+
};
Index: _source/core/htmlparser/text.js
===================================================================
--- _source/core/htmlparser/text.js (revision 4858)
+++ _source/core/htmlparser/text.js (revision )
@@ -50,6 +50,12 @@
return;
writer.text( text );
+ },
+
+ toDom : function( doc )
+ {
+ // DON'T use CKEDITOR.dom.text, it's not handling entities..
+ return CKEDITOR.dom.element.createFromHtml( this.value );
}
};
})();
Index: _source/plugins/wysiwygarea/plugin.js
===================================================================
--- _source/plugins/wysiwygarea/plugin.js (revision 5929)
+++ _source/plugins/wysiwygarea/plugin.js (revision )
@@ -36,63 +36,47 @@
if ( checkReadOnly( selection ) )
return;
- var data = evt.data;
+ var ranges = selection.getRanges( true ),
+ data = evt.data;
this.fire( 'saveSnapshot' );
if ( this.dataProcessor )
data = this.dataProcessor.toHtml( data );
- if ( CKEDITOR.env.ie )
+ // Process the ranges counter-clockwise to avoid any impacts among them.
+ for ( var i = ranges.length - 1 ; i >= 0 ; i-- )
{
- var selIsLocked = selection.isLocked;
+ var range = ranges[ i ],
+ previousRange = ranges[ i - 1 ];
- if ( selIsLocked )
- selection.unlock();
-
- var $sel = selection.getNative();
-
- // Delete control selections to avoid IE bugs on pasteHTML.
- if ( $sel.type == 'Control' )
- $sel.clear();
- else if ( selection.getType() == CKEDITOR.SELECTION_TEXT )
+ // Merge next sibling range when possible, it's not a common case,
+ // but necessary for situation like multiple table cell selection in Firefox.
+ if ( previousRange )
{
- // Due to IE bugs on handling contenteditable=false blocks
- // (#6005), we need to make some checks and eventually
- // delete the selection first.
-
- var range = selection.getRanges()[0],
- endContainer = range && range.endContainer;
-
- if ( endContainer &&
- endContainer.type == CKEDITOR.NODE_ELEMENT &&
- endContainer.getAttribute( 'contenteditable' ) == 'false' &&
- range.checkBoundaryOfElement( endContainer, CKEDITOR.END ) )
+ if ( previousRange.endContainer.equals( range.startContainer )
+ && previousRange.endOffset == range.startOffset )
{
- range.setEndAfter( range.endContainer );
- range.deleteContents();
+ previousRange.endContainer = range.endContainer;
+ previousRange.endOffset = range.endOffset;
+ ranges.splice( i, 1 );
+ continue;
}
}
- try
+ // Pasting should only go into the first range.
+ if ( i == 0 )
+ range.pasteHtml( data );
+ else
{
- $sel.createRange().pasteHTML( data );
+ range.enlarge( CKEDITOR.NODE_ELEMENT );
+ // Drop the range after selected content is deleted.
+ range.extractContents();
+ ranges.length--;
}
- catch (e) {}
-
- if ( selIsLocked )
- this.getSelection().lock();
}
- else
- this.document.$.execCommand( 'inserthtml', false, data );
- // Webkit does not scroll to the cursor position after pasting (#5558)
- if ( CKEDITOR.env.webkit )
- {
- this.document.$.execCommand( 'inserthtml', false, '' );
- var marker = this.document.getById( 'cke_paste_marker' );
- marker.scrollIntoView();
- marker.remove();
- }
+ selection.selectRanges( ranges );
+ !CKEDITOR.env.ie && selection.scrollIntoView();
CKEDITOR.tools.setTimeout( function()
{
Index: _source/core/dom/range.js
===================================================================
--- _source/core/dom/range.js (revision 5832)
+++ _source/core/dom/range.js (revision )
@@ -317,6 +317,27 @@
return !whitespaceEval( node ) && !bookmarkEval( node );
}
+
+ function collectValidChildren( element, parentName )
+ {
+ var next = element, isValid,
+ candidates = [];
+
+ while ( ( next = next.getNextSourceNode( isValid, CKEDITOR.NODE_ELEMENT ) ) )
+ {
+ if ( isValid = next.getName() in CKEDITOR.dtd[ parentName ] )
+ candidates.push( next );
+ }
+
+ var fragment = new CKEDITOR.dom.documentFragment();
+ for ( var i = 0, count = candidates.length; i < count; i++ )
+ fragment.append( candidates[ i ] );
+ return fragment;
+ }
+
+ var emptyspaces = CKEDITOR.dom.walker.whitespaces(),
+ bookmarks = CKEDITOR.dom.walker.bookmark();
+
CKEDITOR.dom.range.prototype =
{
clone : function()
@@ -671,6 +692,109 @@
},
/**
+ * Paste the give HTML at the start of the range, and replacing any previously selected nodes within the range.
+ * Note: This method might alter the specified HTML to make it fit the given range context. For example, when pasting a table
+ * when actually a table row is selected (a DTD violation), instead of inserting the table, it results in only the cells of the table
+ * getting pasted into the row.
+ * For predictable results, paste only well-formed HTML that fits at the given range context.
+ * @param {String} html Valid HTML string to paste into the range.
+ */
+ pasteHtml : function ( html )
+ {
+ // Create a fragment of nodes without context that represents the structure to paste. (W3C Spec)
+ var fragment = CKEDITOR.htmlParser.fragment.fromHtml( html, false ),
+ nodeList = fragment.children;
+
+ if ( !this.collapsed )
+ {
+ // Avoid resulting in dummy element after the deletion.
+ this.enlarge( CKEDITOR.NODE_ELEMENT );
+ this.extractContents();
+ }
+
+ // Split up text node at position.
+ if ( this.startContainer.type == CKEDITOR.NODE_TEXT )
+ this.trim();
+
+ // If we're at end of block, remove any bogus node, because:
+ // 1. It's not needed any more due to the incomming content.
+ // 2. It will get doubled (visible) if the to be inserted node is a bogus too.
+ var block;
+ if ( block = this.checkEndOfBlock() )
+ {
+ var bogus = block.getBogus();
+ bogus && bogus.remove();
+ }
+
+ var targetNode, targetNodeOffset, targetName, path,
+ updateTarget = CKEDITOR.tools.bind( function ()
+ {
+ targetNode = this.startContainer,
+ targetNodeOffset = this.startOffset,
+ targetName = targetNode.getName();
+ path = new CKEDITOR.dom.elementPath( targetNode );
+ }, this );
+
+ updateTarget();
+ for ( var i = 0, count = nodeList.length; i < count; i++ )
+ {
+ var node = nodeList[ i ],
+ nodeName = node.type == CKEDITOR.NODE_ELEMENT ? node.name : '#';
+
+ // If we have a list to insert, and we're actually standing inside a list item,
+ // insert the appropriate children instead, in short, merge the lists
+ // instead of pasting in a sublist.
+ if ( nodeName in CKEDITOR.dtd.$list
+ && path.block && path.block.getName() in CKEDITOR.dtd.$listItem )
+ {
+ this.splitElement( path.block );
+ updateTarget();
+ }
+
+ // The to be inserted node fails DTD examination.
+ while ( !CKEDITOR.dtd[ targetName ][ nodeName ] )
+ {
+ // If the failure is caused by pasting table/list inside a table/list
+ // structure, instead of splitting up the element, just insert the
+ // appropriate children.
+ if ( nodeName == 'table' && targetName in { table:1, tfoot:1,thead:1,tbody:1,tr:1 }
+ || nodeName in { ul:1, ol:1, dl:1 } && targetName in { ul:1, ol:1, dl:1 } )
+ {
+ node = collectValidChildren( node.toDom(), targetName );
+ break;
+ }
+
+ // Users are more likely want to paste content intead of the element, e.g. pre, headings.
+ if ( ( targetName == nodeName )
+ && ( nodeName == 'pre' || nodeName.match( /^h[1-6]$/ ) ) )
+ {
+ Array.prototype.splice.apply( nodeList, [ i, 1 ].concat( node.children ) );
+ node = nodeList[ i ];
+ break;
+ }
+
+ // Ok, it's our last option, split up or move out of current element.
+ this.splitElement( targetNode );
+ updateTarget();
+ }
+
+ var domNode = node.$ ? node : node.toDom();
+ // Where to put the cursor?
+ var anchorNode = domNode.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ?
+ domNode.getLast() : domNode;
+
+ this.insertNode( domNode );
+
+ // Try to place cursor at the end of the last pasted node,
+ // with the only exception that we should discontinue any pasted link,
+ // If that fails, simply collapse by the end of last node.
+ if ( !( i == count - 1 && anchorNode.type == CKEDITOR.NODE_ELEMENT
+ && !anchorNode.is( 'a' ) && this.moveToElementEditEnd( anchorNode ) ) )
+ this.collapse();
+ }
+ },
+
+ /**
* Transforms the startContainer and endContainer properties from text
* nodes to element nodes, whenever possible. This is actually possible
* if either of the boundary containers point to a text node, and its
@@ -871,7 +995,7 @@
{
// If we reached the common ancestor, mark the flag
// for it.
- if ( !commonReached && enlargeable.equals( commonAncestor ) )
+ if ( !commonReached && ( enlargeable.contains( commonAncestor ) || enlargeable.equals( commonAncestor ) ) )
commonReached = true;
if ( !body.contains( enlargeable ) )
@@ -1041,7 +1165,7 @@
{
if ( enlargeable && !sibling )
{
- if ( !commonReached && enlargeable.equals( commonAncestor ) )
+ if ( !commonReached && ( enlargeable.contains( commonAncestor ) || enlargeable.equals( commonAncestor ) ) )
commonReached = true;
if ( !body.contains( enlargeable ) )
@@ -1354,6 +1478,12 @@
var startContainer = this.startContainer;
var startOffset = this.startOffset;
+ var count = node.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ?
+ node.getChildren().count() : 1;
+
+ var anchorNode = node.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ?
+ node.getChildren().getItem( 0 ) : node;
+
var nextNode = startContainer.getChild( startOffset );
if ( nextNode )
@@ -1362,11 +1492,11 @@
startContainer.append( node );
// Check if we need to update the end boundary.
- if ( node.getParent().equals( this.endContainer ) )
- this.endOffset++;
+ if ( startContainer.equals( this.endContainer ) )
+ this.endOffset += count;
// Expand the range to embrace the new node.
- this.setStartBefore( node );
+ this.setStartBefore( anchorNode );
},
moveToPosition : function( node, position )
@@ -1621,6 +1751,33 @@
if ( !this.collapsed )
return null;
+ var testRange = this.clone(),
+ walker = new CKEDITOR.dom.walker( testRange );
+
+ walker.evaluator = function( node )
+ {
+ return !!( emptyspaces( node ) || bookmarks( node ) );
+ };
+
+ testRange.setEndAfter( toSplit );
+ if ( walker.checkForward() )
+ {
+ this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
+ return null;
+ }
+ else
+ {
+ testRange = this.clone();
+ testRange.setStartBefore( toSplit );
+ walker.reset();
+ walker.range = testRange;
+ if ( walker.checkBackward() )
+ {
+ this.moveToPosition( toSplit, CKEDITOR.POSITION_BEFORE_START );
+ return null;
+ }
+ }
+
// Extract the contents of the block from the selection point to the end
// of its contents.
this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
@@ -1729,12 +1886,13 @@
// Creates a range starting at the block start until the range start.
var walkerRange = this.clone();
walkerRange.collapse( false );
- walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
+ var pathBlock = path.block || path.blockLimit;
+ walkerRange.setEndAt( pathBlock, CKEDITOR.POSITION_BEFORE_END );
var walker = new CKEDITOR.dom.walker( walkerRange );
walker.evaluator = getCheckStartEndBlockEvalFunction( false );
- return walker.checkForward();
+ return walker.checkForward() && pathBlock;
},
/**