0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2025-01-03 05:00:13 -05:00

Make Squire work without an iframe(!)

This commit is contained in:
Neil Jenkins 2016-03-22 17:57:00 +11:00
parent e133f26db1
commit 6a348e084b
10 changed files with 643 additions and 665 deletions

124
Demo.html
View file

@ -23,10 +23,41 @@
p {
margin: 5px 0;
}
iframe {
#editor {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
min-height: 200px;
border: 1px solid #888;
width: 100%;
height: 500px;
padding: 1em;
background: transparent;
color: #2b2b2b;
font: 13px/1.35 Helvetica, arial, sans-serif;
cursor: text;
}
a {
text-decoration: underline;
}
h2 {
font-size: 123.1%;
}
h3 {
font-size: 108%;
}
h1,h2,h3,p {
margin: 1em 0;
}
h4,h5,h6 {
margin: 0;
}
ul, ol {
margin: 0 1em;
padding: 0 1em;
}
blockquote {
border-left: 2px solid blue;
margin: 0;
padding: 0 10px;
}
</style>
</head>
@ -69,84 +100,19 @@
<span id="redo">Redo</span>
</p>
</header>
<script type="text/template" id="editorStyles">
html {
height: 100%;
}
body {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
height: 100%;
padding: 1em;
background: transparent;
color: #2b2b2b;
font: 13px/1.35 Helvetica, arial, sans-serif;
cursor: text;
}
a {
text-decoration: underline;
}
h1 {
font-size: 138.5%;
}
h2 {
font-size: 123.1%;
}
h3 {
font-size: 108%;
}
h1,h2,h3,p {
margin: 1em 0;
}
h4,h5,h6 {
margin: 0;
}
ul, ol {
margin: 0 1em;
padding: 0 1em;
}
blockquote {
border-left: 2px solid blue;
margin: 0;
padding: 0 10px;
}
</script>
<script type="text/javascript" src="build/squire-raw.js"></script>
<div id="editor"></div>
<script type="text/javascript" charset="utf-8">
var editor;
var iframe = document.createElement( 'iframe' );
iframe.addEventListener( 'load', function () {
// Make sure we're in standards mode.
var doc = iframe.contentDocument;
if ( doc.compatMode !== 'CSS1Compat' ) {
doc.open();
doc.write( '<!DOCTYPE html><title></title>' );
doc.close();
}
// doc.close() can cause a re-entrant load event in some browsers,
// such as IE9.
if ( editor ) {
return;
}
// Create Squire instance
editor = new Squire( doc, {
blockTag: 'p',
blockAttributes: {'class': 'paragraph'},
tagAttributes: {
ul: {'class': 'UL'},
ol: {'class': 'OL'},
li: {'class': 'listItem'}
}
});
// Add styles to frame
var style = doc.createElement( 'style' );
style.type = 'text/css';
style.textContent = document.getElementById( 'editorStyles' ).textContent;
doc.querySelector( 'head' ).appendChild( style );
}, false );
document.body.appendChild( iframe );
var div = document.getElementById( 'editor' );
var editor = new Squire( div, {
blockTag: 'p',
blockAttributes: {'class': 'paragraph'},
tagAttributes: {
ul: {'class': 'UL'},
ol: {'class': 'OL'},
li: {'class': 'listItem'}
}
});
document.addEventListener( 'click', function ( e ) {
var id = e.target.id,

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -272,8 +272,8 @@ var cleanTree = function cleanTree ( node ) {
// ---
var removeEmptyInlines = function removeEmptyInlines ( root ) {
var children = root.childNodes,
var removeEmptyInlines = function removeEmptyInlines ( node ) {
var children = node.childNodes,
l = children.length,
child;
while ( l-- ) {
@ -281,10 +281,10 @@ var removeEmptyInlines = function removeEmptyInlines ( root ) {
if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) {
removeEmptyInlines( child );
if ( isInline( child ) && !child.firstChild ) {
root.removeChild( child );
node.removeChild( child );
}
} else if ( child.nodeType === TEXT_NODE && !child.data ) {
root.removeChild( child );
node.removeChild( child );
}
}
};
@ -314,8 +314,8 @@ var isLineBreak = function ( br ) {
// line breaks by wrapping the inline text in a <div>. Browsers that want <br>
// elements at the end of each block will then have them added back in a later
// fixCursor method call.
var cleanupBRs = function ( root ) {
var brs = root.querySelectorAll( 'BR' ),
var cleanupBRs = function ( node, root ) {
var brs = node.querySelectorAll( 'BR' ),
brBreaksLine = [],
l = brs.length,
i, br, parent;
@ -340,7 +340,7 @@ var cleanupBRs = function ( root ) {
if ( !brBreaksLine[l] ) {
detach( br );
} else if ( !isInline( parent ) ) {
fixContainer( parent );
fixContainer( parent, root );
}
}
};

View file

@ -4,7 +4,7 @@ var onCut = function ( event ) {
var clipboardData = event.clipboardData;
var range = this.getSelection();
var node = this.createElement( 'div' );
var body = this._body;
var root = this._root;
var self = this;
// Save undo checkpoint
@ -12,8 +12,8 @@ var onCut = function ( event ) {
// Edge only seems to support setting plain text as of 2016-03-11.
if ( !isEdge && clipboardData ) {
moveRangeBoundariesUpTree( range, body );
node.appendChild( deleteContentsOfRange( range, body ) );
moveRangeBoundariesUpTree( range, root );
node.appendChild( deleteContentsOfRange( range, root ) );
clipboardData.setData( 'text/html', node.innerHTML );
clipboardData.setData( 'text/plain',
node.innerText || node.textContent );
@ -21,7 +21,7 @@ var onCut = function ( event ) {
} else {
setTimeout( function () {
try {
// If all content removed, ensure div at start of body.
// If all content removed, ensure div at start of root.
self._ensureBottomLine();
} catch ( error ) {
self.didError( error );
@ -141,21 +141,18 @@ var onPaste = function ( event ) {
this._awaitingPaste = true;
var body = this._body,
var body = this._doc.body,
range = this.getSelection(),
startContainer = range.startContainer,
startOffset = range.startOffset,
endContainer = range.endContainer,
endOffset = range.endOffset,
startBlock = getStartBlockOfRange( range );
endOffset = range.endOffset;
// We need to position the pasteArea in the visible portion of the screen
// to stop the browser auto-scrolling.
var pasteArea = this.createElement( 'DIV', {
style: 'position: absolute; overflow: hidden; top:' +
( body.scrollTop +
( startBlock ? startBlock.getBoundingClientRect().top : 0 ) ) +
'px; right: 150%; width: 1px; height: 1px;'
contenteditable: 'true',
style: 'position:fixed; overflow:hidden; top:0; right:100%; width:1px; height:1px;'
});
body.appendChild( pasteArea );
range.selectNodeContents( pasteArea );

View file

@ -3,6 +3,7 @@
var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING
var ELEMENT_NODE = 1; // Node.ELEMENT_NODE;
var TEXT_NODE = 3; // Node.TEXT_NODE;
var DOCUMENT_NODE = 9; // Node.DOCUMENT_NODE;
var DOCUMENT_FRAGMENT_NODE = 11; // Node.DOCUMENT_FRAGMENT_NODE;
var SHOW_ELEMENT = 1; // NodeFilter.SHOW_ELEMENT;
var SHOW_TEXT = 4; // NodeFilter.SHOW_TEXT;

View file

@ -28,14 +28,17 @@ function mergeObjects ( base, extras ) {
return base;
}
function Squire ( doc, config ) {
function Squire ( root, config ) {
if ( root.nodeType === DOCUMENT_NODE ) {
root = root.body;
}
var doc = root.ownerDocument;
var win = doc.defaultView;
var body = doc.body;
var mutation;
this._win = win;
this._doc = doc;
this._body = body;
this._root = root;
this._events = {};
@ -56,9 +59,6 @@ function Squire ( doc, config ) {
this.addEventListener( 'keyup', this._updatePathOnEvent );
this.addEventListener( 'mouseup', this._updatePathOnEvent );
win.addEventListener( 'focus', this, false );
win.addEventListener( 'blur', this, false );
this._undoIndex = -1;
this._undoStack = [];
this._undoStackLength = 0;
@ -67,7 +67,7 @@ function Squire ( doc, config ) {
if ( canObserveMutations ) {
mutation = new MutationObserver( this._docWasChanged.bind( this ) );
mutation.observe( body, {
mutation.observe( root, {
childList: true,
attributes: true,
characterData: true,
@ -123,7 +123,7 @@ function Squire ( doc, config ) {
};
}
body.setAttribute( 'contenteditable', 'true' );
root.setAttribute( 'contenteditable', 'true' );
// Remove Firefox's built-in controls
try {
@ -167,7 +167,8 @@ proto.createElement = function ( tag, props, children ) {
proto.createDefaultBlock = function ( children ) {
var config = this._config;
return fixCursor(
this.createElement( config.blockTag, config.blockAttributes, children )
this.createElement( config.blockTag, config.blockAttributes, children ),
this._root
);
};
@ -185,8 +186,8 @@ proto.getDocument = function () {
// document node, since these events are fired in a custom manner by the
// editor code.
var customEvents = {
focus: 1, blur: 1,
pathChange: 1, select: 1, input: 1, undoStateChange: 1
pathChange: 1, select: 1, input: 1,
undoStateChange: 1, scrollPointIntoView: 1
};
proto.fireEvent = function ( type, event ) {
@ -220,15 +221,12 @@ proto.fireEvent = function ( type, event ) {
};
proto.destroy = function () {
var win = this._win,
doc = this._doc,
var root = this._root,
events = this._events,
type;
win.removeEventListener( 'focus', this, false );
win.removeEventListener( 'blur', this, false );
for ( type in events ) {
if ( !customEvents[ type ] ) {
doc.removeEventListener( type, this, true );
root.removeEventListener( type, this, true );
}
}
if ( this._mutation ) {
@ -258,7 +256,7 @@ proto.addEventListener = function ( type, fn ) {
if ( !handlers ) {
handlers = this._events[ type ] = [];
if ( !customEvents[ type ] ) {
this._doc.addEventListener( type, this, true );
this._root.addEventListener( type, this, true );
}
}
handlers.push( fn );
@ -278,7 +276,7 @@ proto.removeEventListener = function ( type, fn ) {
if ( !handlers.length ) {
delete this._events[ type ];
if ( !customEvents[ type ] ) {
this._doc.removeEventListener( type, this, false );
this._root.removeEventListener( type, this, true );
}
}
}
@ -315,26 +313,18 @@ proto.scrollRangeIntoView = function ( range ) {
parent.removeChild( node );
parent.normalize();
}
if ( !rect ) {
return;
}
// Then check and scroll
var win = this._win;
var height = win.innerHeight;
var top = rect.top;
if ( top > height ) {
win.scrollBy( 0, top - height + 20 );
}
// And fire event for integrations to use
this.fireEvent( 'scrollPointIntoView', {
x: rect.left,
y: top
});
if ( rect ) {
this.fireEvent( 'scrollPointIntoView', {
x: rect.left,
y: rect.top
});
}
};
proto._moveCursorTo = function ( toStart ) {
var body = this._body,
range = this._createRange( body, toStart ? 0 : body.childNodes.length );
var root = this._root,
range = this._createRange( root, toStart ? 0 : root.childNodes.length );
moveRangeBoundariesDownTree( range );
this.setSelection( range );
return this;
@ -370,8 +360,9 @@ proto.setSelection = function ( range ) {
};
proto.getSelection = function () {
var sel = getWindowSelection( this ),
selection, startContainer, endContainer;
var sel = getWindowSelection( this );
var root = this._root;
var selection, startContainer, endContainer;
if ( sel && sel.rangeCount ) {
selection = sel.getRangeAt( 0 ).cloneRange();
startContainer = selection.startContainer;
@ -383,12 +374,15 @@ proto.getSelection = function () {
if ( endContainer && isLeaf( endContainer ) ) {
selection.setEndBefore( endContainer );
}
}
if ( selection &&
isOrContains( root, selection.commonAncestorContainer ) ) {
this._lastSelection = selection;
} else {
selection = this._lastSelection;
}
if ( !selection ) {
selection = this._createRange( this._body.firstChild, 0 );
selection = this._createRange( root.firstChild, 0 );
}
return selection;
};
@ -474,7 +468,7 @@ proto._removeZWS = function () {
if ( !this._hasZWS ) {
return;
}
removeZWS( this._body );
removeZWS( this._root );
this._hasZWS = false;
};
@ -489,7 +483,7 @@ proto._updatePath = function ( range, force ) {
this._lastAnchorNode = anchor;
this._lastFocusNode = focus;
newPath = ( anchor && focus ) ? ( anchor === focus ) ?
getPath( focus ) : '(selection)' : '';
getPath( focus, this._root ) : '(selection)' : '';
if ( this._path !== newPath ) {
this._path = newPath;
this.fireEvent( 'pathChange', { path: newPath } );
@ -507,26 +501,12 @@ proto._updatePathOnEvent = function () {
// --- Focus ---
proto.focus = function () {
// FF seems to need the body to be focussed (at least on first load).
// Chrome also now needs body to be focussed in order to show the cursor
// (otherwise it is focussed, but the cursor doesn't appear).
// Opera (Presto-variant) however will lose the selection if you call this!
if ( !isPresto ) {
this._body.focus();
}
this._win.focus();
this._root.focus();
return this;
};
proto.blur = function () {
// IE will remove the whole browser window from focus if you call
// win.blur() or body.blur(), so instead we call top.focus() to focus
// the top frame, thus blurring this frame. This works in everything
// except FF, so we need to call body.blur() in that as well.
if ( isGecko ) {
this._body.blur();
}
top.focus();
this._root.blur();
return this;
};
@ -565,9 +545,9 @@ proto._saveRangeToBookmark = function ( range ) {
};
proto._getRangeAndRemoveBookmark = function ( range ) {
var doc = this._doc,
start = doc.getElementById( startSelectionId ),
end = doc.getElementById( endSelectionId );
var root = this._root,
start = root.querySelector( '#' + startSelectionId ),
end = root.querySelector( '#' + endSelectionId );
if ( start && end ) {
var startContainer = start.parentNode,
@ -595,7 +575,7 @@ proto._getRangeAndRemoveBookmark = function ( range ) {
}
if ( !range ) {
range = doc.createRange();
range = this._doc.createRange();
}
range.setStart( _range.startContainer, _range.startOffset );
range.setEnd( _range.endContainer, _range.endOffset );
@ -744,27 +724,28 @@ proto.hasFormat = function ( tag, attributes, range ) {
// If the common ancestor is inside the tag we require, we definitely
// have the format.
var root = range.commonAncestorContainer,
walker, node;
if ( getNearest( root, tag, attributes ) ) {
var root = this._root;
var common = range.commonAncestorContainer;
var walker, node;
if ( getNearest( common, root, tag, attributes ) ) {
return true;
}
// If common ancestor is a text node and doesn't have the format, we
// definitely don't have it.
if ( root.nodeType === TEXT_NODE ) {
if ( common.nodeType === TEXT_NODE ) {
return false;
}
// Otherwise, check each text node at least partially contained within
// the selection and make sure all of them have the format we want.
walker = new TreeWalker( root, SHOW_TEXT, function ( node ) {
walker = new TreeWalker( common, SHOW_TEXT, function ( node ) {
return isNodeContainedInRange( range, node, true );
}, false );
var seenNode = false;
while ( node = walker.nextNode() ) {
if ( !getNearest( node, tag, attributes ) ) {
if ( !getNearest( node, root, tag, attributes ) ) {
return false;
}
seenNode = true;
@ -823,11 +804,12 @@ proto.getFontInfo = function ( range ) {
proto._addFormat = function ( tag, attributes, range ) {
// If the range is collapsed we simply insert the node by wrapping
// it round the range and focus it.
var root = this._root;
var el, walker, startContainer, endContainer, startOffset, endOffset,
node, needsFormat;
if ( range.collapsed ) {
el = fixCursor( this.createElement( tag, attributes ) );
el = fixCursor( this.createElement( tag, attributes ), root );
insertNodeInRange( range, el );
range.setStart( el.firstChild, el.firstChild.length );
range.collapse( true );
@ -880,7 +862,7 @@ proto._addFormat = function ( tag, attributes, range ) {
do {
node = walker.currentNode;
needsFormat = !getNearest( node, tag, attributes );
needsFormat = !getNearest( node, root, tag, attributes );
if ( needsFormat ) {
// <br> can never be a container node, so must have a text node
// if node == (end|start)Container
@ -1078,7 +1060,7 @@ var tagAfterSplit = {
var splitBlock = function ( self, block, node, offset ) {
var splitTag = tagAfterSplit[ block.nodeName ],
splitProperties = null,
nodeAfterSplit = split( node, offset, block.parentNode ),
nodeAfterSplit = split( node, offset, block.parentNode, self._root ),
config = self._config;
if ( !splitTag ) {
@ -1110,12 +1092,13 @@ proto.forEachBlock = function ( fn, mutates, range ) {
this.saveUndoState( range );
}
var start = getStartBlockOfRange( range ),
end = getEndBlockOfRange( range );
var root = this._root;
var start = getStartBlockOfRange( range, root );
var end = getEndBlockOfRange( range, root );
if ( start && end ) {
do {
if ( fn( start ) || start === end ) { break; }
} while ( start = getNextBlock( start ) );
} while ( start = getNextBlock( start, root ) );
}
if ( mutates ) {
@ -1144,23 +1127,24 @@ proto.modifyBlocks = function ( modify, range ) {
this._recordUndoState( range );
}
var root = this._root;
var frag;
// 2. Expand range to block boundaries
expandRangeToBlockBoundaries( range );
expandRangeToBlockBoundaries( range, root );
// 3. Remove range.
var body = this._body,
frag;
moveRangeBoundariesUpTree( range, body );
frag = extractContentsOfRange( range, body );
moveRangeBoundariesUpTree( range, root );
frag = extractContentsOfRange( range, root, root );
// 4. Modify tree of fragment and reinsert.
insertNodeInRange( range, modify.call( this, frag ) );
// 5. Merge containers at edges
if ( range.endOffset < range.endContainer.childNodes.length ) {
mergeContainers( range.endContainer.childNodes[ range.endOffset ] );
mergeContainers( range.endContainer.childNodes[ range.endOffset ], root );
}
mergeContainers( range.startContainer.childNodes[ range.startOffset ] );
mergeContainers( range.startContainer.childNodes[ range.startOffset ], root );
// 6. Restore selection
this._getRangeAndRemoveBookmark( range );
@ -1183,9 +1167,10 @@ var increaseBlockQuoteLevel = function ( frag ) {
};
var decreaseBlockQuoteLevel = function ( frag ) {
var root = this._root;
var blockquotes = frag.querySelectorAll( 'blockquote' );
Array.prototype.filter.call( blockquotes, function ( el ) {
return !getNearest( el.parentNode, 'BLOCKQUOTE' );
return !getNearest( el.parentNode, root, 'BLOCKQUOTE' );
}).forEach( function ( el ) {
replaceWith( el, empty( el ) );
});
@ -1206,7 +1191,7 @@ var removeBlockQuote = function (/* frag */) {
};
var makeList = function ( self, frag, type ) {
var walker = getBlockWalker( frag ),
var walker = getBlockWalker( frag, self._root ),
node, tag, prev, newLi,
tagAttributes = self._config.tagAttributes,
listAttrs = tagAttributes[ type.toLowerCase() ],
@ -1269,7 +1254,7 @@ var removeList = function ( frag ) {
child = children[ll];
replaceWith( child, empty( child ) );
}
fixContainer( listFrag );
fixContainer( listFrag, this._root );
replaceWith( list, listFrag );
}
return frag;
@ -1305,6 +1290,7 @@ var increaseListLevel = function ( frag ) {
};
var decreaseListLevel = function ( frag ) {
var root = this._root;
var items = frag.querySelectorAll( 'LI' );
Array.prototype.filter.call( items, function ( el ) {
return !isContainer( el.firstChild );
@ -1315,7 +1301,7 @@ var decreaseListLevel = function ( frag ) {
node = first,
next;
if ( item.previousSibling ) {
parent = split( parent, item, newParent );
parent = split( parent, item, newParent, root );
}
while ( node ) {
next = node.nextSibling;
@ -1326,7 +1312,7 @@ var decreaseListLevel = function ( frag ) {
node = next;
}
if ( newParent.nodeName === 'LI' && first.previousSibling ) {
split( newParent, first, newParent.parentNode );
split( newParent, first, newParent.parentNode, root );
}
while ( item !== frag && !item.childNodes.length ) {
parent = item.parentNode;
@ -1334,16 +1320,16 @@ var decreaseListLevel = function ( frag ) {
item = parent;
}
}, this );
fixContainer( frag );
fixContainer( frag, root );
return frag;
};
proto._ensureBottomLine = function () {
var body = this._body,
last = body.lastElementChild;
var root = this._root;
var last = root.lastElementChild;
if ( !last ||
last.nodeName !== this._config.blockTag || !isBlock( last ) ) {
body.appendChild( this.createDefaultBlock() );
root.appendChild( this.createDefaultBlock() );
}
};
@ -1357,27 +1343,29 @@ proto.setKeyHandler = function ( key, fn ) {
// --- Get/Set data ---
proto._getHTML = function () {
return this._body.innerHTML;
return this._root.innerHTML;
};
proto._setHTML = function ( html ) {
var node = this._body;
var root = this._root;
var node = root;
node.innerHTML = html;
do {
fixCursor( node );
} while ( node = getNextBlock( node ) );
fixCursor( node, root );
} while ( node = getNextBlock( node, root ) );
this._ignoreChange = true;
};
proto.getHTML = function ( withBookMark ) {
var brs = [],
node, fixer, html, l, range;
root, node, fixer, html, l, range;
if ( withBookMark && ( range = this.getSelection() ) ) {
this._saveRangeToBookmark( range );
}
if ( useTextFixer ) {
node = this._body;
while ( node = getNextBlock( node ) ) {
root = this._root;
node = root;
while ( node = getNextBlock( node, root ) ) {
if ( !node.textContent && !node.querySelector( 'BR' ) ) {
fixer = this.createElement( 'BR' );
node.appendChild( fixer );
@ -1399,37 +1387,37 @@ proto.getHTML = function ( withBookMark ) {
};
proto.setHTML = function ( html ) {
var frag = this._doc.createDocumentFragment(),
div = this.createElement( 'DIV' ),
child;
var frag = this._doc.createDocumentFragment();
var div = this.createElement( 'DIV' );
var root = this._root;
var child;
// Parse HTML into DOM tree
div.innerHTML = html;
frag.appendChild( empty( div ) );
cleanTree( frag );
cleanupBRs( frag );
cleanupBRs( frag, root );
fixContainer( frag );
fixContainer( frag, root );
// Fix cursor
var node = frag;
while ( node = getNextBlock( node ) ) {
fixCursor( node );
while ( node = getNextBlock( node, root ) ) {
fixCursor( node, root );
}
// Don't fire an input event
this._ignoreChange = true;
// Remove existing body children
var body = this._body;
while ( child = body.lastChild ) {
body.removeChild( child );
// Remove existing root children
while ( child = root.lastChild ) {
root.removeChild( child );
}
// And insert new content
body.appendChild( frag );
fixCursor( body );
root.appendChild( frag );
fixCursor( root, root );
// Reset the undo stack
this._undoIndex = -1;
@ -1439,17 +1427,13 @@ proto.setHTML = function ( html ) {
// Record undo state
var range = this._getRangeAndRemoveBookmark() ||
this._createRange( body.firstChild, 0 );
this._createRange( root.firstChild, 0 );
this.saveUndoState( range );
// IE will also set focus when selecting text so don't use
// setSelection. Instead, just store it in lastSelection, so if
// anything calls getSelection before first focus, we have a range
// to return.
if ( losesSelectionOnBlur ) {
this._lastSelection = range;
} else {
this.setSelection( range );
}
this._lastSelection = range;
this._updatePath( range, true );
return this;
@ -1463,25 +1447,25 @@ proto.insertElement = function ( el, range ) {
range.setStartAfter( el );
} else {
// Get containing block node.
var body = this._body,
splitNode = getStartBlockOfRange( range ) || body,
parent, nodeAfterSplit;
var root = this._root;
var splitNode = getStartBlockOfRange( range, root ) || root;
var parent, nodeAfterSplit;
// While at end of container node, move up DOM tree.
while ( splitNode !== body && !splitNode.nextSibling ) {
while ( splitNode !== root && !splitNode.nextSibling ) {
splitNode = splitNode.parentNode;
}
// If in the middle of a container node, split up to body.
if ( splitNode !== body ) {
// If in the middle of a container node, split up to root.
if ( splitNode !== root ) {
parent = splitNode.parentNode;
nodeAfterSplit = split( parent, splitNode.nextSibling, body );
nodeAfterSplit = split( parent, splitNode.nextSibling, root, root );
}
if ( nodeAfterSplit ) {
body.insertBefore( el, nodeAfterSplit );
root.insertBefore( el, nodeAfterSplit );
} else {
body.appendChild( el );
root.appendChild( el );
// Insert blank line below block.
nodeAfterSplit = this.createDefaultBlock();
body.appendChild( nodeAfterSplit );
root.appendChild( nodeAfterSplit );
}
range.setStart( nodeAfterSplit, 0 );
range.setEnd( nodeAfterSplit, 0 );
@ -1503,11 +1487,11 @@ proto.insertImage = function ( src, attributes ) {
var linkRegExp = /\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))|([\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,}\b)/i;
var addLinks = function ( frag ) {
var addLinks = function ( frag, root ) {
var doc = frag.ownerDocument,
walker = new TreeWalker( frag, SHOW_TEXT,
function ( node ) {
return !getNearest( node, 'A' );
return !getNearest( node, root, 'A' );
}, false ),
node, data, parent, match, index, endIndex, child;
while ( node = walker.nextNode() ) {
@ -1549,6 +1533,7 @@ proto.insertHTML = function ( html, isPaste ) {
this.saveUndoState( range );
try {
var root = this._root;
var node = frag;
var event = {
fragment: frag,
@ -1558,14 +1543,14 @@ proto.insertHTML = function ( html, isPaste ) {
defaultPrevented: false
};
addLinks( frag );
addLinks( frag, root );
cleanTree( frag );
cleanupBRs( frag );
cleanupBRs( frag, root );
removeEmptyInlines( frag );
frag.normalize();
while ( node = getNextBlock( node ) ) {
fixCursor( node );
while ( node = getNextBlock( node, root ) ) {
fixCursor( node, root );
}
if ( isPaste ) {
@ -1573,7 +1558,7 @@ proto.insertHTML = function ( html, isPaste ) {
}
if ( !event.defaultPrevented ) {
insertTreeFragmentIntoRange( range, event.fragment );
insertTreeFragmentIntoRange( range, event.fragment, root );
if ( !canObserveMutations ) {
this._docWasChanged();
}
@ -1778,13 +1763,14 @@ proto.removeAllFormatting = function ( range ) {
return this;
}
var root = this._root;
var stopNode = range.commonAncestorContainer;
while ( stopNode && !isBlock( stopNode ) ) {
stopNode = stopNode.parentNode;
}
if ( !stopNode ) {
expandRangeToBlockBoundaries( range );
stopNode = this._body;
expandRangeToBlockBoundaries( range, root );
stopNode = root;
}
if ( stopNode.nodeType === TEXT_NODE ) {
return this;
@ -1797,7 +1783,7 @@ proto.removeAllFormatting = function ( range ) {
moveRangeBoundariesUpTree( range, stopNode );
// Split the selection up to the block, or if whole selection in same
// block, expand range boundaries to ends of block and split up to body.
// block, expand range boundaries to ends of block and split up to root.
var doc = stopNode.ownerDocument;
var startContainer = range.startContainer;
var startOffset = range.startOffset;
@ -1808,8 +1794,8 @@ proto.removeAllFormatting = function ( range ) {
// in same container.
var formattedNodes = doc.createDocumentFragment();
var cleanNodes = doc.createDocumentFragment();
var nodeAfterSplit = split( endContainer, endOffset, stopNode );
var nodeInSplit = split( startContainer, startOffset, stopNode );
var nodeAfterSplit = split( endContainer, endOffset, stopNode, root );
var nodeInSplit = split( startContainer, startOffset, stopNode, root );
var nextNode, _range, childNodes;
// Then replace contents in split with a cleaned version of the same:

View file

@ -63,7 +63,7 @@ var onKey = function ( event ) {
// Record undo checkpoint.
this.saveUndoState( range );
// Delete the selection
deleteContentsOfRange( range );
deleteContentsOfRange( range, this._root );
this._ensureBottomLine();
this.setSelection( range );
this._updatePath( range, true );
@ -120,17 +120,17 @@ var afterDelete = function ( self, range ) {
parent.removeChild( node );
// Fix cursor in block
if ( !isBlock( parent ) ) {
parent = getPreviousBlock( parent );
parent = getPreviousBlock( parent, self._root );
}
fixCursor( parent );
fixCursor( parent, self._root );
// Move cursor into text node
moveRangeBoundariesDownTree( range );
}
// If you delete the last character in the sole <div> in Chrome,
// it removes the div and replaces it with just a <br> inside the
// body. Detach the <br>; the _ensureBottomLine call will insert a new
// root. Detach the <br>; the _ensureBottomLine call will insert a new
// block.
if ( node.nodeName === 'BODY' &&
if ( node === self._root &&
( node = node.firstChild ) && node.nodeName === 'BR' ) {
detach( node );
}
@ -144,6 +144,7 @@ var afterDelete = function ( self, range ) {
var keyHandlers = {
enter: function ( self, event, range ) {
var root = self._root;
var block, parent, nodeAfterSplit;
// We handle this ourselves
@ -160,10 +161,10 @@ var keyHandlers = {
// Selected text is overwritten, therefore delete the contents
// to collapse selection.
if ( !range.collapsed ) {
deleteContentsOfRange( range );
deleteContentsOfRange( range, root );
}
block = getStartBlockOfRange( range );
block = getStartBlockOfRange( range, root );
// If this is a malformed bit of document or in a table;
// just play it safe and insert a <br>.
@ -176,17 +177,18 @@ var keyHandlers = {
}
// If in a list, we'll split the LI instead.
if ( parent = getNearest( block, 'LI' ) ) {
if ( parent = getNearest( block, root, 'LI' ) ) {
block = parent;
}
if ( !block.textContent ) {
// Break list
if ( getNearest( block, 'UL' ) || getNearest( block, 'OL' ) ) {
if ( getNearest( block, root, 'UL' ) ||
getNearest( block, root, 'OL' ) ) {
return self.modifyBlocks( decreaseListLevel, range );
}
// Break blockquote
else if ( getNearest( block, 'BLOCKQUOTE' ) ) {
else if ( getNearest( block, root, 'BLOCKQUOTE' ) ) {
return self.modifyBlocks( removeBlockQuote, range );
}
}
@ -199,7 +201,7 @@ var keyHandlers = {
// block
removeZWS( block );
removeEmptyInlines( block );
fixCursor( block );
fixCursor( block, root );
// Focus cursor
// If there's a <b>/<i> etc. at the beginning of the split
@ -242,27 +244,28 @@ var keyHandlers = {
self._updatePath( range, true );
},
backspace: function ( self, event, range ) {
var root = self._root;
self._removeZWS();
// Record undo checkpoint.
self.saveUndoState( range );
// If not collapsed, delete contents
if ( !range.collapsed ) {
event.preventDefault();
deleteContentsOfRange( range );
deleteContentsOfRange( range, root );
afterDelete( self, range );
}
// If at beginning of block, merge with previous
else if ( rangeDoesStartAtBlockBoundary( range ) ) {
else if ( rangeDoesStartAtBlockBoundary( range, root ) ) {
event.preventDefault();
var current = getStartBlockOfRange( range );
var current = getStartBlockOfRange( range, root );
var previous;
if ( !current ) {
return;
}
// In case inline data has somehow got between blocks.
fixContainer( current.parentNode );
fixContainer( current.parentNode, root );
// Now get previous block
previous = getPreviousBlock( current );
previous = getPreviousBlock( current, root );
// Must not be at the very beginning of the text area.
if ( previous ) {
// If not editable, just delete whole block.
@ -279,7 +282,7 @@ var keyHandlers = {
current = current.parentNode;
}
if ( current && ( current = current.nextSibling ) ) {
mergeContainers( current );
mergeContainers( current, root );
}
self.setSelection( range );
}
@ -287,12 +290,12 @@ var keyHandlers = {
// to break lists/blockquote.
else if ( current ) {
// Break list
if ( getNearest( current, 'UL' ) ||
getNearest( current, 'OL' ) ) {
if ( getNearest( current, root, 'UL' ) ||
getNearest( current, root, 'OL' ) ) {
return self.modifyBlocks( decreaseListLevel, range );
}
// Break blockquote
else if ( getNearest( current, 'BLOCKQUOTE' ) ) {
else if ( getNearest( current, root, 'BLOCKQUOTE' ) ) {
return self.modifyBlocks( decreaseBlockQuoteLevel, range );
}
self.setSelection( range );
@ -307,6 +310,7 @@ var keyHandlers = {
}
},
'delete': function ( self, event, range ) {
var root = self._root;
var current, next, originalRange,
cursorContainer, cursorOffset, nodeAfterCursor;
self._removeZWS();
@ -315,20 +319,20 @@ var keyHandlers = {
// If not collapsed, delete contents
if ( !range.collapsed ) {
event.preventDefault();
deleteContentsOfRange( range );
deleteContentsOfRange( range, root );
afterDelete( self, range );
}
// If at end of block, merge next into this block
else if ( rangeDoesEndAtBlockBoundary( range ) ) {
else if ( rangeDoesEndAtBlockBoundary( range, root ) ) {
event.preventDefault();
current = getStartBlockOfRange( range );
current = getStartBlockOfRange( range, root );
if ( !current ) {
return;
}
// In case inline data has somehow got between blocks.
fixContainer( current.parentNode );
fixContainer( current.parentNode, root );
// Now get next block
next = getNextBlock( current );
next = getNextBlock( current, root );
// Must not be at the very end of the text area.
if ( next ) {
// If not editable, just delete whole block.
@ -345,7 +349,7 @@ var keyHandlers = {
next = next.parentNode;
}
if ( next && ( next = next.nextSibling ) ) {
mergeContainers( next );
mergeContainers( next, root );
}
self.setSelection( range );
self._updatePath( range, true );
@ -358,7 +362,7 @@ var keyHandlers = {
// delete it ourselves, because the browser won't if it is not
// inline.
originalRange = range.cloneRange();
moveRangeBoundariesUpTree( range, self._body );
moveRangeBoundariesUpTree( range, self._root );
cursorContainer = range.endContainer;
cursorOffset = range.endOffset;
if ( cursorContainer.nodeType === ELEMENT_NODE ) {
@ -376,11 +380,12 @@ var keyHandlers = {
}
},
tab: function ( self, event, range ) {
var root = self._root;
var node, parent;
self._removeZWS();
// If no selection and at start of block
if ( range.collapsed && rangeDoesStartAtBlockBoundary( range ) ) {
node = getStartBlockOfRange( range );
if ( range.collapsed && rangeDoesStartAtBlockBoundary( range, root ) ) {
node = getStartBlockOfRange( range, root );
// Iterate through the block's parents
while ( parent = node.parentNode ) {
// If we find a UL or OL (so are in a list, node must be an LI)
@ -398,12 +403,15 @@ var keyHandlers = {
}
},
'shift-tab': function ( self, event, range ) {
var root = self._root;
var node;
self._removeZWS();
// If no selection and at start of block
if ( range.collapsed && rangeDoesStartAtBlockBoundary( range ) ) {
if ( range.collapsed && rangeDoesStartAtBlockBoundary( range, root ) ) {
// Break list
var node = range.startContainer;
if ( getNearest( node, 'UL' ) || getNearest( node, 'OL' ) ) {
node = range.startContainer;
if ( getNearest( node, root, 'UL' ) ||
getNearest( node, root, 'OL' ) ) {
event.preventDefault();
self.modifyBlocks( decreaseListLevel, range );
}

View file

@ -20,27 +20,6 @@ function every ( nodeList, fn ) {
// ---
function hasTagAttributes ( node, tag, attributes ) {
if ( node.nodeName !== tag ) {
return false;
}
for ( var attr in attributes ) {
if ( node.getAttribute( attr ) !== attributes[ attr ] ) {
return false;
}
}
return true;
}
function areAlike ( node, node2 ) {
return !isLeaf( node ) && (
node.nodeType === node2.nodeType &&
node.nodeName === node2.nodeName &&
node.className === node2.className &&
( ( !node.style && !node2.style ) ||
node.style.cssText === node2.style.cssText )
);
}
function isLeaf ( node ) {
return node.nodeType === ELEMENT_NODE &&
!!leafNodeNames[ node.nodeName ];
@ -59,48 +38,80 @@ function isContainer ( node ) {
!isInline( node ) && !isBlock( node );
}
function getBlockWalker ( node ) {
var doc = node.ownerDocument,
walker = new TreeWalker(
doc.body, SHOW_ELEMENT, isBlock, false );
function getBlockWalker ( node, root ) {
var walker = new TreeWalker( root, SHOW_ELEMENT, isBlock, false );
walker.currentNode = node;
return walker;
}
function getPreviousBlock ( node, root ) {
node = getBlockWalker( node, root ).previousNode();
return node !== root ? node : null;
}
function getNextBlock ( node, root ) {
node = getBlockWalker( node, root ).nextNode();
return node !== root ? node : null;
}
function getPreviousBlock ( node ) {
return getBlockWalker( node ).previousNode();
function areAlike ( node, node2 ) {
return !isLeaf( node ) && (
node.nodeType === node2.nodeType &&
node.nodeName === node2.nodeName &&
node.className === node2.className &&
( ( !node.style && !node2.style ) ||
node.style.cssText === node2.style.cssText )
);
}
function getNextBlock ( node ) {
return getBlockWalker( node ).nextNode();
function hasTagAttributes ( node, tag, attributes ) {
if ( node.nodeName !== tag ) {
return false;
}
for ( var attr in attributes ) {
if ( node.getAttribute( attr ) !== attributes[ attr ] ) {
return false;
}
}
return true;
}
function getNearest ( node, tag, attributes ) {
do {
function getNearest ( node, root, tag, attributes ) {
while ( node && node !== root ) {
if ( hasTagAttributes( node, tag, attributes ) ) {
return node;
}
} while ( node = node.parentNode );
node = node.parentNode;
}
return null;
}
function isOrContains ( parent, node ) {
while ( node ) {
if ( node === parent ) {
return true;
}
node = node.parentNode;
}
return false;
}
function getPath ( node ) {
function getPath ( node, root ) {
var parent = node.parentNode,
path, id, className, classNames, dir;
if ( !parent || node.nodeType !== ELEMENT_NODE ) {
path = parent ? getPath( parent ) : '';
if ( node === root ) {
path = '';
} else {
path = getPath( parent );
path += ( path ? '>' : '' ) + node.nodeName;
if ( id = node.id ) {
path += '#' + id;
}
if ( className = node.className.trim() ) {
classNames = className.split( /\s\s*/ );
classNames.sort();
path += '.';
path += classNames.join( '.' );
}
if ( dir = node.dir ) {
path += '[dir=' + dir + ']';
path = getPath( parent, root );
if ( node.nodeType === ELEMENT_NODE ) {
path += ( path ? '>' : '' ) + node.nodeName;
if ( id = node.id ) {
path += '#' + id;
}
if ( className = node.className.trim() ) {
classNames = className.split( /\s\s*/ );
classNames.sort();
path += '.';
path += classNames.join( '.' );
}
if ( dir = node.dir ) {
path += '[dir=' + dir + ']';
}
}
}
return path;
@ -158,16 +169,16 @@ function createElement ( doc, tag, props, children ) {
return el;
}
function fixCursor ( node ) {
function fixCursor ( node, root ) {
// In Webkit and Gecko, block level elements are collapsed and
// unfocussable if they have no content. To remedy this, a <BR> must be
// inserted. In Opera and IE, we just need a textnode in order for the
// cursor to appear.
var doc = node.ownerDocument,
root = node,
originalNode = node,
fixer, child;
if ( node.nodeName === 'BODY' ) {
if ( node === root ) {
if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) {
fixer = getSquireInstance( doc ).createDefaultBlock();
if ( child ) {
@ -227,11 +238,11 @@ function fixCursor ( node ) {
node.appendChild( fixer );
}
return root;
return originalNode;
}
// Recursively examine container nodes and wrap any inline children.
function fixContainer ( container ) {
function fixContainer ( container, root ) {
var children = container.childNodes,
doc = container.ownerDocument,
wrapper = null,
@ -254,7 +265,7 @@ function fixContainer ( container ) {
wrapper = createElement( doc,
config.blockTag, config.blockAttributes );
}
fixCursor( wrapper );
fixCursor( wrapper, root );
if ( isBR ) {
container.replaceChild( wrapper, child );
} else {
@ -265,20 +276,21 @@ function fixContainer ( container ) {
wrapper = null;
}
if ( isContainer( child ) ) {
fixContainer( child );
fixContainer( child, root );
}
}
if ( wrapper ) {
container.appendChild( fixCursor( wrapper ) );
container.appendChild( fixCursor( wrapper, root ) );
}
return container;
}
function split ( node, offset, stopNode ) {
function split ( node, offset, stopNode, root ) {
var nodeType = node.nodeType,
parent, clone, next;
if ( nodeType === TEXT_NODE && node !== stopNode ) {
return split( node.parentNode, node.splitText( offset ), stopNode );
return split(
node.parentNode, node.splitText( offset ), stopNode, root );
}
if ( nodeType === ELEMENT_NODE ) {
if ( typeof( offset ) === 'number' ) {
@ -301,7 +313,8 @@ function split ( node, offset, stopNode ) {
}
// Maintain li numbering if inside a quote.
if ( node.nodeName === 'OL' && getNearest( node, 'BLOCKQUOTE' ) ) {
if ( node.nodeName === 'OL' &&
getNearest( node, root, 'BLOCKQUOTE' ) ) {
clone.start = ( +node.start || 1 ) + node.childNodes.length - 1;
}
@ -309,8 +322,8 @@ function split ( node, offset, stopNode ) {
// of a node lower down the tree!
// We need something in the element in order for the cursor to appear.
fixCursor( node );
fixCursor( clone );
fixCursor( node, root );
fixCursor( clone, root );
// Inject clone after original node
if ( next = node.nextSibling ) {
@ -320,7 +333,7 @@ function split ( node, offset, stopNode ) {
}
// Keep on splitting up the tree
return split( parent, clone, stopNode );
return split( parent, clone, stopNode, root );
}
return offset;
}
@ -425,7 +438,7 @@ function mergeWithBlock ( block, next, range ) {
}
}
function mergeContainers ( node ) {
function mergeContainers ( node, root ) {
var prev = node.previousSibling,
first = node.firstChild,
doc = node.ownerDocument,
@ -451,14 +464,14 @@ function mergeContainers ( node ) {
needsFix = !isContainer( node );
prev.appendChild( empty( node ) );
if ( needsFix ) {
fixContainer( prev );
fixContainer( prev, root );
}
if ( first ) {
mergeContainers( first );
mergeContainers( first, root );
}
} else if ( isListItem ) {
prev = createElement( doc, 'DIV' );
node.insertBefore( prev, first );
fixCursor( prev );
fixCursor( prev, root );
}
}

View file

@ -80,7 +80,7 @@ var insertNodeInRange = function ( range, node ) {
range.setEnd( endContainer, endOffset );
};
var extractContentsOfRange = function ( range, common ) {
var extractContentsOfRange = function ( range, common, root ) {
var startContainer = range.startContainer,
startOffset = range.startOffset,
endContainer = range.endContainer,
@ -94,8 +94,8 @@ var extractContentsOfRange = function ( range, common ) {
common = common.parentNode;
}
var endNode = split( endContainer, endOffset, common ),
startNode = split( startContainer, startOffset, common ),
var endNode = split( endContainer, endOffset, common, root ),
startNode = split( startContainer, startOffset, common, root ),
frag = common.ownerDocument.createDocumentFragment(),
next, before, after;
@ -127,12 +127,12 @@ var extractContentsOfRange = function ( range, common ) {
range.setStart( startContainer, startOffset );
range.collapse( true );
fixCursor( common );
fixCursor( common, root );
return frag;
};
var deleteContentsOfRange = function ( range ) {
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
@ -145,7 +145,7 @@ var deleteContentsOfRange = function ( range ) {
( isInline( endBlock ) || isBlock( endBlock ) );
// Remove selected range
var frag = extractContentsOfRange( 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
@ -154,8 +154,8 @@ var deleteContentsOfRange = function ( range ) {
// If we split into two different blocks, merge the blocks.
if ( needsMerge ) {
startBlock = getStartBlockOfRange( range );
endBlock = getEndBlockOfRange( range );
startBlock = getStartBlockOfRange( range, root );
endBlock = getEndBlockOfRange( range, root );
if ( startBlock && endBlock && startBlock !== endBlock ) {
mergeWithBlock( startBlock, endBlock, range );
}
@ -163,15 +163,14 @@ var deleteContentsOfRange = function ( range ) {
// Ensure block has necessary children
if ( startBlock ) {
fixCursor( startBlock );
fixCursor( startBlock, root );
}
// Ensure body has a block-level element in it.
var body = range.endContainer.ownerDocument.body,
child = body.firstChild;
// Ensure root has a block-level element in it.
var child = root.firstChild;
if ( !child || child.nodeName === 'BR' ) {
fixCursor( body );
range.selectNodeContents( body.firstChild );
fixCursor( root, root );
range.selectNodeContents( root.firstChild );
} else {
range.collapse( false );
}
@ -180,7 +179,7 @@ var deleteContentsOfRange = function ( range ) {
// ---
var insertTreeFragmentIntoRange = function ( range, frag ) {
var insertTreeFragmentIntoRange = function ( range, frag, root ) {
// Check if it's all inline content
var allInline = true,
children = frag.childNodes,
@ -194,7 +193,7 @@ var insertTreeFragmentIntoRange = function ( range, frag ) {
// Delete any selected content
if ( !range.collapsed ) {
deleteContentsOfRange( range );
deleteContentsOfRange( range, root );
}
// Move range down into text nodes
@ -206,11 +205,14 @@ var insertTreeFragmentIntoRange = function ( range, frag ) {
range.collapse( false );
} else {
// Otherwise...
// 1. Split up to blockquote (if a parent) or body
// 1. Split up to blockquote (if a parent) or root
var splitPoint = range.startContainer,
nodeAfterSplit = split( splitPoint, range.startOffset,
getNearest( splitPoint.parentNode, 'BLOCKQUOTE' ) ||
splitPoint.ownerDocument.body ),
nodeAfterSplit = split(
splitPoint,
range.startOffset,
getNearest( splitPoint.parentNode, root, 'BLOCKQUOTE' ) || root,
root
),
nodeBeforeSplit = nodeAfterSplit.previousSibling,
startContainer = nodeBeforeSplit,
startOffset = startContainer.childNodes.length,
@ -246,15 +248,15 @@ var insertTreeFragmentIntoRange = function ( range, frag ) {
// 3. Fix cursor then insert block(s) in the fragment
node = frag;
while ( node = getNextBlock( node ) ) {
fixCursor( node );
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 );
node = getPreviousBlock( next, root );
if ( !/\S/.test( node.textContent ) ) {
do {
parent = node.parentNode;
@ -273,12 +275,12 @@ var insertTreeFragmentIntoRange = function ( range, frag ) {
}
// Merge inserted containers with edges of split
if ( isContainer( next ) ) {
mergeContainers( next );
mergeContainers( next, root );
}
prev = nodeAfterSplit.previousSibling;
node = isBlock( nodeAfterSplit ) ?
nodeAfterSplit : getNextBlock( nodeAfterSplit );
nodeAfterSplit : getNextBlock( nodeAfterSplit, root );
if ( !/\S/.test( node.textContent ) ) {
do {
parent = node.parentNode;
@ -296,7 +298,7 @@ var insertTreeFragmentIntoRange = function ( range, frag ) {
}
// Merge inserted containers with edges of split
if ( nodeAfterSplit && isContainer( nodeAfterSplit ) ) {
mergeContainers( nodeAfterSplit );
mergeContainers( nodeAfterSplit, root );
}
range.setStart( startContainer, startOffset );
@ -408,18 +410,18 @@ var moveRangeBoundariesUpTree = function ( range, common ) {
// 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 getStartBlockOfRange = function ( range, root ) {
var container = range.startContainer,
block;
// If inline, get the containing block.
if ( isInline( container ) ) {
block = getPreviousBlock( container );
block = getPreviousBlock( container, root );
} else if ( isBlock( container ) ) {
block = container;
} else {
block = getNodeBefore( container, range.startOffset );
block = getNextBlock( block );
block = getNextBlock( block, root );
}
// Check the block actually intersects the range
return block && isNodeContainedInRange( range, block, true ) ? block : null;
@ -427,25 +429,24 @@ var getStartBlockOfRange = function ( range ) {
// 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 getEndBlockOfRange = function ( range, root ) {
var container = range.endContainer,
block, child;
// If inline, get the containing block.
if ( isInline( container ) ) {
block = getPreviousBlock( container );
block = getPreviousBlock( container, root );
} else if ( isBlock( container ) ) {
block = container;
} else {
block = getNodeAfter( container, range.endOffset );
if ( !block ) {
block = container.ownerDocument.body;
block = root;
while ( child = block.lastChild ) {
block = child;
}
}
block = getPreviousBlock( block );
block = getPreviousBlock( block, root );
}
// Check the block actually intersects the range
return block && isNodeContainedInRange( range, block, true ) ? block : null;
@ -460,7 +461,7 @@ var contentWalker = new TreeWalker( null,
}
);
var rangeDoesStartAtBlockBoundary = function ( range ) {
var rangeDoesStartAtBlockBoundary = function ( range, root ) {
var startContainer = range.startContainer,
startOffset = range.startOffset;
@ -476,12 +477,12 @@ var rangeDoesStartAtBlockBoundary = function ( range ) {
}
// Otherwise, look for any previous content in the same block.
contentWalker.root = getStartBlockOfRange( range );
contentWalker.root = getStartBlockOfRange( range, root );
return !contentWalker.previousNode();
};
var rangeDoesEndAtBlockBoundary = function ( range ) {
var rangeDoesEndAtBlockBoundary = function ( range, root ) {
var endContainer = range.endContainer,
endOffset = range.endOffset,
length;
@ -500,14 +501,14 @@ var rangeDoesEndAtBlockBoundary = function ( range ) {
}
// Otherwise, look for any further content in the same block.
contentWalker.root = getEndBlockOfRange( range );
contentWalker.root = getEndBlockOfRange( range, root );
return !contentWalker.nextNode();
};
var expandRangeToBlockBoundaries = function ( range ) {
var start = getStartBlockOfRange( range ),
end = getEndBlockOfRange( range ),
var expandRangeToBlockBoundaries = function ( range, root ) {
var start = getStartBlockOfRange( range, root ),
end = getEndBlockOfRange( range, root ),
parent;
if ( start && end ) {