mirror of
https://github.com/fastmail/Squire.git
synced 2024-12-22 15:23:29 -05:00
c5a7c622fe
If you have a document like this: <div style="font-size:20px">XXXX</div> <div style="font-size:14px">YYYY</div> and you select the YYYY text and copy it, we just copy the text to the clipboard and not the block formatting. This is fine. Now you select XXXX and paste. Because that removes all content from the first block we were replacing it with the block formatting from the clipboard. But this has no block formatting, so you essentially just "lost" the font-size:20px, which broke user expectations. (If the copied text *did* have block formatting, then replacing the block is the correct thing to do in this case, which we still do.)
557 lines
19 KiB
JavaScript
557 lines
19 KiB
JavaScript
/*jshint strict:false, undef:false, unused:false, latedef:false */
|
||
|
||
var getNodeBefore = function ( node, offset ) {
|
||
var children = node.childNodes;
|
||
while ( offset && node.nodeType === ELEMENT_NODE ) {
|
||
node = children[ offset - 1 ];
|
||
children = node.childNodes;
|
||
offset = children.length;
|
||
}
|
||
return node;
|
||
};
|
||
|
||
var getNodeAfter = function ( node, offset ) {
|
||
if ( node.nodeType === ELEMENT_NODE ) {
|
||
var children = node.childNodes;
|
||
if ( offset < children.length ) {
|
||
node = children[ offset ];
|
||
} else {
|
||
while ( node && !node.nextSibling ) {
|
||
node = node.parentNode;
|
||
}
|
||
if ( node ) { node = node.nextSibling; }
|
||
}
|
||
}
|
||
return node;
|
||
};
|
||
|
||
// ---
|
||
|
||
var insertNodeInRange = function ( range, node ) {
|
||
// Insert at start.
|
||
var startContainer = range.startContainer,
|
||
startOffset = range.startOffset,
|
||
endContainer = range.endContainer,
|
||
endOffset = range.endOffset,
|
||
parent, children, childCount, afterSplit;
|
||
|
||
// If part way through a text node, split it.
|
||
if ( startContainer.nodeType === TEXT_NODE ) {
|
||
parent = startContainer.parentNode;
|
||
children = parent.childNodes;
|
||
if ( startOffset === startContainer.length ) {
|
||
startOffset = indexOf.call( children, startContainer ) + 1;
|
||
if ( range.collapsed ) {
|
||
endContainer = parent;
|
||
endOffset = startOffset;
|
||
}
|
||
} else {
|
||
if ( startOffset ) {
|
||
afterSplit = startContainer.splitText( startOffset );
|
||
if ( endContainer === startContainer ) {
|
||
endOffset -= startOffset;
|
||
endContainer = afterSplit;
|
||
}
|
||
else if ( endContainer === parent ) {
|
||
endOffset += 1;
|
||
}
|
||
startContainer = afterSplit;
|
||
}
|
||
startOffset = indexOf.call( children, startContainer );
|
||
}
|
||
startContainer = parent;
|
||
} else {
|
||
children = startContainer.childNodes;
|
||
}
|
||
|
||
childCount = children.length;
|
||
|
||
if ( startOffset === childCount ) {
|
||
startContainer.appendChild( node );
|
||
} else {
|
||
startContainer.insertBefore( node, children[ startOffset ] );
|
||
}
|
||
|
||
if ( startContainer === endContainer ) {
|
||
endOffset += children.length - childCount;
|
||
}
|
||
|
||
range.setStart( startContainer, startOffset );
|
||
range.setEnd( endContainer, endOffset );
|
||
};
|
||
|
||
var extractContentsOfRange = function ( range, common, root ) {
|
||
var startContainer = range.startContainer,
|
||
startOffset = range.startOffset,
|
||
endContainer = range.endContainer,
|
||
endOffset = range.endOffset;
|
||
|
||
if ( !common ) {
|
||
common = range.commonAncestorContainer;
|
||
}
|
||
|
||
if ( common.nodeType === TEXT_NODE ) {
|
||
common = common.parentNode;
|
||
}
|
||
|
||
var endNode = split( endContainer, endOffset, common, root ),
|
||
startNode = split( startContainer, startOffset, common, root ),
|
||
frag = common.ownerDocument.createDocumentFragment(),
|
||
next, before, after, beforeText, afterText;
|
||
|
||
// End node will be null if at end of child nodes list.
|
||
while ( startNode !== endNode ) {
|
||
next = startNode.nextSibling;
|
||
frag.appendChild( startNode );
|
||
startNode = next;
|
||
}
|
||
|
||
startContainer = common;
|
||
startOffset = endNode ?
|
||
indexOf.call( common.childNodes, endNode ) :
|
||
common.childNodes.length;
|
||
|
||
// Merge text nodes if adjacent. IE10 in particular will not focus
|
||
// between two text nodes
|
||
after = common.childNodes[ startOffset ];
|
||
before = after && after.previousSibling;
|
||
if ( before &&
|
||
before.nodeType === TEXT_NODE &&
|
||
after.nodeType === TEXT_NODE ) {
|
||
startContainer = before;
|
||
startOffset = before.length;
|
||
beforeText = before.data;
|
||
afterText = after.data;
|
||
|
||
// If we now have two adjacent spaces, the second one needs to become
|
||
// a nbsp, otherwise the browser will swallow it due to HTML whitespace
|
||
// collapsing.
|
||
if ( beforeText.charAt( beforeText.length - 1 ) === ' ' &&
|
||
afterText.charAt( 0 ) === ' ' ) {
|
||
afterText = ' ' + afterText.slice( 1 ); // nbsp
|
||
}
|
||
before.appendData( afterText );
|
||
detach( after );
|
||
}
|
||
|
||
range.setStart( startContainer, startOffset );
|
||
range.collapse( true );
|
||
|
||
fixCursor( common, root );
|
||
|
||
return frag;
|
||
};
|
||
|
||
var deleteContentsOfRange = function ( range, root ) {
|
||
var startBlock = getStartBlockOfRange( range, root );
|
||
var endBlock = getEndBlockOfRange( range, root );
|
||
var needsMerge = ( startBlock !== endBlock );
|
||
var frag, child;
|
||
|
||
// Move boundaries up as much as possible without exiting block,
|
||
// to reduce need to split.
|
||
moveRangeBoundariesDownTree( range );
|
||
moveRangeBoundariesUpTree( range, startBlock, endBlock, root );
|
||
|
||
// Remove selected range
|
||
frag = extractContentsOfRange( range, null, root );
|
||
|
||
// Move boundaries back down tree as far as possible.
|
||
moveRangeBoundariesDownTree( range );
|
||
|
||
// If we split into two different blocks, merge the blocks.
|
||
if ( needsMerge ) {
|
||
// endBlock will have been split, so need to refetch
|
||
endBlock = getEndBlockOfRange( range, root );
|
||
if ( startBlock && endBlock && startBlock !== endBlock ) {
|
||
mergeWithBlock( startBlock, endBlock, range, root );
|
||
}
|
||
}
|
||
|
||
// Ensure block has necessary children
|
||
if ( startBlock ) {
|
||
fixCursor( startBlock, root );
|
||
}
|
||
|
||
// Ensure root has a block-level element in it.
|
||
child = root.firstChild;
|
||
if ( !child || child.nodeName === 'BR' ) {
|
||
fixCursor( root, root );
|
||
range.selectNodeContents( root.firstChild );
|
||
} else {
|
||
range.collapse( true );
|
||
}
|
||
return frag;
|
||
};
|
||
|
||
// ---
|
||
|
||
// Contents of range will be deleted.
|
||
// After method, range will be around inserted content
|
||
var insertTreeFragmentIntoRange = function ( range, frag, root ) {
|
||
var firstInFragIsInline = frag.firstChild && isInline( frag.firstChild );
|
||
var node, block, blockContentsAfterSplit, stopPoint, container, offset;
|
||
var replaceBlock, firstBlockInFrag, nodeAfterSplit, nodeBeforeSplit;
|
||
var 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.
|
||
if ( !range.collapsed ) {
|
||
deleteContentsOfRange( range, root );
|
||
}
|
||
|
||
// Move range down into text nodes.
|
||
moveRangeBoundariesDownTree( range );
|
||
range.collapse( false ); // collapse to end
|
||
|
||
// 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. This preserves the style that was
|
||
// present in this bit of the page.
|
||
//
|
||
// If the block being inserted into is empty though, replace it instead of
|
||
// merging if the fragment had block contents.
|
||
// e.g. <blockquote><p>Foo</p></blockquote>
|
||
// This seems a reasonable approximation of user intent.
|
||
|
||
block = getStartBlockOfRange( range, root );
|
||
firstBlockInFrag = getNextBlock( frag, frag );
|
||
replaceBlock = !firstInFragIsInline && !!block && isEmptyBlock( block );
|
||
if ( block && firstBlockInFrag && !replaceBlock &&
|
||
// Don't merge table cells or PRE elements into block
|
||
!getNearest( firstBlockInFrag, frag, 'PRE' ) &&
|
||
!getNearest( firstBlockInFrag, frag, 'TABLE' ) ) {
|
||
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(
|
||
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 );
|
||
}
|
||
}
|
||
// And merge the first block in.
|
||
mergeWithBlock( container, firstBlockInFrag, range, root );
|
||
|
||
// And where we will insert
|
||
offset = indexOf.call( container.parentNode.childNodes, container ) + 1;
|
||
container = container.parentNode;
|
||
range.setEnd( container, offset );
|
||
}
|
||
|
||
// Is there still any content in the fragment?
|
||
if ( getLength( frag ) ) {
|
||
if ( replaceBlock ) {
|
||
range.setEndBefore( block );
|
||
range.collapse( false );
|
||
detach( block );
|
||
}
|
||
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;
|
||
|
||
// Merge inserted containers with edges of split
|
||
if ( nodeAfterSplit && isContainer( nodeAfterSplit ) ) {
|
||
mergeContainers( nodeAfterSplit, root );
|
||
}
|
||
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 );
|
||
};
|
||
|
||
// ---
|
||
|
||
var isNodeContainedInRange = function ( range, node, partial ) {
|
||
var nodeRange = node.ownerDocument.createRange();
|
||
|
||
nodeRange.selectNode( node );
|
||
|
||
if ( partial ) {
|
||
// Node must not finish before range starts or start after range
|
||
// finishes.
|
||
var nodeEndBeforeStart = ( range.compareBoundaryPoints(
|
||
END_TO_START, nodeRange ) > -1 ),
|
||
nodeStartAfterEnd = ( range.compareBoundaryPoints(
|
||
START_TO_END, nodeRange ) < 1 );
|
||
return ( !nodeEndBeforeStart && !nodeStartAfterEnd );
|
||
}
|
||
else {
|
||
// Node must start after range starts and finish before range
|
||
// finishes
|
||
var nodeStartAfterStart = ( range.compareBoundaryPoints(
|
||
START_TO_START, nodeRange ) < 1 ),
|
||
nodeEndBeforeEnd = ( range.compareBoundaryPoints(
|
||
END_TO_END, nodeRange ) > -1 );
|
||
return ( nodeStartAfterStart && nodeEndBeforeEnd );
|
||
}
|
||
};
|
||
|
||
var moveRangeBoundariesDownTree = function ( range ) {
|
||
var startContainer = range.startContainer,
|
||
startOffset = range.startOffset,
|
||
endContainer = range.endContainer,
|
||
endOffset = range.endOffset,
|
||
maySkipBR = true,
|
||
child;
|
||
|
||
while ( startContainer.nodeType !== TEXT_NODE ) {
|
||
child = startContainer.childNodes[ startOffset ];
|
||
if ( !child || isLeaf( child ) ) {
|
||
break;
|
||
}
|
||
startContainer = child;
|
||
startOffset = 0;
|
||
}
|
||
if ( endOffset ) {
|
||
while ( endContainer.nodeType !== TEXT_NODE ) {
|
||
child = endContainer.childNodes[ endOffset - 1 ];
|
||
if ( !child || isLeaf( child ) ) {
|
||
if ( maySkipBR && child && child.nodeName === 'BR' ) {
|
||
endOffset -= 1;
|
||
maySkipBR = false;
|
||
continue;
|
||
}
|
||
break;
|
||
}
|
||
endContainer = child;
|
||
endOffset = getLength( endContainer );
|
||
}
|
||
} else {
|
||
while ( endContainer.nodeType !== TEXT_NODE ) {
|
||
child = endContainer.firstChild;
|
||
if ( !child || isLeaf( child ) ) {
|
||
break;
|
||
}
|
||
endContainer = child;
|
||
}
|
||
}
|
||
|
||
// If collapsed, this algorithm finds the nearest text node positions
|
||
// *outside* the range rather than inside, but also it flips which is
|
||
// assigned to which.
|
||
if ( range.collapsed ) {
|
||
range.setStart( endContainer, endOffset );
|
||
range.setEnd( startContainer, startOffset );
|
||
} else {
|
||
range.setStart( startContainer, startOffset );
|
||
range.setEnd( endContainer, endOffset );
|
||
}
|
||
};
|
||
|
||
var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
|
||
var startContainer = range.startContainer;
|
||
var startOffset = range.startOffset;
|
||
var endContainer = range.endContainer;
|
||
var endOffset = range.endOffset;
|
||
var maySkipBR = true;
|
||
var parent;
|
||
|
||
if ( !startMax ) {
|
||
startMax = range.commonAncestorContainer;
|
||
}
|
||
if ( !endMax ) {
|
||
endMax = startMax;
|
||
}
|
||
|
||
while ( !startOffset &&
|
||
startContainer !== startMax &&
|
||
startContainer !== root ) {
|
||
parent = startContainer.parentNode;
|
||
startOffset = indexOf.call( parent.childNodes, startContainer );
|
||
startContainer = parent;
|
||
}
|
||
|
||
while ( true ) {
|
||
if ( maySkipBR &&
|
||
endContainer.nodeType !== TEXT_NODE &&
|
||
endContainer.childNodes[ endOffset ] &&
|
||
endContainer.childNodes[ endOffset ].nodeName === 'BR' ) {
|
||
endOffset += 1;
|
||
maySkipBR = false;
|
||
}
|
||
if ( endContainer === endMax ||
|
||
endContainer === root ||
|
||
endOffset !== getLength( endContainer ) ) {
|
||
break;
|
||
}
|
||
parent = endContainer.parentNode;
|
||
endOffset = indexOf.call( parent.childNodes, endContainer ) + 1;
|
||
endContainer = parent;
|
||
}
|
||
|
||
range.setStart( startContainer, startOffset );
|
||
range.setEnd( endContainer, endOffset );
|
||
};
|
||
|
||
// Returns the first block at least partially contained by the range,
|
||
// or null if no block is contained by the range.
|
||
var getStartBlockOfRange = function ( range, root ) {
|
||
var container = range.startContainer,
|
||
block;
|
||
|
||
// If inline, get the containing block.
|
||
if ( isInline( container ) ) {
|
||
block = getPreviousBlock( container, root );
|
||
} else if ( container !== root && isBlock( container ) ) {
|
||
block = container;
|
||
} else {
|
||
block = getNodeBefore( container, range.startOffset );
|
||
block = getNextBlock( block, root );
|
||
}
|
||
// Check the block actually intersects the range
|
||
return block && isNodeContainedInRange( range, block, true ) ? block : null;
|
||
};
|
||
|
||
// Returns the last block at least partially contained by the range,
|
||
// or null if no block is contained by the range.
|
||
var getEndBlockOfRange = function ( range, root ) {
|
||
var container = range.endContainer,
|
||
block, child;
|
||
|
||
// If inline, get the containing block.
|
||
if ( isInline( container ) ) {
|
||
block = getPreviousBlock( container, root );
|
||
} else if ( container !== root && isBlock( container ) ) {
|
||
block = container;
|
||
} else {
|
||
block = getNodeAfter( container, range.endOffset );
|
||
if ( !block || !isOrContains( root, block ) ) {
|
||
block = root;
|
||
while ( child = block.lastChild ) {
|
||
block = child;
|
||
}
|
||
}
|
||
block = getPreviousBlock( block, root );
|
||
}
|
||
// Check the block actually intersects the range
|
||
return block && isNodeContainedInRange( range, block, true ) ? block : null;
|
||
};
|
||
|
||
var contentWalker = new TreeWalker( null,
|
||
SHOW_TEXT|SHOW_ELEMENT,
|
||
function ( node ) {
|
||
return node.nodeType === TEXT_NODE ?
|
||
notWS.test( node.data ) :
|
||
node.nodeName === 'IMG';
|
||
}
|
||
);
|
||
|
||
var rangeDoesStartAtBlockBoundary = function ( range, root ) {
|
||
var startContainer = range.startContainer;
|
||
var startOffset = range.startOffset;
|
||
var nodeAfterCursor;
|
||
|
||
// If in the middle or end of a text node, we're not at the boundary.
|
||
contentWalker.root = null;
|
||
if ( startContainer.nodeType === TEXT_NODE ) {
|
||
if ( startOffset ) {
|
||
return false;
|
||
}
|
||
nodeAfterCursor = startContainer;
|
||
} else {
|
||
nodeAfterCursor = getNodeAfter( startContainer, startOffset );
|
||
if ( nodeAfterCursor && !isOrContains( root, nodeAfterCursor ) ) {
|
||
nodeAfterCursor = null;
|
||
}
|
||
// The cursor was right at the end of the document
|
||
if ( !nodeAfterCursor ) {
|
||
nodeAfterCursor = getNodeBefore( startContainer, startOffset );
|
||
if ( nodeAfterCursor.nodeType === TEXT_NODE &&
|
||
nodeAfterCursor.length ) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Otherwise, look for any previous content in the same block.
|
||
contentWalker.currentNode = nodeAfterCursor;
|
||
contentWalker.root = getStartBlockOfRange( range, root );
|
||
|
||
return !contentWalker.previousNode();
|
||
};
|
||
|
||
var rangeDoesEndAtBlockBoundary = function ( range, root ) {
|
||
var endContainer = range.endContainer,
|
||
endOffset = range.endOffset,
|
||
length;
|
||
|
||
// If in a text node with content, and not at the end, we're not
|
||
// at the boundary
|
||
contentWalker.root = null;
|
||
if ( endContainer.nodeType === TEXT_NODE ) {
|
||
length = endContainer.data.length;
|
||
if ( length && endOffset < length ) {
|
||
return false;
|
||
}
|
||
contentWalker.currentNode = endContainer;
|
||
} else {
|
||
contentWalker.currentNode = getNodeBefore( endContainer, endOffset );
|
||
}
|
||
|
||
// Otherwise, look for any further content in the same block.
|
||
contentWalker.root = getEndBlockOfRange( range, root );
|
||
|
||
return !contentWalker.nextNode();
|
||
};
|
||
|
||
var expandRangeToBlockBoundaries = function ( range, root ) {
|
||
var start = getStartBlockOfRange( range, root ),
|
||
end = getEndBlockOfRange( range, root ),
|
||
parent;
|
||
|
||
if ( start && end ) {
|
||
parent = start.parentNode;
|
||
range.setStart( parent, indexOf.call( parent.childNodes, start ) );
|
||
parent = end.parentNode;
|
||
range.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 );
|
||
}
|
||
};
|