0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 23:40:35 -05:00

Fix formatting commands when selection has no text nodes

Fixes #60
This commit is contained in:
Neil Jenkins 2015-04-08 15:33:27 +07:00
parent 1b9606452e
commit e6ae25b589
3 changed files with 81 additions and 57 deletions

View file

@ -1700,7 +1700,7 @@ proto._addFormat = function ( tag, attributes, range ) {
// If the range is collapsed we simply insert the node by wrapping // If the range is collapsed we simply insert the node by wrapping
// it round the range and focus it. // it round the range and focus it.
var el, walker, startContainer, endContainer, startOffset, endOffset, var el, walker, startContainer, endContainer, startOffset, endOffset,
textNode, needsFormat; node, needsFormat;
if ( range.collapsed ) { if ( range.collapsed ) {
el = fixCursor( this.createElement( tag, attributes ) ); el = fixCursor( this.createElement( tag, attributes ) );
@ -1712,16 +1712,21 @@ proto._addFormat = function ( tag, attributes, range ) {
// partially selected nodes) and if they're not already formatted // partially selected nodes) and if they're not already formatted
// correctly we wrap them in the appropriate tag. // correctly we wrap them in the appropriate tag.
else { else {
// We don't want to apply formatting twice so we check each text
// node to see if it has an ancestor with the formatting already.
// Create an iterator to walk over all the text nodes under this // Create an iterator to walk over all the text nodes under this
// ancestor which are in the range and not already formatted // ancestor which are in the range and not already formatted
// correctly. // correctly.
//
// In Blink/WebKit, empty blocks may have no text nodes, just a <br>.
// Therefore we wrap this in the tag as well, as this will then cause it
// to apply when the user types something in the block, which is
// presumably what was intended.
walker = new TreeWalker( walker = new TreeWalker(
range.commonAncestorContainer, range.commonAncestorContainer,
SHOW_TEXT, SHOW_TEXT|SHOW_ELEMENT,
function ( node ) { function ( node ) {
return isNodeContainedInRange( range, node, true ); return ( node.nodeType === TEXT_NODE ||
node.nodeName === 'BR' ) &&
isNodeContainedInRange( range, node, true );
}, },
false false
); );
@ -1733,41 +1738,53 @@ proto._addFormat = function ( tag, attributes, range ) {
endContainer = range.endContainer; endContainer = range.endContainer;
endOffset = range.endOffset; endOffset = range.endOffset;
// Make sure we start inside a text node. // Make sure we start with a valid node.
walker.currentNode = startContainer; walker.currentNode = startContainer;
if ( startContainer.nodeType !== TEXT_NODE ) { if ( !walker.filter( startContainer ) ) {
startContainer = walker.nextNode(); startContainer = walker.nextNode();
startOffset = 0; startOffset = 0;
} }
do { // If there are no interesting nodes in the selection, abort
textNode = walker.currentNode; if ( !startContainer ) {
needsFormat = !getNearest( textNode, tag, attributes ); return range;
if ( needsFormat ) {
if ( textNode === endContainer &&
textNode.length > endOffset ) {
textNode.splitText( endOffset );
} }
if ( textNode === startContainer && startOffset ) {
textNode = textNode.splitText( startOffset ); do {
node = walker.currentNode;
needsFormat = !getNearest( node, tag, attributes );
if ( needsFormat ) {
// <br> can never be a container node, so must have a text node
// if node == (end|start)Container
if ( node === endContainer && node.length > endOffset ) {
node.splitText( endOffset );
}
if ( node === startContainer && startOffset ) {
node = node.splitText( startOffset );
if ( endContainer === startContainer ) { if ( endContainer === startContainer ) {
endContainer = textNode; endContainer = node;
endOffset -= startOffset; endOffset -= startOffset;
} }
startContainer = textNode; startContainer = node;
startOffset = 0; startOffset = 0;
} }
el = this.createElement( tag, attributes ); el = this.createElement( tag, attributes );
replaceWith( textNode, el ); replaceWith( node, el );
el.appendChild( textNode ); el.appendChild( node );
} }
} while ( walker.nextNode() ); } while ( walker.nextNode() );
// Make sure we finish inside a text node. Otherwise offset may have // If we don't finish inside a text node, offset may have changed.
// changed.
if ( endContainer.nodeType !== TEXT_NODE ) { if ( endContainer.nodeType !== TEXT_NODE ) {
endContainer = textNode; if ( node.nodeType === TEXT_NODE ) {
endOffset = textNode.length; endContainer = node;
endOffset = node.length;
} else {
// If <br>, we must have just wrapped it, so it must have only
// one child
endContainer = node.parentNode;
endOffset = 1;
}
} }
// Now set the selection to as it was before // Now set the selection to as it was before
@ -2557,17 +2574,7 @@ var cleanupBRs = function ( root ) {
proto._ensureBottomLine = function () { proto._ensureBottomLine = function () {
var body = this._body, var body = this._body,
last; last = body.lastElementChild;
// Safari (+others?) adds white-space text nodes to the end of <body>
// for no apparent reason. Remove them, since they're semantically
// meaningless.
while ( last = body.lastChild ) {
if ( last.nodeType === TEXT_NODE && !notWS.test( last.data ) ) {
body.removeChild( last );
} else {
break;
}
}
if ( !last || last.nodeName !== this.defaultBlockTag || !isBlock( last ) ) { if ( !last || last.nodeName !== this.defaultBlockTag || !isBlock( last ) ) {
body.appendChild( this.createDefaultBlock() ); body.appendChild( this.createDefaultBlock() );
} }

File diff suppressed because one or more lines are too long

View file

@ -610,7 +610,7 @@ proto._addFormat = function ( tag, attributes, range ) {
// If the range is collapsed we simply insert the node by wrapping // If the range is collapsed we simply insert the node by wrapping
// it round the range and focus it. // it round the range and focus it.
var el, walker, startContainer, endContainer, startOffset, endOffset, var el, walker, startContainer, endContainer, startOffset, endOffset,
textNode, needsFormat; node, needsFormat;
if ( range.collapsed ) { if ( range.collapsed ) {
el = fixCursor( this.createElement( tag, attributes ) ); el = fixCursor( this.createElement( tag, attributes ) );
@ -622,16 +622,21 @@ proto._addFormat = function ( tag, attributes, range ) {
// partially selected nodes) and if they're not already formatted // partially selected nodes) and if they're not already formatted
// correctly we wrap them in the appropriate tag. // correctly we wrap them in the appropriate tag.
else { else {
// We don't want to apply formatting twice so we check each text
// node to see if it has an ancestor with the formatting already.
// Create an iterator to walk over all the text nodes under this // Create an iterator to walk over all the text nodes under this
// ancestor which are in the range and not already formatted // ancestor which are in the range and not already formatted
// correctly. // correctly.
//
// In Blink/WebKit, empty blocks may have no text nodes, just a <br>.
// Therefore we wrap this in the tag as well, as this will then cause it
// to apply when the user types something in the block, which is
// presumably what was intended.
walker = new TreeWalker( walker = new TreeWalker(
range.commonAncestorContainer, range.commonAncestorContainer,
SHOW_TEXT, SHOW_TEXT|SHOW_ELEMENT,
function ( node ) { function ( node ) {
return isNodeContainedInRange( range, node, true ); return ( node.nodeType === TEXT_NODE ||
node.nodeName === 'BR' ) &&
isNodeContainedInRange( range, node, true );
}, },
false false
); );
@ -643,41 +648,53 @@ proto._addFormat = function ( tag, attributes, range ) {
endContainer = range.endContainer; endContainer = range.endContainer;
endOffset = range.endOffset; endOffset = range.endOffset;
// Make sure we start inside a text node. // Make sure we start with a valid node.
walker.currentNode = startContainer; walker.currentNode = startContainer;
if ( startContainer.nodeType !== TEXT_NODE ) { if ( !walker.filter( startContainer ) ) {
startContainer = walker.nextNode(); startContainer = walker.nextNode();
startOffset = 0; startOffset = 0;
} }
do { // If there are no interesting nodes in the selection, abort
textNode = walker.currentNode; if ( !startContainer ) {
needsFormat = !getNearest( textNode, tag, attributes ); return range;
if ( needsFormat ) {
if ( textNode === endContainer &&
textNode.length > endOffset ) {
textNode.splitText( endOffset );
} }
if ( textNode === startContainer && startOffset ) {
textNode = textNode.splitText( startOffset ); do {
node = walker.currentNode;
needsFormat = !getNearest( node, tag, attributes );
if ( needsFormat ) {
// <br> can never be a container node, so must have a text node
// if node == (end|start)Container
if ( node === endContainer && node.length > endOffset ) {
node.splitText( endOffset );
}
if ( node === startContainer && startOffset ) {
node = node.splitText( startOffset );
if ( endContainer === startContainer ) { if ( endContainer === startContainer ) {
endContainer = textNode; endContainer = node;
endOffset -= startOffset; endOffset -= startOffset;
} }
startContainer = textNode; startContainer = node;
startOffset = 0; startOffset = 0;
} }
el = this.createElement( tag, attributes ); el = this.createElement( tag, attributes );
replaceWith( textNode, el ); replaceWith( node, el );
el.appendChild( textNode ); el.appendChild( node );
} }
} while ( walker.nextNode() ); } while ( walker.nextNode() );
// Make sure we finish inside a text node. Otherwise offset may have // If we don't finish inside a text node, offset may have changed.
// changed.
if ( endContainer.nodeType !== TEXT_NODE ) { if ( endContainer.nodeType !== TEXT_NODE ) {
endContainer = textNode; if ( node.nodeType === TEXT_NODE ) {
endOffset = textNode.length; endContainer = node;
endOffset = node.length;
} else {
// If <br>, we must have just wrapped it, so it must have only
// one child
endContainer = node.parentNode;
endOffset = 1;
}
} }
// Now set the selection to as it was before // Now set the selection to as it was before