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

View file

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

View file

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

View file

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