mirror of
https://github.com/fastmail/Squire.git
synced 2025-01-03 13:16:31 -05:00
Merge pull request #101 from gertsonderby-feature/remove-formatting
Implements #7 (Remove All Formatting command).
This commit is contained in:
commit
a712b4f8e7
11 changed files with 346 additions and 6 deletions
6
.jscsrc
Normal file
6
.jscsrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"disallowSpacesInsideParentheses": false,
|
||||||
|
"disallowSpacesInFunctionDeclaration": {
|
||||||
|
"beforeOpeningRoundBrace": false
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
Makefile
|
Makefile
|
||||||
|
.jscsrc
|
||||||
.jshintrc
|
.jshintrc
|
||||||
.gitignore
|
.gitignore
|
||||||
.editorconfig
|
.editorconfig
|
||||||
|
|
|
@ -43,6 +43,8 @@
|
||||||
<span id="underline">Underline</span>
|
<span id="underline">Underline</span>
|
||||||
<span id="removeUnderline">Deunderline</span>
|
<span id="removeUnderline">Deunderline</span>
|
||||||
|
|
||||||
|
<span id="removeAllFormatting">Remove formatting</span>
|
||||||
|
|
||||||
<span id="setFontSize" class="prompt">Font size</span>
|
<span id="setFontSize" class="prompt">Font size</span>
|
||||||
<span id="setFontFace" class="prompt">Font face</span>
|
<span id="setFontFace" class="prompt">Font face</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -403,3 +403,9 @@ Returns self (the Squire instance).
|
||||||
Decreases by 1 the nesting level of any at-least-partially selected blocks which are part of a list.
|
Decreases by 1 the nesting level of any at-least-partially selected blocks which are part of a list.
|
||||||
|
|
||||||
Returns self (the Squire instance).
|
Returns self (the Squire instance).
|
||||||
|
|
||||||
|
### removeAllFormatting
|
||||||
|
|
||||||
|
Removes all formatting from the selection. Block elements (list items, table cells, etc.) are kept as separate blocks.
|
||||||
|
|
||||||
|
Returns self (the Squire instance).
|
||||||
|
|
|
@ -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 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3618,6 +3619,95 @@ proto.setTextDirection = function ( direction ) {
|
||||||
return this.focus();
|
return this.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function removeFormatting ( self, root, clean ) {
|
||||||
|
var node, next;
|
||||||
|
for ( node = root.firstChild; node; node = next ) {
|
||||||
|
next = node.nextSibling;
|
||||||
|
if ( isInline( node ) ) {
|
||||||
|
if ( node.nodeType === TEXT_NODE || isLeaf( node ) ) {
|
||||||
|
clean.appendChild( node );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if ( isBlock( node ) ) {
|
||||||
|
clean.appendChild( self.createDefaultBlock([
|
||||||
|
removeFormatting(
|
||||||
|
self, node, self._doc.createDocumentFragment() )
|
||||||
|
]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
removeFormatting( self, node, clean );
|
||||||
|
}
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
proto.removeAllFormatting = function ( range ) {
|
||||||
|
if ( !range && !( range = this.getSelection() ) || range.collapsed ) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stopNode = range.commonAncestorContainer;
|
||||||
|
while ( stopNode && !isBlock( stopNode ) ) {
|
||||||
|
stopNode = stopNode.parentNode;
|
||||||
|
}
|
||||||
|
if ( !stopNode ) {
|
||||||
|
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 );
|
||||||
|
|
||||||
|
// 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 startContainer = range.startContainer;
|
||||||
|
var startOffset = range.startOffset;
|
||||||
|
var endContainer = range.endContainer;
|
||||||
|
var endOffset = range.endOffset;
|
||||||
|
|
||||||
|
// Split end point first to avoid problems when end and start
|
||||||
|
// in same container.
|
||||||
|
var formattedNodes = doc.createDocumentFragment();
|
||||||
|
var cleanNodes = doc.createDocumentFragment();
|
||||||
|
var nodeAfterSplit = split( endContainer, endOffset, stopNode );
|
||||||
|
var nodeInSplit = split( startContainer, startOffset, stopNode );
|
||||||
|
var nextNode;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
if ( nodeInSplit ) {
|
||||||
|
stopNode.insertBefore( cleanNodes, nodeAfterSplit );
|
||||||
|
range.setStartBefore( nodeInSplit );
|
||||||
|
range.setEndAfter( nextNode );
|
||||||
|
} else {
|
||||||
|
range.setStartBefore( nodeAfterSplit );
|
||||||
|
range.setEndBefore( nodeAfterSplit );
|
||||||
|
}
|
||||||
|
moveRangeBoundariesDownTree( range );
|
||||||
|
|
||||||
|
this.setSelection( range );
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
proto.increaseQuoteLevel = command( 'modifyBlocks', increaseBlockQuoteLevel );
|
proto.increaseQuoteLevel = command( 'modifyBlocks', increaseBlockQuoteLevel );
|
||||||
proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
|
proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -24,6 +24,8 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/neilj/Squire",
|
"homepage": "https://github.com/neilj/Squire",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"uglify-js": "^2.4.15"
|
"mocha": "2.2.5",
|
||||||
|
"uglify-js": "^2.4.15",
|
||||||
|
"unexpected": "8.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2160,6 +2160,95 @@ proto.setTextDirection = function ( direction ) {
|
||||||
return this.focus();
|
return this.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function removeFormatting ( self, root, clean ) {
|
||||||
|
var node, next;
|
||||||
|
for ( node = root.firstChild; node; node = next ) {
|
||||||
|
next = node.nextSibling;
|
||||||
|
if ( isInline( node ) ) {
|
||||||
|
if ( node.nodeType === TEXT_NODE || isLeaf( node ) ) {
|
||||||
|
clean.appendChild( node );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if ( isBlock( node ) ) {
|
||||||
|
clean.appendChild( self.createDefaultBlock([
|
||||||
|
removeFormatting(
|
||||||
|
self, node, self._doc.createDocumentFragment() )
|
||||||
|
]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
removeFormatting( self, node, clean );
|
||||||
|
}
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
proto.removeAllFormatting = function ( range ) {
|
||||||
|
if ( !range && !( range = this.getSelection() ) || range.collapsed ) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stopNode = range.commonAncestorContainer;
|
||||||
|
while ( stopNode && !isBlock( stopNode ) ) {
|
||||||
|
stopNode = stopNode.parentNode;
|
||||||
|
}
|
||||||
|
if ( !stopNode ) {
|
||||||
|
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 );
|
||||||
|
|
||||||
|
// 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 startContainer = range.startContainer;
|
||||||
|
var startOffset = range.startOffset;
|
||||||
|
var endContainer = range.endContainer;
|
||||||
|
var endOffset = range.endOffset;
|
||||||
|
|
||||||
|
// Split end point first to avoid problems when end and start
|
||||||
|
// in same container.
|
||||||
|
var formattedNodes = doc.createDocumentFragment();
|
||||||
|
var cleanNodes = doc.createDocumentFragment();
|
||||||
|
var nodeAfterSplit = split( endContainer, endOffset, stopNode );
|
||||||
|
var nodeInSplit = split( startContainer, startOffset, stopNode );
|
||||||
|
var nextNode;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
if ( nodeInSplit ) {
|
||||||
|
stopNode.insertBefore( cleanNodes, nodeAfterSplit );
|
||||||
|
range.setStartBefore( nodeInSplit );
|
||||||
|
range.setEndAfter( nextNode );
|
||||||
|
} else {
|
||||||
|
range.setStartBefore( nodeAfterSplit );
|
||||||
|
range.setEndBefore( nodeAfterSplit );
|
||||||
|
}
|
||||||
|
moveRangeBoundariesDownTree( range );
|
||||||
|
|
||||||
|
this.setSelection( range );
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
proto.increaseQuoteLevel = command( 'modifyBlocks', increaseBlockQuoteLevel );
|
proto.increaseQuoteLevel = command( 'modifyBlocks', increaseBlockQuoteLevel );
|
||||||
proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
|
proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
|
||||||
|
|
||||||
|
|
0
test/blank.html
Normal file
0
test/blank.html
Normal file
23
test/index.html
Normal file
23
test/index.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Squire rich-text editor tests</title>
|
||||||
|
<link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="../node_modules/mocha/mocha.js"></script>
|
||||||
|
<script src="../node_modules/unexpected/unexpected.js"></script>
|
||||||
|
<script src="../build/squire-raw.js"></script>
|
||||||
|
<iframe id="testFrame" style="visibility: hidden;"></iframe>
|
||||||
|
<div id="mocha"></div>
|
||||||
|
<script>
|
||||||
|
mocha.setup('bdd');
|
||||||
|
var expect = weknowhow.expect;
|
||||||
|
</script>
|
||||||
|
<script src="squire.spec.js"></script>
|
||||||
|
<script>
|
||||||
|
mocha.run();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
121
test/squire.spec.js
Normal file
121
test/squire.spec.js
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
/*global expect, describe, afterEach, beforeEach, it */
|
||||||
|
expect = expect.clone()
|
||||||
|
.addType({
|
||||||
|
name: 'SquireRTE',
|
||||||
|
base: 'object',
|
||||||
|
identify: function (value) {
|
||||||
|
return value instanceof Squire;
|
||||||
|
},
|
||||||
|
inspect: function (value) {
|
||||||
|
return 'Squire RTE: ' + value.getHTML();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addAssertion('[not] to contain HTML', function (expect, editor, expectedValue) {
|
||||||
|
this.errorMode = 'bubble';
|
||||||
|
var actualHTML = editor.getHTML().replace(/<br>/g, '');
|
||||||
|
// BR tags are inconsistent across browsers. Removing them allows cross-browser testing.
|
||||||
|
expect(actualHTML, '[not] to be', expectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Squire RTE', function () {
|
||||||
|
var doc, editor;
|
||||||
|
beforeEach(function () {
|
||||||
|
var iframe = document.getElementById('testFrame');
|
||||||
|
doc = iframe.contentDocument;
|
||||||
|
editor = new Squire(doc);
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectAll(editor) {
|
||||||
|
var range = doc.createRange();
|
||||||
|
range.setStart(doc.body.childNodes.item(0), 0);
|
||||||
|
range.setEnd(doc.body.childNodes.item(0), doc.body.childNodes.item(0).childNodes.length);
|
||||||
|
editor.setSelection(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('removeAllFormatting', function () {
|
||||||
|
// Trivial cases
|
||||||
|
it('removes inline styles', function () {
|
||||||
|
var startHTML = '<div><i>one</i> <b>two</b> <u>three</u> <sub>four</sub> <sup>five</sup></div>';
|
||||||
|
editor.setHTML(startHTML);
|
||||||
|
expect(editor, 'to contain HTML', startHTML);
|
||||||
|
selectAll(editor);
|
||||||
|
editor.removeAllFormatting();
|
||||||
|
expect(editor, 'to contain HTML', '<div>one two three four five</div>');
|
||||||
|
});
|
||||||
|
it('removes block styles', function () {
|
||||||
|
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>';
|
||||||
|
editor.setHTML(startHTML);
|
||||||
|
expect(editor, 'to contain HTML', startHTML);
|
||||||
|
selectAll(editor);
|
||||||
|
editor.removeAllFormatting();
|
||||||
|
var expectedHTML = '<div>one</div><div>two</div><div>three</div><div>four</div><div>five</div>';
|
||||||
|
expect(editor, 'to contain HTML', expectedHTML);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Potential bugs
|
||||||
|
it('removes styles that begin inside the range', function () {
|
||||||
|
var startHTML = '<div>one <i>two three four five</i></div>';
|
||||||
|
editor.setHTML(startHTML);
|
||||||
|
expect(editor, 'to contain HTML', startHTML);
|
||||||
|
var range = doc.createRange();
|
||||||
|
range.setStart(doc.body.childNodes.item(0), 0);
|
||||||
|
range.setEnd(doc.getElementsByTagName('i').item(0).childNodes.item(0), 4);
|
||||||
|
editor.removeAllFormatting(range);
|
||||||
|
expect(editor, 'to contain HTML', '<div>one two <i>three four five</i></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes styles that end inside the range', function () {
|
||||||
|
var startHTML = '<div><i>one two three four</i> five</div>';
|
||||||
|
editor.setHTML(startHTML);
|
||||||
|
expect(editor, 'to contain HTML', startHTML);
|
||||||
|
var range = doc.createRange();
|
||||||
|
range.setStart(doc.getElementsByTagName('i').item(0).childNodes.item(0), 13);
|
||||||
|
range.setEnd(doc.body.childNodes.item(0), doc.body.childNodes.item(0).childNodes.length);
|
||||||
|
editor.removeAllFormatting(range);
|
||||||
|
expect(editor, 'to contain HTML', '<div><i>one two three</i> four five</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes styles enclosed by the range', function () {
|
||||||
|
var startHTML = '<div>one <i>two three four</i> five</div>';
|
||||||
|
editor.setHTML(startHTML);
|
||||||
|
expect(editor, 'to contain HTML', startHTML);
|
||||||
|
var range = doc.createRange();
|
||||||
|
range.setStart(doc.body.childNodes.item(0), 0);
|
||||||
|
range.setEnd(doc.body.childNodes.item(0), doc.body.childNodes.item(0).childNodes.length);
|
||||||
|
editor.removeAllFormatting(range);
|
||||||
|
expect(editor, 'to contain HTML', '<div>one two three four five</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes styles enclosing the range', function () {
|
||||||
|
var startHTML = '<div><i>one two three four five</i></div>';
|
||||||
|
editor.setHTML(startHTML);
|
||||||
|
expect(editor, 'to contain HTML', startHTML);
|
||||||
|
var range = doc.createRange();
|
||||||
|
range.setStart(doc.getElementsByTagName('i').item(0).childNodes.item(0), 4);
|
||||||
|
range.setEnd(doc.getElementsByTagName('i').item(0).childNodes.item(0), 18);
|
||||||
|
editor.removeAllFormatting(range);
|
||||||
|
expect(editor, 'to contain HTML', '<div><i>one </i>two three four<i> five</i></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes nested styles and closes tags correctly', function () {
|
||||||
|
var startHTML = '<table><tbody><tr><td>one</td></tr><tr><td>two</td><td>three</td></tr><tr><td>four</td><td>five</td></tr></tbody></table>';
|
||||||
|
editor.setHTML(startHTML);
|
||||||
|
expect(editor, 'to contain HTML', startHTML);
|
||||||
|
var range = doc.createRange();
|
||||||
|
range.setStart(doc.getElementsByTagName('td').item(1), 0);
|
||||||
|
range.setEnd(doc.getElementsByTagName('td').item(2), doc.getElementsByTagName('td').item(2).childNodes.length);
|
||||||
|
editor.removeAllFormatting(range);
|
||||||
|
expect(editor, 'to contain HTML', '<table><tbody><tr><td>one</td></tr></tbody></table>' +
|
||||||
|
'<div>two</div>' +
|
||||||
|
'<div>three</div>' +
|
||||||
|
'<table><tbody><tr><td>four</td><td>five</td></tr></tbody></table>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
editor = null;
|
||||||
|
var iframe = document.getElementById('testFrame');
|
||||||
|
iframe.src = 'blank.html';
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue