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

Improved algorithm for inserting tree into range

Fixes #283
This commit is contained in:
Neil Jenkins 2017-07-15 20:33:32 +10:00
parent 239b7d19e9
commit 48fabd491a
6 changed files with 200 additions and 246 deletions

View file

@ -265,6 +265,10 @@ function getNextBlock ( node, root ) {
return node !== root ? node : null;
}
function isEmptyBlock ( block ) {
return !block.textContent && !block.querySelector( 'IMG' );
}
function areAlike ( node, node2 ) {
return !isLeaf( node ) && (
node.nodeType === node2.nodeType &&
@ -348,7 +352,7 @@ function getPath ( node, root ) {
function getLength ( node ) {
var nodeType = node.nodeType;
return nodeType === ELEMENT_NODE ?
return nodeType === ELEMENT_NODE || nodeType === DOCUMENT_FRAGMENT_NODE ?
node.childNodes.length : node.length || 0;
}
@ -651,11 +655,14 @@ function mergeInlines ( node, range ) {
}
}
function mergeWithBlock ( block, next, range ) {
var container = next,
last, offset;
while ( container.parentNode.childNodes.length === 1 ) {
container = container.parentNode;
function mergeWithBlock ( block, next, range, root ) {
var container = next;
var parent, last, offset;
while ( ( parent = container.parentNode ) &&
parent !== root &&
parent.nodeType === ELEMENT_NODE &&
parent.childNodes.length === 1 ) {
container = parent;
}
detach( container );
@ -879,7 +886,7 @@ var deleteContentsOfRange = function ( range, root ) {
// endBlock will have been split, so need to refetch
endBlock = getEndBlockOfRange( range, root );
if ( startBlock && endBlock && startBlock !== endBlock ) {
mergeWithBlock( startBlock, endBlock, range );
mergeWithBlock( startBlock, endBlock, range, root );
}
}
@ -901,134 +908,107 @@ var deleteContentsOfRange = function ( range, root ) {
// ---
// Contents of range will be deleted.
// After method, range will be around inserted content
var insertTreeFragmentIntoRange = function ( range, frag, root ) {
// Check if it's all inline content
var allInline = true,
children = frag.childNodes,
l = children.length;
while ( l-- ) {
if ( !isInline( children[l] ) ) {
allInline = false;
break;
}
var node, block, blockContentsAfterSplit, stopPoint, container, offset;
var nodeAfterSplit, nodeBeforeSplit, tempRange;
// Fixup content: ensure no top-level inline, and add cursor fix elements.
fixContainer( frag, root );
node = frag;
while ( ( node = getNextBlock( node, root ) ) ) {
fixCursor( node, root );
}
// Delete any selected content
// Delete any selected content.
if ( !range.collapsed ) {
deleteContentsOfRange( range, root );
}
// Move range down into text nodes
// Move range down into text nodes.
moveRangeBoundariesDownTree( range );
range.collapse( false ); // collapse to end
if ( allInline ) {
// If inline, just insert at the current position.
insertNodeInRange( range, frag );
if ( range.startContainer !== range.endContainer ) {
mergeInlines( range.endContainer, range );
}
mergeInlines( range.startContainer, range );
range.collapse( false );
} else {
// Otherwise...
// 1. Split up to blockquote (if a parent) or root
var splitPoint = range.startContainer,
// Where will we split up to? First blockquote parent, otherwise root.
stopPoint = getNearest( range.endContainer, root, 'BLOCKQUOTE' ) || root;
// Merge the contents of the first block in the frag with the focused block.
// If there are contents in the block after the focus point, collect this
// up to insert in the last block later
block = getStartBlockOfRange( range );
if ( block ) {
moveRangeBoundariesUpTree( range, block, block, root );
range.collapse( true ); // collapse to start
container = range.endContainer;
offset = range.endOffset;
// Remove trailing <br>  we don't want this considered content to be
// inserted again later
cleanupBRs( block, root, false );
if ( isInline( container ) ) {
// Split up to block parent.
nodeAfterSplit = split(
splitPoint,
range.startOffset,
getNearest( splitPoint.parentNode, root, 'BLOCKQUOTE' ) || root,
root
),
nodeBeforeSplit = nodeAfterSplit.previousSibling,
startContainer = nodeBeforeSplit,
startOffset = startContainer.childNodes.length,
endContainer = nodeAfterSplit,
endOffset = 0,
parent = nodeAfterSplit.parentNode,
child, node, prev, next, startAnchor;
// 2. Move down into edge either side of split and insert any inline
// nodes at the beginning/end of the fragment
while ( ( child = startContainer.lastChild ) &&
child.nodeType === ELEMENT_NODE ) {
if ( child.nodeName === 'BR' ) {
startOffset -= 1;
break;
container, offset, getPreviousBlock( container, root ), root );
container = nodeAfterSplit.parentNode;
offset = indexOf.call( container.childNodes, nodeAfterSplit );
}
if ( /*isBlock( container ) && */offset !== getLength( container ) ) {
// Collect any inline contents of the block after the range point
blockContentsAfterSplit =
root.ownerDocument.createDocumentFragment();
while ( ( node = container.childNodes[ offset ] ) ) {
blockContentsAfterSplit.appendChild( node );
}
startContainer = child;
startOffset = startContainer.childNodes.length;
}
while ( ( child = endContainer.firstChild ) &&
child.nodeType === ELEMENT_NODE &&
child.nodeName !== 'BR' ) {
endContainer = child;
}
startAnchor = startContainer.childNodes[ startOffset ] || null;
while ( ( child = frag.firstChild ) && isInline( child ) ) {
startContainer.insertBefore( child, startAnchor );
}
while ( ( child = frag.lastChild ) && isInline( child ) ) {
endContainer.insertBefore( child, endContainer.firstChild );
endOffset += 1;
}
// And merge the first block in.
mergeWithBlock( container, getNextBlock( frag, frag ), range, root );
// 3. Fix cursor then insert block(s) in the fragment
node = frag;
while ( node = getNextBlock( node, root ) ) {
fixCursor( node, root );
}
parent.insertBefore( frag, nodeAfterSplit );
// And where we will insert
offset = indexOf.call( container.parentNode.childNodes, container ) + 1;
container = container.parentNode;
range.setEnd( container, offset );
}
// 4. Remove empty nodes created either side of split, then
// merge containers at the edges.
next = nodeBeforeSplit.nextSibling;
node = getPreviousBlock( next, root );
if ( node && !/\S/.test( node.textContent ) ) {
do {
parent = node.parentNode;
parent.removeChild( node );
node = parent;
} while ( node && !node.lastChild && node !== root );
}
if ( !nodeBeforeSplit.parentNode ) {
nodeBeforeSplit = next.previousSibling;
}
if ( !startContainer.parentNode ) {
startContainer = nodeBeforeSplit || next.parentNode;
startOffset = nodeBeforeSplit ?
nodeBeforeSplit.childNodes.length : 0;
}
// Merge inserted containers with edges of split
if ( isContainer( next ) ) {
mergeContainers( next, root );
// Is there still any content in the fragment?
if ( getLength( frag ) ) {
moveRangeBoundariesUpTree( range, stopPoint, stopPoint, root );
// Now split after block up to blockquote (if a parent) or root
nodeAfterSplit = split(
range.endContainer, range.endOffset, stopPoint, root );
nodeBeforeSplit = nodeAfterSplit ?
nodeAfterSplit.previousSibling :
stopPoint.lastChild;
stopPoint.insertBefore( frag, nodeAfterSplit );
if ( nodeAfterSplit ) {
range.setEndBefore( nodeAfterSplit );
} else {
range.setEnd( stopPoint, getLength( stopPoint ) );
}
block = getEndBlockOfRange( range, root );
// Get a reference that won't be invalidated if we merge containers.
moveRangeBoundariesDownTree( range );
container = range.endContainer;
offset = range.endOffset;
prev = nodeAfterSplit.previousSibling;
node = isBlock( nodeAfterSplit ) ?
nodeAfterSplit : getNextBlock( nodeAfterSplit, root );
if ( node && !/\S/.test( node.textContent ) ) {
do {
parent = node.parentNode;
parent.removeChild( node );
node = parent;
} while ( node && !node.lastChild && node !== root );
}
if ( !nodeAfterSplit.parentNode ) {
nodeAfterSplit = prev.nextSibling;
}
if ( !endOffset ) {
endContainer = prev;
endOffset = prev.childNodes.length;
}
// Merge inserted containers with edges of split
if ( nodeAfterSplit && isContainer( nodeAfterSplit ) ) {
mergeContainers( nodeAfterSplit, root );
}
range.setStart( startContainer, startOffset );
range.setEnd( endContainer, endOffset );
moveRangeBoundariesDownTree( range );
nodeAfterSplit = nodeBeforeSplit && nodeBeforeSplit.nextSibling;
if ( nodeAfterSplit && isContainer( nodeAfterSplit ) ) {
mergeContainers( nodeAfterSplit, root );
}
range.setEnd( container, offset );
}
// Insert inline content saved from before.
if ( blockContentsAfterSplit ) {
tempRange = range.cloneRange();
mergeWithBlock( block, blockContentsAfterSplit, tempRange, root );
range.setEnd( tempRange.endContainer, tempRange.endOffset );
}
moveRangeBoundariesDownTree( range );
};
// ---
@ -1467,7 +1447,7 @@ var keyHandlers = {
block = parent;
}
if ( !block.textContent ) {
if ( isEmptyBlock( block ) ) {
// Break list
if ( getNearest( block, root, 'UL' ) ||
getNearest( block, root, 'OL' ) ) {
@ -1560,7 +1540,7 @@ var keyHandlers = {
return;
}
// Otherwise merge.
mergeWithBlock( previous, current, range );
mergeWithBlock( previous, current, range, root );
// If deleted line between containers, merge newly adjacent
// containers.
current = previous.parentNode;
@ -1627,7 +1607,7 @@ var keyHandlers = {
return;
}
// Otherwise merge.
mergeWithBlock( current, next, range );
mergeWithBlock( current, next, range, root );
// If deleted line between containers, merge newly adjacent
// containers.
next = current.parentNode;
@ -4306,11 +4286,8 @@ proto.insertPlainText = function ( plainText, isPaste ) {
for ( i = 0, l = lines.length; i < l; i += 1 ) {
line = lines[i];
line = escapeHTMLFragement( line ).replace( / (?= )/g, '&nbsp;' );
// Wrap all but first/last lines in <div></div>
if ( i && i + 1 < l ) {
line = openBlock + ( line || '<BR>' ) + closeBlock;
}
lines[i] = line;
// Wrap each line in <div></div>
lines[i] = openBlock + ( line || '<BR>' ) + closeBlock;
}
return this.insertHTML( lines.join( '' ), isPaste );
};

File diff suppressed because one or more lines are too long

View file

@ -1852,11 +1852,8 @@ proto.insertPlainText = function ( plainText, isPaste ) {
for ( i = 0, l = lines.length; i < l; i += 1 ) {
line = lines[i];
line = escapeHTMLFragement( line ).replace( / (?= )/g, '&nbsp;' );
// Wrap all but first/last lines in <div></div>
if ( i && i + 1 < l ) {
line = openBlock + ( line || '<BR>' ) + closeBlock;
}
lines[i] = line;
// Wrap each line in <div></div>
lines[i] = openBlock + ( line || '<BR>' ) + closeBlock;
}
return this.insertHTML( lines.join( '' ), isPaste );
};

View file

@ -188,7 +188,7 @@ var keyHandlers = {
block = parent;
}
if ( !block.textContent ) {
if ( isEmptyBlock( block ) ) {
// Break list
if ( getNearest( block, root, 'UL' ) ||
getNearest( block, root, 'OL' ) ) {
@ -281,7 +281,7 @@ var keyHandlers = {
return;
}
// Otherwise merge.
mergeWithBlock( previous, current, range );
mergeWithBlock( previous, current, range, root );
// If deleted line between containers, merge newly adjacent
// containers.
current = previous.parentNode;
@ -348,7 +348,7 @@ var keyHandlers = {
return;
}
// Otherwise merge.
mergeWithBlock( current, next, range );
mergeWithBlock( current, next, range, root );
// If deleted line between containers, merge newly adjacent
// containers.
next = current.parentNode;

View file

@ -85,6 +85,10 @@ function getNextBlock ( node, root ) {
return node !== root ? node : null;
}
function isEmptyBlock ( block ) {
return !block.textContent && !block.querySelector( 'IMG' );
}
function areAlike ( node, node2 ) {
return !isLeaf( node ) && (
node.nodeType === node2.nodeType &&
@ -168,7 +172,7 @@ function getPath ( node, root ) {
function getLength ( node ) {
var nodeType = node.nodeType;
return nodeType === ELEMENT_NODE ?
return nodeType === ELEMENT_NODE || nodeType === DOCUMENT_FRAGMENT_NODE ?
node.childNodes.length : node.length || 0;
}
@ -471,11 +475,14 @@ function mergeInlines ( node, range ) {
}
}
function mergeWithBlock ( block, next, range ) {
var container = next,
last, offset;
while ( container.parentNode.childNodes.length === 1 ) {
container = container.parentNode;
function mergeWithBlock ( block, next, range, root ) {
var container = next;
var parent, last, offset;
while ( ( parent = container.parentNode ) &&
parent !== root &&
parent.nodeType === ELEMENT_NODE &&
parent.childNodes.length === 1 ) {
container = parent;
}
detach( container );

View file

@ -154,7 +154,7 @@ var deleteContentsOfRange = function ( range, root ) {
// endBlock will have been split, so need to refetch
endBlock = getEndBlockOfRange( range, root );
if ( startBlock && endBlock && startBlock !== endBlock ) {
mergeWithBlock( startBlock, endBlock, range );
mergeWithBlock( startBlock, endBlock, range, root );
}
}
@ -176,134 +176,107 @@ var deleteContentsOfRange = function ( range, root ) {
// ---
// Contents of range will be deleted.
// After method, range will be around inserted content
var insertTreeFragmentIntoRange = function ( range, frag, root ) {
// Check if it's all inline content
var allInline = true,
children = frag.childNodes,
l = children.length;
while ( l-- ) {
if ( !isInline( children[l] ) ) {
allInline = false;
break;
}
var node, block, blockContentsAfterSplit, stopPoint, container, offset;
var nodeAfterSplit, nodeBeforeSplit, tempRange;
// Fixup content: ensure no top-level inline, and add cursor fix elements.
fixContainer( frag, root );
node = frag;
while ( ( node = getNextBlock( node, root ) ) ) {
fixCursor( node, root );
}
// Delete any selected content
// Delete any selected content.
if ( !range.collapsed ) {
deleteContentsOfRange( range, root );
}
// Move range down into text nodes
// Move range down into text nodes.
moveRangeBoundariesDownTree( range );
range.collapse( false ); // collapse to end
if ( allInline ) {
// If inline, just insert at the current position.
insertNodeInRange( range, frag );
if ( range.startContainer !== range.endContainer ) {
mergeInlines( range.endContainer, range );
}
mergeInlines( range.startContainer, range );
range.collapse( false );
} else {
// Otherwise...
// 1. Split up to blockquote (if a parent) or root
var splitPoint = range.startContainer,
// Where will we split up to? First blockquote parent, otherwise root.
stopPoint = getNearest( range.endContainer, root, 'BLOCKQUOTE' ) || root;
// Merge the contents of the first block in the frag with the focused block.
// If there are contents in the block after the focus point, collect this
// up to insert in the last block later
block = getStartBlockOfRange( range );
if ( block ) {
moveRangeBoundariesUpTree( range, block, block, root );
range.collapse( true ); // collapse to start
container = range.endContainer;
offset = range.endOffset;
// Remove trailing <br>  we don't want this considered content to be
// inserted again later
cleanupBRs( block, root, false );
if ( isInline( container ) ) {
// Split up to block parent.
nodeAfterSplit = split(
splitPoint,
range.startOffset,
getNearest( splitPoint.parentNode, root, 'BLOCKQUOTE' ) || root,
root
),
nodeBeforeSplit = nodeAfterSplit.previousSibling,
startContainer = nodeBeforeSplit,
startOffset = startContainer.childNodes.length,
endContainer = nodeAfterSplit,
endOffset = 0,
parent = nodeAfterSplit.parentNode,
child, node, prev, next, startAnchor;
// 2. Move down into edge either side of split and insert any inline
// nodes at the beginning/end of the fragment
while ( ( child = startContainer.lastChild ) &&
child.nodeType === ELEMENT_NODE ) {
if ( child.nodeName === 'BR' ) {
startOffset -= 1;
break;
container, offset, getPreviousBlock( container, root ), root );
container = nodeAfterSplit.parentNode;
offset = indexOf.call( container.childNodes, nodeAfterSplit );
}
if ( /*isBlock( container ) && */offset !== getLength( container ) ) {
// Collect any inline contents of the block after the range point
blockContentsAfterSplit =
root.ownerDocument.createDocumentFragment();
while ( ( node = container.childNodes[ offset ] ) ) {
blockContentsAfterSplit.appendChild( node );
}
startContainer = child;
startOffset = startContainer.childNodes.length;
}
while ( ( child = endContainer.firstChild ) &&
child.nodeType === ELEMENT_NODE &&
child.nodeName !== 'BR' ) {
endContainer = child;
}
startAnchor = startContainer.childNodes[ startOffset ] || null;
while ( ( child = frag.firstChild ) && isInline( child ) ) {
startContainer.insertBefore( child, startAnchor );
}
while ( ( child = frag.lastChild ) && isInline( child ) ) {
endContainer.insertBefore( child, endContainer.firstChild );
endOffset += 1;
}
// And merge the first block in.
mergeWithBlock( container, getNextBlock( frag, frag ), range, root );
// 3. Fix cursor then insert block(s) in the fragment
node = frag;
while ( node = getNextBlock( node, root ) ) {
fixCursor( node, root );
}
parent.insertBefore( frag, nodeAfterSplit );
// And where we will insert
offset = indexOf.call( container.parentNode.childNodes, container ) + 1;
container = container.parentNode;
range.setEnd( container, offset );
}
// 4. Remove empty nodes created either side of split, then
// merge containers at the edges.
next = nodeBeforeSplit.nextSibling;
node = getPreviousBlock( next, root );
if ( node && !/\S/.test( node.textContent ) ) {
do {
parent = node.parentNode;
parent.removeChild( node );
node = parent;
} while ( node && !node.lastChild && node !== root );
}
if ( !nodeBeforeSplit.parentNode ) {
nodeBeforeSplit = next.previousSibling;
}
if ( !startContainer.parentNode ) {
startContainer = nodeBeforeSplit || next.parentNode;
startOffset = nodeBeforeSplit ?
nodeBeforeSplit.childNodes.length : 0;
}
// Merge inserted containers with edges of split
if ( isContainer( next ) ) {
mergeContainers( next, root );
// Is there still any content in the fragment?
if ( getLength( frag ) ) {
moveRangeBoundariesUpTree( range, stopPoint, stopPoint, root );
// Now split after block up to blockquote (if a parent) or root
nodeAfterSplit = split(
range.endContainer, range.endOffset, stopPoint, root );
nodeBeforeSplit = nodeAfterSplit ?
nodeAfterSplit.previousSibling :
stopPoint.lastChild;
stopPoint.insertBefore( frag, nodeAfterSplit );
if ( nodeAfterSplit ) {
range.setEndBefore( nodeAfterSplit );
} else {
range.setEnd( stopPoint, getLength( stopPoint ) );
}
block = getEndBlockOfRange( range, root );
// Get a reference that won't be invalidated if we merge containers.
moveRangeBoundariesDownTree( range );
container = range.endContainer;
offset = range.endOffset;
prev = nodeAfterSplit.previousSibling;
node = isBlock( nodeAfterSplit ) ?
nodeAfterSplit : getNextBlock( nodeAfterSplit, root );
if ( node && !/\S/.test( node.textContent ) ) {
do {
parent = node.parentNode;
parent.removeChild( node );
node = parent;
} while ( node && !node.lastChild && node !== root );
}
if ( !nodeAfterSplit.parentNode ) {
nodeAfterSplit = prev.nextSibling;
}
if ( !endOffset ) {
endContainer = prev;
endOffset = prev.childNodes.length;
}
// Merge inserted containers with edges of split
if ( nodeAfterSplit && isContainer( nodeAfterSplit ) ) {
mergeContainers( nodeAfterSplit, root );
}
range.setStart( startContainer, startOffset );
range.setEnd( endContainer, endOffset );
moveRangeBoundariesDownTree( range );
nodeAfterSplit = nodeBeforeSplit && nodeBeforeSplit.nextSibling;
if ( nodeAfterSplit && isContainer( nodeAfterSplit ) ) {
mergeContainers( nodeAfterSplit, root );
}
range.setEnd( container, offset );
}
// Insert inline content saved from before.
if ( blockContentsAfterSplit ) {
tempRange = range.cloneRange();
mergeWithBlock( block, blockContentsAfterSplit, tempRange, root );
range.setEnd( tempRange.endContainer, tempRange.endOffset );
}
moveRangeBoundariesDownTree( range );
};
// ---