0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 15:23:29 -05:00
Squire/source/Range.js
Neil Jenkins 597024eecb Remove FILTER_(ACCEPT|SKIP) constants.
Just return a boolean for the TreeWalker filter fn. This diverges from the spec,
but since the goal of this implementation is not to fully implement the spec
and we're never going to use a native implementation, this doesn't matter and
the code is easier to read when the function is just returning a boolean like
any normal filter function.
2014-09-04 17:24:31 +07:00

517 lines
16 KiB
JavaScript

/*global
ELEMENT_NODE,
TEXT_NODE,
SHOW_TEXT,
START_TO_START,
START_TO_END,
END_TO_END,
END_TO_START,
notWS,
indexOf,
TreeWalker,
isLeaf,
isInline,
isBlock,
getPreviousBlock,
getNextBlock,
getLength,
fixCursor,
split,
mergeWithBlock,
mergeContainers
*/
/*jshint strict: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 forEachTextNodeInRange = function ( range, fn ) {
range = range.cloneRange();
moveRangeBoundariesDownTree( range );
var startContainer = range.startContainer,
endContainer = range.endContainer,
root = range.commonAncestorContainer,
walker = new TreeWalker(
root, SHOW_TEXT, function (/* node */) {
return true;
}, false ),
textnode = walker.currentNode = startContainer;
while ( !fn( textnode, range ) &&
textnode !== endContainer &&
( textnode = walker.nextNode() ) ) {}
};
var getTextContentInRange = function ( range ) {
var textContent = '';
forEachTextNodeInRange( range, function ( textnode, range ) {
var value = textnode.data;
if ( value && ( /\S/.test( value ) ) ) {
if ( textnode === range.endContainer ) {
value = value.slice( 0, range.endOffset );
}
if ( textnode === range.startContainer ) {
value = value.slice( range.startOffset );
}
textContent += value;
}
});
return textContent;
};
// ---
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 ) {
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 ),
startNode = split( startContainer, startOffset, common ),
frag = common.ownerDocument.createDocumentFragment(),
next;
// End node will be null if at end of child nodes list.
while ( startNode !== endNode ) {
next = startNode.nextSibling;
frag.appendChild( startNode );
startNode = next;
}
range.setStart( common, endNode ?
indexOf.call( common.childNodes, endNode ) :
common.childNodes.length );
range.collapse( true );
fixCursor( common );
return frag;
};
var deleteContentsOfRange = function ( range ) {
// Move boundaries up as much as possible to reduce need to split.
moveRangeBoundariesUpTree( range );
// Remove selected range
extractContentsOfRange( range );
// If we split into two different blocks, merge the blocks.
var startBlock = getStartBlockOfRange( range ),
endBlock = getEndBlockOfRange( range );
if ( startBlock && endBlock && startBlock !== endBlock ) {
mergeWithBlock( startBlock, endBlock, range );
}
// Ensure block has necessary children
if ( startBlock ) {
fixCursor( startBlock );
}
// Ensure body has a block-level element in it.
var body = range.endContainer.ownerDocument.body,
child = body.firstChild;
if ( !child || child.nodeName === 'BR' ) {
fixCursor( body );
range.selectNodeContents( body.firstChild );
}
// Ensure valid range (must have only block or inline containers)
var isCollapsed = range.collapsed;
moveRangeBoundariesDownTree( range );
if ( isCollapsed ) {
// Collapse
range.collapse( true );
}
};
// ---
var insertTreeFragmentIntoRange = function ( range, frag ) {
// 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 );
}
// Move range down into text ndoes
moveRangeBoundariesDownTree( range );
// If inline, just insert at the current position.
if ( allInline ) {
insertNodeInRange( range, frag );
range.collapse( false );
}
// Otherwise, split up to body, insert inline before and after split
// and insert block in between split, then merge containers.
else {
var nodeAfterSplit = split( range.startContainer, range.startOffset,
range.startContainer.ownerDocument.body ),
nodeBeforeSplit = nodeAfterSplit.previousSibling,
startContainer = nodeBeforeSplit,
startOffset = startContainer.childNodes.length,
endContainer = nodeAfterSplit,
endOffset = 0,
parent = nodeAfterSplit.parentNode,
child, node;
while ( ( child = startContainer.lastChild ) &&
child.nodeType === ELEMENT_NODE &&
child.nodeName !== 'BR' ) {
startContainer = child;
startOffset = startContainer.childNodes.length;
}
while ( ( child = endContainer.firstChild ) &&
child.nodeType === ELEMENT_NODE &&
child.nodeName !== 'BR' ) {
endContainer = child;
}
while ( ( child = frag.firstChild ) && isInline( child ) ) {
startContainer.appendChild( child );
}
while ( ( child = frag.lastChild ) && isInline( child ) ) {
endContainer.insertBefore( child, endContainer.firstChild );
endOffset += 1;
}
// Fix cursor then insert block(s)
node = frag;
while ( node = getNextBlock( node ) ) {
fixCursor( node );
}
parent.insertBefore( frag, nodeAfterSplit );
// Remove empty nodes created by split and merge inserted containers
// with edges of split
node = nodeAfterSplit.previousSibling;
if ( !nodeAfterSplit.textContent ) {
parent.removeChild( nodeAfterSplit );
} else {
mergeContainers( nodeAfterSplit );
}
if ( !nodeAfterSplit.parentNode ) {
endContainer = node;
endOffset = getLength( endContainer );
}
if ( !nodeBeforeSplit.textContent) {
startContainer = nodeBeforeSplit.nextSibling;
startOffset = 0;
parent.removeChild( nodeBeforeSplit );
} else {
mergeContainers( nodeBeforeSplit );
}
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 ) {
var container = range.startContainer,
block;
// If inline, get the containing block.
if ( isInline( container ) ) {
block = getPreviousBlock( container );
} else if ( isBlock( container ) ) {
block = container;
} else {
block = getNodeBefore( container, range.startOffset );
block = getNextBlock( block );
}
// 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 ) {
var container = range.endContainer,
block, child;
// If inline, get the containing block.
if ( isInline( container ) ) {
block = getPreviousBlock( container );
} else if ( isBlock( container ) ) {
block = container;
} else {
block = getNodeAfter( container, range.endOffset );
if ( !block ) {
block = container.ownerDocument.body;
while ( child = block.lastChild ) {
block = child;
}
}
block = getPreviousBlock( block );
}
// 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 ) {
var startContainer = range.startContainer,
startOffset = range.startOffset;
// If in the middle or end of a text node, we're not at the boundary.
if ( startContainer.nodeType === TEXT_NODE ) {
if ( startOffset ) {
return false;
}
contentWalker.currentNode = startContainer;
} else {
contentWalker.currentNode = getNodeAfter( startContainer, startOffset );
}
// Otherwise, look for any previous content in the same block.
contentWalker.root = getStartBlockOfRange( range );
return !contentWalker.previousNode();
};
var rangeDoesEndAtBlockBoundary = function ( range ) {
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
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 );
return !contentWalker.nextNode();
};
var expandRangeToBlockBoundaries = function ( range ) {
var start = getStartBlockOfRange( range ),
end = getEndBlockOfRange( range ),
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 );
}
};