0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2025-01-03 05:00:13 -05:00

Better algorithm for remove all formatting action.

1. Keeps all leaf nodes not just text nodes, so images etc. are not removed.
2. If the selection is not within a single block, it is expanded to the edges
   of the blocks rather than splitting the blocks; this is unlikely to have
   been what the user wanted.
3. More efficient tree traversal and manipulation; no duplication of nodes.
4. Records undo state before performing the action.
This commit is contained in:
Neil Jenkins 2015-06-17 15:38:12 +07:00
parent d9872fb4b2
commit 345159b4c4
4 changed files with 137 additions and 126 deletions

View file

@ -851,8 +851,9 @@ var insertTreeFragmentIntoRange = function ( range, frag ) {
nodeBeforeSplit = next.previousSibling; nodeBeforeSplit = next.previousSibling;
} }
if ( !startContainer.parentNode ) { if ( !startContainer.parentNode ) {
startContainer = nodeBeforeSplit; startContainer = nodeBeforeSplit || next.parentNode;
startOffset = nodeBeforeSplit.childNodes.length; startOffset = nodeBeforeSplit ?
nodeBeforeSplit.childNodes.length : 0;
} }
// Merge inserted containers with edges of split // Merge inserted containers with edges of split
if ( isContainer( next ) ) { if ( isContainer( next ) ) {
@ -878,7 +879,7 @@ var insertTreeFragmentIntoRange = function ( range, frag ) {
endOffset = prev.childNodes.length; endOffset = prev.childNodes.length;
} }
// Merge inserted containers with edges of split // Merge inserted containers with edges of split
if ( isContainer( nodeAfterSplit ) ) { if ( nodeAfterSplit && isContainer( nodeAfterSplit ) ) {
mergeContainers( nodeAfterSplit ); mergeContainers( nodeAfterSplit );
} }
@ -1983,7 +1984,7 @@ proto._saveRangeToBookmark = function ( range ) {
range.setEndBefore( endNode ); range.setEndBefore( endNode );
}; };
proto._getRangeAndRemoveBookmark = function ( range, persistSplits ) { proto._getRangeAndRemoveBookmark = function ( range ) {
var doc = this._doc, var doc = this._doc,
start = doc.getElementById( startSelectionId ), start = doc.getElementById( startSelectionId ),
end = doc.getElementById( endSelectionId ); end = doc.getElementById( endSelectionId );
@ -2007,12 +2008,10 @@ proto._getRangeAndRemoveBookmark = function ( range, persistSplits ) {
detach( start ); detach( start );
detach( end ); detach( end );
if ( !persistSplits ) { // Merge any text nodes we split
// Merge any text nodes we split mergeInlines( startContainer, _range );
mergeInlines( startContainer, _range ); if ( startContainer !== endContainer ) {
if ( startContainer !== endContainer ) { mergeInlines( endContainer, _range );
mergeInlines( endContainer, _range );
}
} }
if ( !range ) { if ( !range ) {
@ -3620,84 +3619,91 @@ proto.setTextDirection = function ( direction ) {
return this.focus(); return this.focus();
}; };
function removeFormatting ( self, root, clean ) {
function forEachChildInRange ( rootNode, range, iterator ) { var node, next;
var childNodes = rootNode.childNodes, for ( node = root.firstChild; node; node = next ) {
node = rootNode.firstChild; next = node.nextSibling;
while ( node ) { if ( isInline( node ) ) {
if ( isNodeContainedInRange( range, node, false ) ) { if ( node.nodeType === TEXT_NODE || isLeaf( node ) ) {
iterator( node ); clean.appendChild( node );
continue;
}
} else if ( isBlock( node ) ) {
clean.appendChild( self.createDefaultBlock([
removeFormatting(
self, node, self._doc.createDocumentFragment() )
]));
continue;
} }
node = node.nextSibling; removeFormatting( self, node, clean );
} }
return clean;
} }
function mapEachChildInRange ( rootNode, range, iterator ) {
var output = [];
forEachChildInRange( rootNode, range, function ( node ) {
output.push( iterator( node ) );
});
return output;
}
var stylingNodeNames = /(^|>)(?:B|I|S|SUB|SUP|U|BLOCKQUOTE|OL|UL|LI|T(?:ABLE|BODY|HEAD|FOOT|R|D|H))(>|$)/;
proto.removeAllFormatting = function ( range ) { proto.removeAllFormatting = function ( range ) {
if ( !range && !( range = this.getSelection() ) || range.collapsed ) { if ( !range && !( range = this.getSelection() ) || range.collapsed ) {
return false; return this;
} }
var stopNode = range.commonAncestorContainer; var stopNode = range.commonAncestorContainer;
while ( stylingNodeNames.test( getPath( stopNode ) ) ) { while ( stopNode && !isBlock( stopNode ) ) {
stopNode = stopNode.parentNode; stopNode = stopNode.parentNode;
} }
if (stopNode.nodeType === TEXT_NODE) { if ( !stopNode ) {
return false; expandRangeToBlockBoundaries( range );
stopNode = this._body;
}
if ( stopNode.nodeType === TEXT_NODE ) {
return this;
} }
// Record undo point
this._recordUndoState( range );
this._getRangeAndRemoveBookmark( range );
// Avoid splitting where we're already at edges.
moveRangeBoundariesUpTree( range, stopNode ); moveRangeBoundariesUpTree( range, stopNode );
this._saveRangeToBookmark( range );
// Split the selection up to the block, or if whole selection in same
// block, expand range boundaries to ends of block and split up to body.
var doc = stopNode.ownerDocument; var doc = stopNode.ownerDocument;
var startContainer = range.startContainer; var startContainer = range.startContainer;
var startOffset = range.startOffset; var startOffset = range.startOffset;
var endContainer = range.endContainer; var endContainer = range.endContainer;
var endOffset = range.endOffset; var endOffset = range.endOffset;
// Split end point first to avoid problems when end and start in same container.
split( endContainer, endOffset, stopNode );
split( startContainer, startOffset, stopNode );
range = this._getRangeAndRemoveBookmark(null, true); // Split end point first to avoid problems when end and start
moveRangeBoundariesUpTree( range, stopNode ); // in same container.
this._saveRangeToBookmark( range ); var formattedNodes = doc.createDocumentFragment();
var cleanNodes = doc.createDocumentFragment();
var nodeAfterSplit = split( endContainer, endOffset, stopNode );
var nodeInSplit = split( startContainer, startOffset, stopNode );
var nextNode;
var that = this; // Then replace contents in split with a cleaned version of the same:
// blocks become default blocks, text and leaf nodes survive, everything
// else is obliterated.
while ( nodeInSplit !== nodeAfterSplit ) {
nextNode = nodeInSplit.nextSibling;
formattedNodes.appendChild( nodeInSplit );
nodeInSplit = nextNode;
}
removeFormatting( this, formattedNodes, cleanNodes );
nodeInSplit = cleanNodes.firstChild;
nextNode = cleanNodes.lastChild;
var contents = []; if ( nodeInSplit ) {
forEachChildInRange( stopNode, range, function cleanSingleNode( node ) { stopNode.insertBefore( cleanNodes, nodeAfterSplit );
if ( isContainer( node ) ) { range.setStartBefore( nodeInSplit );
forEachChildInRange( node, range, cleanSingleNode ); range.setEndAfter( nextNode );
} else if ( isBlock( node ) ) { } else {
var block = that.createDefaultBlock(); range.setStartBefore( nodeAfterSplit );
block.appendChild( doc.createTextNode( node.textContent ) ); range.setEndBefore( nodeAfterSplit );
contents.push( block ); }
} else if ( isInline( node ) ) { moveRangeBoundariesDownTree( range );
contents.push( doc.createTextNode( node.textContent ) );
}
});
var oldContents = mapEachChildInRange( stopNode, range, function ( node ) {
return node;
});
contents.forEach( function ( node ) { this.setSelection( range );
stopNode.insertBefore( node, oldContents[0] );
});
oldContents.forEach( function ( node ) {
stopNode.removeChild( node );
});
this.setSelection( this._getRangeAndRemoveBookmark() );
return this; return this;
}; };

File diff suppressed because one or more lines are too long

View file

@ -525,7 +525,7 @@ proto._saveRangeToBookmark = function ( range ) {
range.setEndBefore( endNode ); range.setEndBefore( endNode );
}; };
proto._getRangeAndRemoveBookmark = function ( range, persistSplits ) { proto._getRangeAndRemoveBookmark = function ( range ) {
var doc = this._doc, var doc = this._doc,
start = doc.getElementById( startSelectionId ), start = doc.getElementById( startSelectionId ),
end = doc.getElementById( endSelectionId ); end = doc.getElementById( endSelectionId );
@ -549,12 +549,10 @@ proto._getRangeAndRemoveBookmark = function ( range, persistSplits ) {
detach( start ); detach( start );
detach( end ); detach( end );
if ( !persistSplits ) { // Merge any text nodes we split
// Merge any text nodes we split mergeInlines( startContainer, _range );
mergeInlines( startContainer, _range ); if ( startContainer !== endContainer ) {
if ( startContainer !== endContainer ) { mergeInlines( endContainer, _range );
mergeInlines( endContainer, _range );
}
} }
if ( !range ) { if ( !range ) {
@ -2162,84 +2160,91 @@ proto.setTextDirection = function ( direction ) {
return this.focus(); return this.focus();
}; };
function removeFormatting ( self, root, clean ) {
function forEachChildInRange ( rootNode, range, iterator ) { var node, next;
var childNodes = rootNode.childNodes, for ( node = root.firstChild; node; node = next ) {
node = rootNode.firstChild; next = node.nextSibling;
while ( node ) { if ( isInline( node ) ) {
if ( isNodeContainedInRange( range, node, false ) ) { if ( node.nodeType === TEXT_NODE || isLeaf( node ) ) {
iterator( node ); clean.appendChild( node );
continue;
}
} else if ( isBlock( node ) ) {
clean.appendChild( self.createDefaultBlock([
removeFormatting(
self, node, self._doc.createDocumentFragment() )
]));
continue;
} }
node = node.nextSibling; removeFormatting( self, node, clean );
} }
return clean;
} }
function mapEachChildInRange ( rootNode, range, iterator ) {
var output = [];
forEachChildInRange( rootNode, range, function ( node ) {
output.push( iterator( node ) );
});
return output;
}
var stylingNodeNames = /(^|>)(?:B|I|S|SUB|SUP|U|BLOCKQUOTE|OL|UL|LI|T(?:ABLE|BODY|HEAD|FOOT|R|D|H))(>|$)/;
proto.removeAllFormatting = function ( range ) { proto.removeAllFormatting = function ( range ) {
if ( !range && !( range = this.getSelection() ) || range.collapsed ) { if ( !range && !( range = this.getSelection() ) || range.collapsed ) {
return this; return this;
} }
var stopNode = range.commonAncestorContainer; var stopNode = range.commonAncestorContainer;
while ( stylingNodeNames.test( getPath( stopNode ) ) ) { while ( stopNode && !isBlock( stopNode ) ) {
stopNode = stopNode.parentNode; stopNode = stopNode.parentNode;
} }
if ( !stopNode ) {
expandRangeToBlockBoundaries( range );
stopNode = this._body;
}
if ( stopNode.nodeType === TEXT_NODE ) { if ( stopNode.nodeType === TEXT_NODE ) {
return this; return this;
} }
// Record undo point
this._recordUndoState( range );
this._getRangeAndRemoveBookmark( range );
// Avoid splitting where we're already at edges.
moveRangeBoundariesUpTree( range, stopNode ); moveRangeBoundariesUpTree( range, stopNode );
this._saveRangeToBookmark( range );
// Split the selection up to the block, or if whole selection in same
// block, expand range boundaries to ends of block and split up to body.
var doc = stopNode.ownerDocument; var doc = stopNode.ownerDocument;
var startContainer = range.startContainer; var startContainer = range.startContainer;
var startOffset = range.startOffset; var startOffset = range.startOffset;
var endContainer = range.endContainer; var endContainer = range.endContainer;
var endOffset = range.endOffset; var endOffset = range.endOffset;
// Split end point first to avoid problems when end and start in same container.
split( endContainer, endOffset, stopNode );
split( startContainer, startOffset, stopNode );
range = this._getRangeAndRemoveBookmark( null, true ); // Split end point first to avoid problems when end and start
moveRangeBoundariesUpTree( range, stopNode ); // in same container.
this._saveRangeToBookmark( range ); var formattedNodes = doc.createDocumentFragment();
var cleanNodes = doc.createDocumentFragment();
var nodeAfterSplit = split( endContainer, endOffset, stopNode );
var nodeInSplit = split( startContainer, startOffset, stopNode );
var nextNode;
var that = this; // Then replace contents in split with a cleaned version of the same:
// blocks become default blocks, text and leaf nodes survive, everything
// else is obliterated.
while ( nodeInSplit !== nodeAfterSplit ) {
nextNode = nodeInSplit.nextSibling;
formattedNodes.appendChild( nodeInSplit );
nodeInSplit = nextNode;
}
removeFormatting( this, formattedNodes, cleanNodes );
nodeInSplit = cleanNodes.firstChild;
nextNode = cleanNodes.lastChild;
var contents = []; if ( nodeInSplit ) {
forEachChildInRange( stopNode, range, function cleanSingleNode ( node ) { stopNode.insertBefore( cleanNodes, nodeAfterSplit );
if ( isContainer( node ) ) { range.setStartBefore( nodeInSplit );
forEachChildInRange( node, range, cleanSingleNode ); range.setEndAfter( nextNode );
} else if ( isBlock( node ) ) { } else {
var block = that.createDefaultBlock(); range.setStartBefore( nodeAfterSplit );
block.appendChild( doc.createTextNode( node.textContent ) ); range.setEndBefore( nodeAfterSplit );
contents.push( block ); }
} else if ( isInline( node ) ) { moveRangeBoundariesDownTree( range );
contents.push( doc.createTextNode( node.textContent ) );
}
});
var oldContents = mapEachChildInRange( stopNode, range, function ( node ) {
return node;
});
contents.forEach( function ( node ) { this.setSelection( range );
stopNode.insertBefore( node, oldContents[0] );
});
oldContents.forEach( function ( node ) {
stopNode.removeChild( node );
});
this.setSelection( this._getRangeAndRemoveBookmark() );
return this; return this;
}; };

View file

@ -43,13 +43,13 @@ describe('Squire RTE', function () {
expect(editor, 'to contain HTML', '<div>one two three four five</div>'); expect(editor, 'to contain HTML', '<div>one two three four five</div>');
}); });
it('removes block styles', function () { it('removes block styles', function () {
var startHTML = '<div><blockquote>one</blockquote><ul><li>two</li></ul>' + var startHTML = '<div><blockquote>one</blockquote><ul><li>two</li></ul>' +
'<ol><li>three</li></ol><table><tbody><tr><th>four</th><td>five</td></tr></tbody></table></div>'; '<ol><li>three</li></ol><table><tbody><tr><th>four</th><td>five</td></tr></tbody></table></div>';
editor.setHTML(startHTML); editor.setHTML(startHTML);
expect(editor, 'to contain HTML', startHTML); expect(editor, 'to contain HTML', startHTML);
selectAll(editor); selectAll(editor);
editor.removeAllFormatting(); editor.removeAllFormatting();
var expectedHTML = '<div><div>one</div><div>two</div><div>three</div><div>four</div><div>five</div></div>'; var expectedHTML = '<div>one</div><div>two</div><div>three</div><div>four</div><div>five</div>';
expect(editor, 'to contain HTML', expectedHTML); expect(editor, 'to contain HTML', expectedHTML);
}); });