0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 15:23:29 -05:00
Squire/source/Range.js
2016-07-14 12:15:06 +10:00

535 lines
18 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;
// 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;
before.appendData( after.data );
detach( after );
}
range.setStart( startContainer, startOffset );
range.collapse( true );
fixCursor( common, root );
return frag;
};
var deleteContentsOfRange = function ( range, root ) {
// Move boundaries up as much as possible to reduce need to split.
// But we need to check whether we've moved the boundary outside of a
// block. If so, the entire block will be removed, so we shouldn't merge
// later.
moveRangeBoundariesUpTree( range );
var startBlock = range.startContainer,
endBlock = range.endContainer,
needsMerge = ( isInline( startBlock ) || isBlock( startBlock ) ) &&
( isInline( endBlock ) || isBlock( endBlock ) );
// Remove selected range
var frag = extractContentsOfRange( range, null, root );
// Move boundaries back down tree so that they are inside the blocks.
// If we don't do this, the range may be collapsed to a point between
// two blocks, so get(Start|End)BlockOfRange will return null.
moveRangeBoundariesDownTree( range );
// If we split into two different blocks, merge the blocks.
startBlock = getStartBlockOfRange( range, root );
if ( needsMerge ) {
endBlock = getEndBlockOfRange( range, root );
if ( startBlock && endBlock && startBlock !== endBlock ) {
mergeWithBlock( startBlock, endBlock, range );
}
}
// Ensure block has necessary children
if ( startBlock ) {
fixCursor( startBlock, root );
}
// Ensure root has a block-level element in it.
var child = root.firstChild;
if ( !child || child.nodeName === 'BR' ) {
fixCursor( root, root );
range.selectNodeContents( root.firstChild );
} else {
range.collapse( false );
}
return frag;
};
// ---
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;
}
}
// Delete any selected content
if ( !range.collapsed ) {
deleteContentsOfRange( range, root );
}
// Move range down into text nodes
moveRangeBoundariesDownTree( range );
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,
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;
}
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;
}
// 3. Fix cursor then insert block(s) in the fragment
node = frag;
while ( node = getNextBlock( node, root ) ) {
fixCursor( node, root );
}
parent.insertBefore( frag, nodeAfterSplit );
// 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 );
}
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 );
}
};
// ---
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,
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 ) ) {
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, common ) {
var startContainer = range.startContainer,
startOffset = range.startOffset,
endContainer = range.endContainer,
endOffset = range.endOffset,
parent;
if ( !common ) {
common = range.commonAncestorContainer;
}
while ( startContainer !== common && !startOffset ) {
parent = startContainer.parentNode;
startOffset = indexOf.call( parent.childNodes, startContainer );
startContainer = parent;
}
while ( endContainer !== common &&
endOffset === getLength( endContainer ) ) {
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 ( 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 ( 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 );
}
};