mirror of
https://github.com/fastmail/Squire.git
synced 2024-12-22 15:23:29 -05:00
af1720282c
* Firefox was sometimes not finding the extensions on elements. * This minifies to a smaller target.
524 lines
16 KiB
JavaScript
524 lines
16 KiB
JavaScript
/*global
|
|
ELEMENT_NODE,
|
|
TEXT_NODE,
|
|
SHOW_TEXT,
|
|
FILTER_ACCEPT,
|
|
START_TO_START,
|
|
START_TO_END,
|
|
END_TO_END,
|
|
END_TO_START,
|
|
indexOf,
|
|
|
|
TreeWalker,
|
|
|
|
isLeaf,
|
|
isInline,
|
|
isBlock,
|
|
getPreviousBlock,
|
|
getNextBlock,
|
|
getLength,
|
|
fixCursor,
|
|
split,
|
|
mergeWithBlock,
|
|
mergeContainers,
|
|
|
|
Range
|
|
*/
|
|
/*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 RangePrototype = Range.prototype;
|
|
|
|
RangePrototype.forEachTextNode = function ( fn ) {
|
|
var range = this.cloneRange();
|
|
range.moveBoundariesDownTree();
|
|
|
|
var startContainer = range.startContainer,
|
|
endContainer = range.endContainer,
|
|
root = range.commonAncestorContainer,
|
|
walker = new TreeWalker(
|
|
root, SHOW_TEXT, function ( node ) {
|
|
return FILTER_ACCEPT;
|
|
}, false ),
|
|
textnode = walker.currentNode = startContainer;
|
|
|
|
while ( !fn( textnode, range ) &&
|
|
textnode !== endContainer &&
|
|
( textnode = walker.nextNode() ) ) {}
|
|
};
|
|
|
|
RangePrototype.getTextContent = function () {
|
|
var textContent = '';
|
|
this.forEachTextNode( 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;
|
|
};
|
|
|
|
// ---
|
|
|
|
RangePrototype._insertNode = function ( node ) {
|
|
// Insert at start.
|
|
var startContainer = this.startContainer,
|
|
startOffset = this.startOffset,
|
|
endContainer = this.endContainer,
|
|
endOffset = this.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 ( this.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;
|
|
}
|
|
|
|
this.setStart( startContainer, startOffset );
|
|
this.setEnd( endContainer, endOffset );
|
|
|
|
return this;
|
|
};
|
|
|
|
RangePrototype._extractContents = function ( common ) {
|
|
var startContainer = this.startContainer,
|
|
startOffset = this.startOffset,
|
|
endContainer = this.endContainer,
|
|
endOffset = this.endOffset;
|
|
|
|
if ( !common ) {
|
|
common = this.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;
|
|
}
|
|
|
|
this.setStart( common, endNode ?
|
|
indexOf.call( common.childNodes, endNode ) :
|
|
common.childNodes.length );
|
|
this.collapse( true );
|
|
|
|
fixCursor( common );
|
|
|
|
return frag;
|
|
};
|
|
|
|
RangePrototype._deleteContents = function () {
|
|
// Move boundaries up as much as possible to reduce need to split.
|
|
this.moveBoundariesUpTree();
|
|
|
|
// Remove selected range
|
|
this._extractContents();
|
|
|
|
// If we split into two different blocks, merge the blocks.
|
|
var startBlock = this.getStartBlock(),
|
|
endBlock = this.getEndBlock();
|
|
if ( startBlock && endBlock && startBlock !== endBlock ) {
|
|
mergeWithBlock( startBlock, endBlock, this );
|
|
}
|
|
|
|
// Ensure block has necessary children
|
|
if ( startBlock ) {
|
|
fixCursor( startBlock );
|
|
}
|
|
|
|
// Ensure body has a block-level element in it.
|
|
var body = this.endContainer.ownerDocument.body,
|
|
child = body.firstChild;
|
|
if ( !child || child.nodeName === 'BR' ) {
|
|
fixCursor( body );
|
|
this.selectNodeContents( body.firstChild );
|
|
}
|
|
|
|
// Ensure valid range (must have only block or inline containers)
|
|
var isCollapsed = this.collapsed;
|
|
this.moveBoundariesDownTree();
|
|
if ( isCollapsed ) {
|
|
// Collapse
|
|
this.collapse( true );
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
// ---
|
|
|
|
RangePrototype.insertTreeFragment = function ( 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 ( !this.collapsed ) {
|
|
this._deleteContents();
|
|
}
|
|
|
|
// Move range down into text ndoes
|
|
this.moveBoundariesDownTree();
|
|
|
|
// If inline, just insert at the current position.
|
|
if ( allInline ) {
|
|
this._insertNode( frag );
|
|
this.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( this.startContainer, this.startOffset,
|
|
this.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 );
|
|
}
|
|
|
|
this.setStart( startContainer, startOffset );
|
|
this.setEnd( endContainer, endOffset );
|
|
this.moveBoundariesDownTree();
|
|
}
|
|
};
|
|
|
|
// ---
|
|
|
|
RangePrototype.containsNode = function ( node, partial ) {
|
|
var range = this,
|
|
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 );
|
|
}
|
|
};
|
|
|
|
RangePrototype.moveBoundariesDownTree = function () {
|
|
var startContainer = this.startContainer,
|
|
startOffset = this.startOffset,
|
|
endContainer = this.endContainer,
|
|
endOffset = this.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 ( this.collapsed ) {
|
|
this.setStart( endContainer, endOffset );
|
|
this.setEnd( startContainer, startOffset );
|
|
} else {
|
|
this.setStart( startContainer, startOffset );
|
|
this.setEnd( endContainer, endOffset );
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
RangePrototype.moveBoundariesUpTree = function ( common ) {
|
|
var startContainer = this.startContainer,
|
|
startOffset = this.startOffset,
|
|
endContainer = this.endContainer,
|
|
endOffset = this.endOffset,
|
|
parent;
|
|
|
|
if ( !common ) {
|
|
common = this.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;
|
|
}
|
|
|
|
this.setStart( startContainer, startOffset );
|
|
this.setEnd( endContainer, endOffset );
|
|
|
|
return this;
|
|
};
|
|
|
|
// Returns the first block at least partially contained by the range,
|
|
// or null if no block is contained by the range.
|
|
RangePrototype.getStartBlock = function () {
|
|
var container = this.startContainer,
|
|
block;
|
|
|
|
// If inline, get the containing block.
|
|
if ( isInline( container ) ) {
|
|
block = getPreviousBlock( container );
|
|
} else if ( isBlock( container ) ) {
|
|
block = container;
|
|
} else {
|
|
block = getNodeBefore( container, this.startOffset );
|
|
block = getNextBlock( block );
|
|
}
|
|
// Check the block actually intersects the range
|
|
return block && this.containsNode( 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.
|
|
RangePrototype.getEndBlock = function () {
|
|
var container = this.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, this.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 && this.containsNode( block, true ) ? block : null;
|
|
};
|
|
|
|
RangePrototype.startsAtBlockBoundary = function () {
|
|
var startContainer = this.startContainer,
|
|
startOffset = this.startOffset,
|
|
parent, child;
|
|
|
|
while ( isInline( startContainer ) ) {
|
|
if ( startOffset ) {
|
|
return false;
|
|
}
|
|
parent = startContainer.parentNode;
|
|
startOffset = indexOf.call( parent.childNodes, startContainer );
|
|
startContainer = parent;
|
|
}
|
|
// Skip empty text nodes and <br>s.
|
|
while ( startOffset &&
|
|
( child = startContainer.childNodes[ startOffset - 1 ] ) &&
|
|
( child.data === '' || child.nodeName === 'BR' ) ) {
|
|
startOffset -= 1;
|
|
}
|
|
return !startOffset;
|
|
};
|
|
|
|
RangePrototype.endsAtBlockBoundary = function () {
|
|
var endContainer = this.endContainer,
|
|
endOffset = this.endOffset,
|
|
length = getLength( endContainer ),
|
|
parent, child;
|
|
|
|
while ( isInline( endContainer ) ) {
|
|
if ( endOffset !== length ) {
|
|
return false;
|
|
}
|
|
parent = endContainer.parentNode;
|
|
endOffset = indexOf.call( parent.childNodes, endContainer ) + 1;
|
|
endContainer = parent;
|
|
length = endContainer.childNodes.length;
|
|
}
|
|
// Skip empty text nodes and <br>s.
|
|
while ( endOffset < length &&
|
|
( child = endContainer.childNodes[ endOffset ] ) &&
|
|
( child.data === '' || child.nodeName === 'BR' ) ) {
|
|
endOffset += 1;
|
|
}
|
|
return endOffset === length;
|
|
};
|
|
|
|
RangePrototype.expandToBlockBoundaries = function () {
|
|
var start = this.getStartBlock(),
|
|
end = this.getEndBlock(),
|
|
parent;
|
|
|
|
if ( start && end ) {
|
|
parent = start.parentNode;
|
|
this.setStart( parent, indexOf.call( parent.childNodes, start ) );
|
|
parent = end.parentNode;
|
|
this.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 );
|
|
}
|
|
|
|
return this;
|
|
};
|