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:
parent
d9872fb4b2
commit
345159b4c4
4 changed files with 137 additions and 126 deletions
|
@ -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
121
source/Editor.js
121
source/Editor.js
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue