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 { p {
margin: 5px 0; 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; border: 1px solid #888;
width: 100%; padding: 1em;
height: 500px; 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> </style>
</head> </head>
@ -69,84 +100,19 @@
<span id="redo">Redo</span> <span id="redo">Redo</span>
</p> </p>
</header> </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> <script type="text/javascript" src="build/squire-raw.js"></script>
<div id="editor"></div>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
var editor; var div = document.getElementById( 'editor' );
var iframe = document.createElement( 'iframe' ); var editor = new Squire( div, {
iframe.addEventListener( 'load', function () { blockTag: 'p',
// Make sure we're in standards mode. blockAttributes: {'class': 'paragraph'},
var doc = iframe.contentDocument; tagAttributes: {
if ( doc.compatMode !== 'CSS1Compat' ) { ul: {'class': 'UL'},
doc.open(); ol: {'class': 'OL'},
doc.write( '<!DOCTYPE html><title></title>' ); li: {'class': 'listItem'}
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 );
document.addEventListener( 'click', function ( e ) { document.addEventListener( 'click', function ( e ) {
var id = e.target.id, 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 removeEmptyInlines = function removeEmptyInlines ( node ) {
var children = root.childNodes, var children = node.childNodes,
l = children.length, l = children.length,
child; child;
while ( l-- ) { while ( l-- ) {
@ -281,10 +281,10 @@ var removeEmptyInlines = function removeEmptyInlines ( root ) {
if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) { if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) {
removeEmptyInlines( child ); removeEmptyInlines( child );
if ( isInline( child ) && !child.firstChild ) { if ( isInline( child ) && !child.firstChild ) {
root.removeChild( child ); node.removeChild( child );
} }
} else if ( child.nodeType === TEXT_NODE && !child.data ) { } 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> // 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 // elements at the end of each block will then have them added back in a later
// fixCursor method call. // fixCursor method call.
var cleanupBRs = function ( root ) { var cleanupBRs = function ( node, root ) {
var brs = root.querySelectorAll( 'BR' ), var brs = node.querySelectorAll( 'BR' ),
brBreaksLine = [], brBreaksLine = [],
l = brs.length, l = brs.length,
i, br, parent; i, br, parent;
@ -340,7 +340,7 @@ var cleanupBRs = function ( root ) {
if ( !brBreaksLine[l] ) { if ( !brBreaksLine[l] ) {
detach( br ); detach( br );
} else if ( !isInline( parent ) ) { } else if ( !isInline( parent ) ) {
fixContainer( parent ); fixContainer( parent, root );
} }
} }
}; };

View file

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

View file

@ -3,6 +3,7 @@
var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING
var ELEMENT_NODE = 1; // Node.ELEMENT_NODE; var ELEMENT_NODE = 1; // Node.ELEMENT_NODE;
var TEXT_NODE = 3; // Node.TEXT_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 DOCUMENT_FRAGMENT_NODE = 11; // Node.DOCUMENT_FRAGMENT_NODE;
var SHOW_ELEMENT = 1; // NodeFilter.SHOW_ELEMENT; var SHOW_ELEMENT = 1; // NodeFilter.SHOW_ELEMENT;
var SHOW_TEXT = 4; // NodeFilter.SHOW_TEXT; var SHOW_TEXT = 4; // NodeFilter.SHOW_TEXT;

View file

@ -28,14 +28,17 @@ function mergeObjects ( base, extras ) {
return base; 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 win = doc.defaultView;
var body = doc.body;
var mutation; var mutation;
this._win = win; this._win = win;
this._doc = doc; this._doc = doc;
this._body = body; this._root = root;
this._events = {}; this._events = {};
@ -56,9 +59,6 @@ function Squire ( doc, config ) {
this.addEventListener( 'keyup', this._updatePathOnEvent ); this.addEventListener( 'keyup', this._updatePathOnEvent );
this.addEventListener( 'mouseup', this._updatePathOnEvent ); this.addEventListener( 'mouseup', this._updatePathOnEvent );
win.addEventListener( 'focus', this, false );
win.addEventListener( 'blur', this, false );
this._undoIndex = -1; this._undoIndex = -1;
this._undoStack = []; this._undoStack = [];
this._undoStackLength = 0; this._undoStackLength = 0;
@ -67,7 +67,7 @@ function Squire ( doc, config ) {
if ( canObserveMutations ) { if ( canObserveMutations ) {
mutation = new MutationObserver( this._docWasChanged.bind( this ) ); mutation = new MutationObserver( this._docWasChanged.bind( this ) );
mutation.observe( body, { mutation.observe( root, {
childList: true, childList: true,
attributes: true, attributes: true,
characterData: 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 // Remove Firefox's built-in controls
try { try {
@ -167,7 +167,8 @@ proto.createElement = function ( tag, props, children ) {
proto.createDefaultBlock = function ( children ) { proto.createDefaultBlock = function ( children ) {
var config = this._config; var config = this._config;
return fixCursor( 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 // document node, since these events are fired in a custom manner by the
// editor code. // editor code.
var customEvents = { var customEvents = {
focus: 1, blur: 1, pathChange: 1, select: 1, input: 1,
pathChange: 1, select: 1, input: 1, undoStateChange: 1 undoStateChange: 1, scrollPointIntoView: 1
}; };
proto.fireEvent = function ( type, event ) { proto.fireEvent = function ( type, event ) {
@ -220,15 +221,12 @@ proto.fireEvent = function ( type, event ) {
}; };
proto.destroy = function () { proto.destroy = function () {
var win = this._win, var root = this._root,
doc = this._doc,
events = this._events, events = this._events,
type; type;
win.removeEventListener( 'focus', this, false );
win.removeEventListener( 'blur', this, false );
for ( type in events ) { for ( type in events ) {
if ( !customEvents[ type ] ) { if ( !customEvents[ type ] ) {
doc.removeEventListener( type, this, true ); root.removeEventListener( type, this, true );
} }
} }
if ( this._mutation ) { if ( this._mutation ) {
@ -258,7 +256,7 @@ proto.addEventListener = function ( type, fn ) {
if ( !handlers ) { if ( !handlers ) {
handlers = this._events[ type ] = []; handlers = this._events[ type ] = [];
if ( !customEvents[ type ] ) { if ( !customEvents[ type ] ) {
this._doc.addEventListener( type, this, true ); this._root.addEventListener( type, this, true );
} }
} }
handlers.push( fn ); handlers.push( fn );
@ -278,7 +276,7 @@ proto.removeEventListener = function ( type, fn ) {
if ( !handlers.length ) { if ( !handlers.length ) {
delete this._events[ type ]; delete this._events[ type ];
if ( !customEvents[ 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.removeChild( node );
parent.normalize(); 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 // And fire event for integrations to use
this.fireEvent( 'scrollPointIntoView', { if ( rect ) {
x: rect.left, this.fireEvent( 'scrollPointIntoView', {
y: top x: rect.left,
}); y: rect.top
});
}
}; };
proto._moveCursorTo = function ( toStart ) { proto._moveCursorTo = function ( toStart ) {
var body = this._body, var root = this._root,
range = this._createRange( body, toStart ? 0 : body.childNodes.length ); range = this._createRange( root, toStart ? 0 : root.childNodes.length );
moveRangeBoundariesDownTree( range ); moveRangeBoundariesDownTree( range );
this.setSelection( range ); this.setSelection( range );
return this; return this;
@ -370,8 +360,9 @@ proto.setSelection = function ( range ) {
}; };
proto.getSelection = function () { proto.getSelection = function () {
var sel = getWindowSelection( this ), var sel = getWindowSelection( this );
selection, startContainer, endContainer; var root = this._root;
var selection, startContainer, endContainer;
if ( sel && sel.rangeCount ) { if ( sel && sel.rangeCount ) {
selection = sel.getRangeAt( 0 ).cloneRange(); selection = sel.getRangeAt( 0 ).cloneRange();
startContainer = selection.startContainer; startContainer = selection.startContainer;
@ -383,12 +374,15 @@ proto.getSelection = function () {
if ( endContainer && isLeaf( endContainer ) ) { if ( endContainer && isLeaf( endContainer ) ) {
selection.setEndBefore( endContainer ); selection.setEndBefore( endContainer );
} }
}
if ( selection &&
isOrContains( root, selection.commonAncestorContainer ) ) {
this._lastSelection = selection; this._lastSelection = selection;
} else { } else {
selection = this._lastSelection; selection = this._lastSelection;
} }
if ( !selection ) { if ( !selection ) {
selection = this._createRange( this._body.firstChild, 0 ); selection = this._createRange( root.firstChild, 0 );
} }
return selection; return selection;
}; };
@ -474,7 +468,7 @@ proto._removeZWS = function () {
if ( !this._hasZWS ) { if ( !this._hasZWS ) {
return; return;
} }
removeZWS( this._body ); removeZWS( this._root );
this._hasZWS = false; this._hasZWS = false;
}; };
@ -489,7 +483,7 @@ proto._updatePath = function ( range, force ) {
this._lastAnchorNode = anchor; this._lastAnchorNode = anchor;
this._lastFocusNode = focus; this._lastFocusNode = focus;
newPath = ( anchor && focus ) ? ( anchor === focus ) ? newPath = ( anchor && focus ) ? ( anchor === focus ) ?
getPath( focus ) : '(selection)' : ''; getPath( focus, this._root ) : '(selection)' : '';
if ( this._path !== newPath ) { if ( this._path !== newPath ) {
this._path = newPath; this._path = newPath;
this.fireEvent( 'pathChange', { path: newPath } ); this.fireEvent( 'pathChange', { path: newPath } );
@ -507,26 +501,12 @@ proto._updatePathOnEvent = function () {
// --- Focus --- // --- Focus ---
proto.focus = function () { proto.focus = function () {
// FF seems to need the body to be focussed (at least on first load). this._root.focus();
// 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();
return this; return this;
}; };
proto.blur = function () { proto.blur = function () {
// IE will remove the whole browser window from focus if you call this._root.blur();
// 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();
return this; return this;
}; };
@ -565,9 +545,9 @@ proto._saveRangeToBookmark = function ( range ) {
}; };
proto._getRangeAndRemoveBookmark = function ( range ) { proto._getRangeAndRemoveBookmark = function ( range ) {
var doc = this._doc, var root = this._root,
start = doc.getElementById( startSelectionId ), start = root.querySelector( '#' + startSelectionId ),
end = doc.getElementById( endSelectionId ); end = root.querySelector( '#' + endSelectionId );
if ( start && end ) { if ( start && end ) {
var startContainer = start.parentNode, var startContainer = start.parentNode,
@ -595,7 +575,7 @@ proto._getRangeAndRemoveBookmark = function ( range ) {
} }
if ( !range ) { if ( !range ) {
range = doc.createRange(); range = this._doc.createRange();
} }
range.setStart( _range.startContainer, _range.startOffset ); range.setStart( _range.startContainer, _range.startOffset );
range.setEnd( _range.endContainer, _range.endOffset ); 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 // If the common ancestor is inside the tag we require, we definitely
// have the format. // have the format.
var root = range.commonAncestorContainer, var root = this._root;
walker, node; var common = range.commonAncestorContainer;
if ( getNearest( root, tag, attributes ) ) { var walker, node;
if ( getNearest( common, root, tag, attributes ) ) {
return true; return true;
} }
// If common ancestor is a text node and doesn't have the format, we // If common ancestor is a text node and doesn't have the format, we
// definitely don't have it. // definitely don't have it.
if ( root.nodeType === TEXT_NODE ) { if ( common.nodeType === TEXT_NODE ) {
return false; return false;
} }
// Otherwise, check each text node at least partially contained within // Otherwise, check each text node at least partially contained within
// the selection and make sure all of them have the format we want. // 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 ); return isNodeContainedInRange( range, node, true );
}, false ); }, false );
var seenNode = false; var seenNode = false;
while ( node = walker.nextNode() ) { while ( node = walker.nextNode() ) {
if ( !getNearest( node, tag, attributes ) ) { if ( !getNearest( node, root, tag, attributes ) ) {
return false; return false;
} }
seenNode = true; seenNode = true;
@ -823,11 +804,12 @@ proto.getFontInfo = function ( range ) {
proto._addFormat = function ( tag, attributes, range ) { proto._addFormat = function ( tag, attributes, range ) {
// If the range is collapsed we simply insert the node by wrapping // If the range is collapsed we simply insert the node by wrapping
// it round the range and focus it. // it round the range and focus it.
var root = this._root;
var el, walker, startContainer, endContainer, startOffset, endOffset, var el, walker, startContainer, endContainer, startOffset, endOffset,
node, needsFormat; node, needsFormat;
if ( range.collapsed ) { if ( range.collapsed ) {
el = fixCursor( this.createElement( tag, attributes ) ); el = fixCursor( this.createElement( tag, attributes ), root );
insertNodeInRange( range, el ); insertNodeInRange( range, el );
range.setStart( el.firstChild, el.firstChild.length ); range.setStart( el.firstChild, el.firstChild.length );
range.collapse( true ); range.collapse( true );
@ -880,7 +862,7 @@ proto._addFormat = function ( tag, attributes, range ) {
do { do {
node = walker.currentNode; node = walker.currentNode;
needsFormat = !getNearest( node, tag, attributes ); needsFormat = !getNearest( node, root, tag, attributes );
if ( needsFormat ) { if ( needsFormat ) {
// <br> can never be a container node, so must have a text node // <br> can never be a container node, so must have a text node
// if node == (end|start)Container // if node == (end|start)Container
@ -1078,7 +1060,7 @@ var tagAfterSplit = {
var splitBlock = function ( self, block, node, offset ) { var splitBlock = function ( self, block, node, offset ) {
var splitTag = tagAfterSplit[ block.nodeName ], var splitTag = tagAfterSplit[ block.nodeName ],
splitProperties = null, splitProperties = null,
nodeAfterSplit = split( node, offset, block.parentNode ), nodeAfterSplit = split( node, offset, block.parentNode, self._root ),
config = self._config; config = self._config;
if ( !splitTag ) { if ( !splitTag ) {
@ -1110,12 +1092,13 @@ proto.forEachBlock = function ( fn, mutates, range ) {
this.saveUndoState( range ); this.saveUndoState( range );
} }
var start = getStartBlockOfRange( range ), var root = this._root;
end = getEndBlockOfRange( range ); var start = getStartBlockOfRange( range, root );
var end = getEndBlockOfRange( range, root );
if ( start && end ) { if ( start && end ) {
do { do {
if ( fn( start ) || start === end ) { break; } if ( fn( start ) || start === end ) { break; }
} while ( start = getNextBlock( start ) ); } while ( start = getNextBlock( start, root ) );
} }
if ( mutates ) { if ( mutates ) {
@ -1144,23 +1127,24 @@ proto.modifyBlocks = function ( modify, range ) {
this._recordUndoState( range ); this._recordUndoState( range );
} }
var root = this._root;
var frag;
// 2. Expand range to block boundaries // 2. Expand range to block boundaries
expandRangeToBlockBoundaries( range ); expandRangeToBlockBoundaries( range, root );
// 3. Remove range. // 3. Remove range.
var body = this._body, moveRangeBoundariesUpTree( range, root );
frag; frag = extractContentsOfRange( range, root, root );
moveRangeBoundariesUpTree( range, body );
frag = extractContentsOfRange( range, body );
// 4. Modify tree of fragment and reinsert. // 4. Modify tree of fragment and reinsert.
insertNodeInRange( range, modify.call( this, frag ) ); insertNodeInRange( range, modify.call( this, frag ) );
// 5. Merge containers at edges // 5. Merge containers at edges
if ( range.endOffset < range.endContainer.childNodes.length ) { 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 // 6. Restore selection
this._getRangeAndRemoveBookmark( range ); this._getRangeAndRemoveBookmark( range );
@ -1183,9 +1167,10 @@ var increaseBlockQuoteLevel = function ( frag ) {
}; };
var decreaseBlockQuoteLevel = function ( frag ) { var decreaseBlockQuoteLevel = function ( frag ) {
var root = this._root;
var blockquotes = frag.querySelectorAll( 'blockquote' ); var blockquotes = frag.querySelectorAll( 'blockquote' );
Array.prototype.filter.call( blockquotes, function ( el ) { Array.prototype.filter.call( blockquotes, function ( el ) {
return !getNearest( el.parentNode, 'BLOCKQUOTE' ); return !getNearest( el.parentNode, root, 'BLOCKQUOTE' );
}).forEach( function ( el ) { }).forEach( function ( el ) {
replaceWith( el, empty( el ) ); replaceWith( el, empty( el ) );
}); });
@ -1206,7 +1191,7 @@ var removeBlockQuote = function (/* frag */) {
}; };
var makeList = function ( self, frag, type ) { var makeList = function ( self, frag, type ) {
var walker = getBlockWalker( frag ), var walker = getBlockWalker( frag, self._root ),
node, tag, prev, newLi, node, tag, prev, newLi,
tagAttributes = self._config.tagAttributes, tagAttributes = self._config.tagAttributes,
listAttrs = tagAttributes[ type.toLowerCase() ], listAttrs = tagAttributes[ type.toLowerCase() ],
@ -1269,7 +1254,7 @@ var removeList = function ( frag ) {
child = children[ll]; child = children[ll];
replaceWith( child, empty( child ) ); replaceWith( child, empty( child ) );
} }
fixContainer( listFrag ); fixContainer( listFrag, this._root );
replaceWith( list, listFrag ); replaceWith( list, listFrag );
} }
return frag; return frag;
@ -1305,6 +1290,7 @@ var increaseListLevel = function ( frag ) {
}; };
var decreaseListLevel = function ( frag ) { var decreaseListLevel = function ( frag ) {
var root = this._root;
var items = frag.querySelectorAll( 'LI' ); var items = frag.querySelectorAll( 'LI' );
Array.prototype.filter.call( items, function ( el ) { Array.prototype.filter.call( items, function ( el ) {
return !isContainer( el.firstChild ); return !isContainer( el.firstChild );
@ -1315,7 +1301,7 @@ var decreaseListLevel = function ( frag ) {
node = first, node = first,
next; next;
if ( item.previousSibling ) { if ( item.previousSibling ) {
parent = split( parent, item, newParent ); parent = split( parent, item, newParent, root );
} }
while ( node ) { while ( node ) {
next = node.nextSibling; next = node.nextSibling;
@ -1326,7 +1312,7 @@ var decreaseListLevel = function ( frag ) {
node = next; node = next;
} }
if ( newParent.nodeName === 'LI' && first.previousSibling ) { if ( newParent.nodeName === 'LI' && first.previousSibling ) {
split( newParent, first, newParent.parentNode ); split( newParent, first, newParent.parentNode, root );
} }
while ( item !== frag && !item.childNodes.length ) { while ( item !== frag && !item.childNodes.length ) {
parent = item.parentNode; parent = item.parentNode;
@ -1334,16 +1320,16 @@ var decreaseListLevel = function ( frag ) {
item = parent; item = parent;
} }
}, this ); }, this );
fixContainer( frag ); fixContainer( frag, root );
return frag; return frag;
}; };
proto._ensureBottomLine = function () { proto._ensureBottomLine = function () {
var body = this._body, var root = this._root;
last = body.lastElementChild; var last = root.lastElementChild;
if ( !last || if ( !last ||
last.nodeName !== this._config.blockTag || !isBlock( 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 --- // --- Get/Set data ---
proto._getHTML = function () { proto._getHTML = function () {
return this._body.innerHTML; return this._root.innerHTML;
}; };
proto._setHTML = function ( html ) { proto._setHTML = function ( html ) {
var node = this._body; var root = this._root;
var node = root;
node.innerHTML = html; node.innerHTML = html;
do { do {
fixCursor( node ); fixCursor( node, root );
} while ( node = getNextBlock( node ) ); } while ( node = getNextBlock( node, root ) );
this._ignoreChange = true; this._ignoreChange = true;
}; };
proto.getHTML = function ( withBookMark ) { proto.getHTML = function ( withBookMark ) {
var brs = [], var brs = [],
node, fixer, html, l, range; root, node, fixer, html, l, range;
if ( withBookMark && ( range = this.getSelection() ) ) { if ( withBookMark && ( range = this.getSelection() ) ) {
this._saveRangeToBookmark( range ); this._saveRangeToBookmark( range );
} }
if ( useTextFixer ) { if ( useTextFixer ) {
node = this._body; root = this._root;
while ( node = getNextBlock( node ) ) { node = root;
while ( node = getNextBlock( node, root ) ) {
if ( !node.textContent && !node.querySelector( 'BR' ) ) { if ( !node.textContent && !node.querySelector( 'BR' ) ) {
fixer = this.createElement( 'BR' ); fixer = this.createElement( 'BR' );
node.appendChild( fixer ); node.appendChild( fixer );
@ -1399,37 +1387,37 @@ proto.getHTML = function ( withBookMark ) {
}; };
proto.setHTML = function ( html ) { proto.setHTML = function ( html ) {
var frag = this._doc.createDocumentFragment(), var frag = this._doc.createDocumentFragment();
div = this.createElement( 'DIV' ), var div = this.createElement( 'DIV' );
child; var root = this._root;
var child;
// Parse HTML into DOM tree // Parse HTML into DOM tree
div.innerHTML = html; div.innerHTML = html;
frag.appendChild( empty( div ) ); frag.appendChild( empty( div ) );
cleanTree( frag ); cleanTree( frag );
cleanupBRs( frag ); cleanupBRs( frag, root );
fixContainer( frag ); fixContainer( frag, root );
// Fix cursor // Fix cursor
var node = frag; var node = frag;
while ( node = getNextBlock( node ) ) { while ( node = getNextBlock( node, root ) ) {
fixCursor( node ); fixCursor( node, root );
} }
// Don't fire an input event // Don't fire an input event
this._ignoreChange = true; this._ignoreChange = true;
// Remove existing body children // Remove existing root children
var body = this._body; while ( child = root.lastChild ) {
while ( child = body.lastChild ) { root.removeChild( child );
body.removeChild( child );
} }
// And insert new content // And insert new content
body.appendChild( frag ); root.appendChild( frag );
fixCursor( body ); fixCursor( root, root );
// Reset the undo stack // Reset the undo stack
this._undoIndex = -1; this._undoIndex = -1;
@ -1439,17 +1427,13 @@ proto.setHTML = function ( html ) {
// Record undo state // Record undo state
var range = this._getRangeAndRemoveBookmark() || var range = this._getRangeAndRemoveBookmark() ||
this._createRange( body.firstChild, 0 ); this._createRange( root.firstChild, 0 );
this.saveUndoState( range ); this.saveUndoState( range );
// IE will also set focus when selecting text so don't use // IE will also set focus when selecting text so don't use
// setSelection. Instead, just store it in lastSelection, so if // setSelection. Instead, just store it in lastSelection, so if
// anything calls getSelection before first focus, we have a range // anything calls getSelection before first focus, we have a range
// to return. // to return.
if ( losesSelectionOnBlur ) { this._lastSelection = range;
this._lastSelection = range;
} else {
this.setSelection( range );
}
this._updatePath( range, true ); this._updatePath( range, true );
return this; return this;
@ -1463,25 +1447,25 @@ proto.insertElement = function ( el, range ) {
range.setStartAfter( el ); range.setStartAfter( el );
} else { } else {
// Get containing block node. // Get containing block node.
var body = this._body, var root = this._root;
splitNode = getStartBlockOfRange( range ) || body, var splitNode = getStartBlockOfRange( range, root ) || root;
parent, nodeAfterSplit; var parent, nodeAfterSplit;
// While at end of container node, move up DOM tree. // While at end of container node, move up DOM tree.
while ( splitNode !== body && !splitNode.nextSibling ) { while ( splitNode !== root && !splitNode.nextSibling ) {
splitNode = splitNode.parentNode; splitNode = splitNode.parentNode;
} }
// If in the middle of a container node, split up to body. // If in the middle of a container node, split up to root.
if ( splitNode !== body ) { if ( splitNode !== root ) {
parent = splitNode.parentNode; parent = splitNode.parentNode;
nodeAfterSplit = split( parent, splitNode.nextSibling, body ); nodeAfterSplit = split( parent, splitNode.nextSibling, root, root );
} }
if ( nodeAfterSplit ) { if ( nodeAfterSplit ) {
body.insertBefore( el, nodeAfterSplit ); root.insertBefore( el, nodeAfterSplit );
} else { } else {
body.appendChild( el ); root.appendChild( el );
// Insert blank line below block. // Insert blank line below block.
nodeAfterSplit = this.createDefaultBlock(); nodeAfterSplit = this.createDefaultBlock();
body.appendChild( nodeAfterSplit ); root.appendChild( nodeAfterSplit );
} }
range.setStart( nodeAfterSplit, 0 ); range.setStart( nodeAfterSplit, 0 );
range.setEnd( 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 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, var doc = frag.ownerDocument,
walker = new TreeWalker( frag, SHOW_TEXT, walker = new TreeWalker( frag, SHOW_TEXT,
function ( node ) { function ( node ) {
return !getNearest( node, 'A' ); return !getNearest( node, root, 'A' );
}, false ), }, false ),
node, data, parent, match, index, endIndex, child; node, data, parent, match, index, endIndex, child;
while ( node = walker.nextNode() ) { while ( node = walker.nextNode() ) {
@ -1549,6 +1533,7 @@ proto.insertHTML = function ( html, isPaste ) {
this.saveUndoState( range ); this.saveUndoState( range );
try { try {
var root = this._root;
var node = frag; var node = frag;
var event = { var event = {
fragment: frag, fragment: frag,
@ -1558,14 +1543,14 @@ proto.insertHTML = function ( html, isPaste ) {
defaultPrevented: false defaultPrevented: false
}; };
addLinks( frag ); addLinks( frag, root );
cleanTree( frag ); cleanTree( frag );
cleanupBRs( frag ); cleanupBRs( frag, root );
removeEmptyInlines( frag ); removeEmptyInlines( frag );
frag.normalize(); frag.normalize();
while ( node = getNextBlock( node ) ) { while ( node = getNextBlock( node, root ) ) {
fixCursor( node ); fixCursor( node, root );
} }
if ( isPaste ) { if ( isPaste ) {
@ -1573,7 +1558,7 @@ proto.insertHTML = function ( html, isPaste ) {
} }
if ( !event.defaultPrevented ) { if ( !event.defaultPrevented ) {
insertTreeFragmentIntoRange( range, event.fragment ); insertTreeFragmentIntoRange( range, event.fragment, root );
if ( !canObserveMutations ) { if ( !canObserveMutations ) {
this._docWasChanged(); this._docWasChanged();
} }
@ -1778,13 +1763,14 @@ proto.removeAllFormatting = function ( range ) {
return this; return this;
} }
var root = this._root;
var stopNode = range.commonAncestorContainer; var stopNode = range.commonAncestorContainer;
while ( stopNode && !isBlock( stopNode ) ) { while ( stopNode && !isBlock( stopNode ) ) {
stopNode = stopNode.parentNode; stopNode = stopNode.parentNode;
} }
if ( !stopNode ) { if ( !stopNode ) {
expandRangeToBlockBoundaries( range ); expandRangeToBlockBoundaries( range, root );
stopNode = this._body; stopNode = root;
} }
if ( stopNode.nodeType === TEXT_NODE ) { if ( stopNode.nodeType === TEXT_NODE ) {
return this; return this;
@ -1797,7 +1783,7 @@ proto.removeAllFormatting = function ( range ) {
moveRangeBoundariesUpTree( range, stopNode ); moveRangeBoundariesUpTree( range, stopNode );
// Split the selection up to the block, or if whole selection in same // 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 doc = stopNode.ownerDocument;
var startContainer = range.startContainer; var startContainer = range.startContainer;
var startOffset = range.startOffset; var startOffset = range.startOffset;
@ -1808,8 +1794,8 @@ proto.removeAllFormatting = function ( range ) {
// in same container. // in same container.
var formattedNodes = doc.createDocumentFragment(); var formattedNodes = doc.createDocumentFragment();
var cleanNodes = doc.createDocumentFragment(); var cleanNodes = doc.createDocumentFragment();
var nodeAfterSplit = split( endContainer, endOffset, stopNode ); var nodeAfterSplit = split( endContainer, endOffset, stopNode, root );
var nodeInSplit = split( startContainer, startOffset, stopNode ); var nodeInSplit = split( startContainer, startOffset, stopNode, root );
var nextNode, _range, childNodes; var nextNode, _range, childNodes;
// Then replace contents in split with a cleaned version of the same: // 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. // Record undo checkpoint.
this.saveUndoState( range ); this.saveUndoState( range );
// Delete the selection // Delete the selection
deleteContentsOfRange( range ); deleteContentsOfRange( range, this._root );
this._ensureBottomLine(); this._ensureBottomLine();
this.setSelection( range ); this.setSelection( range );
this._updatePath( range, true ); this._updatePath( range, true );
@ -120,17 +120,17 @@ var afterDelete = function ( self, range ) {
parent.removeChild( node ); parent.removeChild( node );
// Fix cursor in block // Fix cursor in block
if ( !isBlock( parent ) ) { if ( !isBlock( parent ) ) {
parent = getPreviousBlock( parent ); parent = getPreviousBlock( parent, self._root );
} }
fixCursor( parent ); fixCursor( parent, self._root );
// Move cursor into text node // Move cursor into text node
moveRangeBoundariesDownTree( range ); moveRangeBoundariesDownTree( range );
} }
// If you delete the last character in the sole <div> in Chrome, // 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 // 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. // block.
if ( node.nodeName === 'BODY' && if ( node === self._root &&
( node = node.firstChild ) && node.nodeName === 'BR' ) { ( node = node.firstChild ) && node.nodeName === 'BR' ) {
detach( node ); detach( node );
} }
@ -144,6 +144,7 @@ var afterDelete = function ( self, range ) {
var keyHandlers = { var keyHandlers = {
enter: function ( self, event, range ) { enter: function ( self, event, range ) {
var root = self._root;
var block, parent, nodeAfterSplit; var block, parent, nodeAfterSplit;
// We handle this ourselves // We handle this ourselves
@ -160,10 +161,10 @@ var keyHandlers = {
// Selected text is overwritten, therefore delete the contents // Selected text is overwritten, therefore delete the contents
// to collapse selection. // to collapse selection.
if ( !range.collapsed ) { 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; // If this is a malformed bit of document or in a table;
// just play it safe and insert a <br>. // 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 in a list, we'll split the LI instead.
if ( parent = getNearest( block, 'LI' ) ) { if ( parent = getNearest( block, root, 'LI' ) ) {
block = parent; block = parent;
} }
if ( !block.textContent ) { if ( !block.textContent ) {
// Break list // Break list
if ( getNearest( block, 'UL' ) || getNearest( block, 'OL' ) ) { if ( getNearest( block, root, 'UL' ) ||
getNearest( block, root, 'OL' ) ) {
return self.modifyBlocks( decreaseListLevel, range ); return self.modifyBlocks( decreaseListLevel, range );
} }
// Break blockquote // Break blockquote
else if ( getNearest( block, 'BLOCKQUOTE' ) ) { else if ( getNearest( block, root, 'BLOCKQUOTE' ) ) {
return self.modifyBlocks( removeBlockQuote, range ); return self.modifyBlocks( removeBlockQuote, range );
} }
} }
@ -199,7 +201,7 @@ var keyHandlers = {
// block // block
removeZWS( block ); removeZWS( block );
removeEmptyInlines( block ); removeEmptyInlines( block );
fixCursor( block ); fixCursor( block, root );
// Focus cursor // Focus cursor
// If there's a <b>/<i> etc. at the beginning of the split // If there's a <b>/<i> etc. at the beginning of the split
@ -242,27 +244,28 @@ var keyHandlers = {
self._updatePath( range, true ); self._updatePath( range, true );
}, },
backspace: function ( self, event, range ) { backspace: function ( self, event, range ) {
var root = self._root;
self._removeZWS(); self._removeZWS();
// Record undo checkpoint. // Record undo checkpoint.
self.saveUndoState( range ); self.saveUndoState( range );
// If not collapsed, delete contents // If not collapsed, delete contents
if ( !range.collapsed ) { if ( !range.collapsed ) {
event.preventDefault(); event.preventDefault();
deleteContentsOfRange( range ); deleteContentsOfRange( range, root );
afterDelete( self, range ); afterDelete( self, range );
} }
// If at beginning of block, merge with previous // If at beginning of block, merge with previous
else if ( rangeDoesStartAtBlockBoundary( range ) ) { else if ( rangeDoesStartAtBlockBoundary( range, root ) ) {
event.preventDefault(); event.preventDefault();
var current = getStartBlockOfRange( range ); var current = getStartBlockOfRange( range, root );
var previous; var previous;
if ( !current ) { if ( !current ) {
return; return;
} }
// In case inline data has somehow got between blocks. // In case inline data has somehow got between blocks.
fixContainer( current.parentNode ); fixContainer( current.parentNode, root );
// Now get previous block // Now get previous block
previous = getPreviousBlock( current ); previous = getPreviousBlock( current, root );
// Must not be at the very beginning of the text area. // Must not be at the very beginning of the text area.
if ( previous ) { if ( previous ) {
// If not editable, just delete whole block. // If not editable, just delete whole block.
@ -279,7 +282,7 @@ var keyHandlers = {
current = current.parentNode; current = current.parentNode;
} }
if ( current && ( current = current.nextSibling ) ) { if ( current && ( current = current.nextSibling ) ) {
mergeContainers( current ); mergeContainers( current, root );
} }
self.setSelection( range ); self.setSelection( range );
} }
@ -287,12 +290,12 @@ var keyHandlers = {
// to break lists/blockquote. // to break lists/blockquote.
else if ( current ) { else if ( current ) {
// Break list // Break list
if ( getNearest( current, 'UL' ) || if ( getNearest( current, root, 'UL' ) ||
getNearest( current, 'OL' ) ) { getNearest( current, root, 'OL' ) ) {
return self.modifyBlocks( decreaseListLevel, range ); return self.modifyBlocks( decreaseListLevel, range );
} }
// Break blockquote // Break blockquote
else if ( getNearest( current, 'BLOCKQUOTE' ) ) { else if ( getNearest( current, root, 'BLOCKQUOTE' ) ) {
return self.modifyBlocks( decreaseBlockQuoteLevel, range ); return self.modifyBlocks( decreaseBlockQuoteLevel, range );
} }
self.setSelection( range ); self.setSelection( range );
@ -307,6 +310,7 @@ var keyHandlers = {
} }
}, },
'delete': function ( self, event, range ) { 'delete': function ( self, event, range ) {
var root = self._root;
var current, next, originalRange, var current, next, originalRange,
cursorContainer, cursorOffset, nodeAfterCursor; cursorContainer, cursorOffset, nodeAfterCursor;
self._removeZWS(); self._removeZWS();
@ -315,20 +319,20 @@ var keyHandlers = {
// If not collapsed, delete contents // If not collapsed, delete contents
if ( !range.collapsed ) { if ( !range.collapsed ) {
event.preventDefault(); event.preventDefault();
deleteContentsOfRange( range ); deleteContentsOfRange( range, root );
afterDelete( self, range ); afterDelete( self, range );
} }
// If at end of block, merge next into this block // If at end of block, merge next into this block
else if ( rangeDoesEndAtBlockBoundary( range ) ) { else if ( rangeDoesEndAtBlockBoundary( range, root ) ) {
event.preventDefault(); event.preventDefault();
current = getStartBlockOfRange( range ); current = getStartBlockOfRange( range, root );
if ( !current ) { if ( !current ) {
return; return;
} }
// In case inline data has somehow got between blocks. // In case inline data has somehow got between blocks.
fixContainer( current.parentNode ); fixContainer( current.parentNode, root );
// Now get next block // Now get next block
next = getNextBlock( current ); next = getNextBlock( current, root );
// Must not be at the very end of the text area. // Must not be at the very end of the text area.
if ( next ) { if ( next ) {
// If not editable, just delete whole block. // If not editable, just delete whole block.
@ -345,7 +349,7 @@ var keyHandlers = {
next = next.parentNode; next = next.parentNode;
} }
if ( next && ( next = next.nextSibling ) ) { if ( next && ( next = next.nextSibling ) ) {
mergeContainers( next ); mergeContainers( next, root );
} }
self.setSelection( range ); self.setSelection( range );
self._updatePath( range, true ); self._updatePath( range, true );
@ -358,7 +362,7 @@ var keyHandlers = {
// delete it ourselves, because the browser won't if it is not // delete it ourselves, because the browser won't if it is not
// inline. // inline.
originalRange = range.cloneRange(); originalRange = range.cloneRange();
moveRangeBoundariesUpTree( range, self._body ); moveRangeBoundariesUpTree( range, self._root );
cursorContainer = range.endContainer; cursorContainer = range.endContainer;
cursorOffset = range.endOffset; cursorOffset = range.endOffset;
if ( cursorContainer.nodeType === ELEMENT_NODE ) { if ( cursorContainer.nodeType === ELEMENT_NODE ) {
@ -376,11 +380,12 @@ var keyHandlers = {
} }
}, },
tab: function ( self, event, range ) { tab: function ( self, event, range ) {
var root = self._root;
var node, parent; var node, parent;
self._removeZWS(); self._removeZWS();
// If no selection and at start of block // If no selection and at start of block
if ( range.collapsed && rangeDoesStartAtBlockBoundary( range ) ) { if ( range.collapsed && rangeDoesStartAtBlockBoundary( range, root ) ) {
node = getStartBlockOfRange( range ); node = getStartBlockOfRange( range, root );
// Iterate through the block's parents // Iterate through the block's parents
while ( parent = node.parentNode ) { while ( parent = node.parentNode ) {
// If we find a UL or OL (so are in a list, node must be an LI) // 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 ) { 'shift-tab': function ( self, event, range ) {
var root = self._root;
var node;
self._removeZWS(); self._removeZWS();
// If no selection and at start of block // If no selection and at start of block
if ( range.collapsed && rangeDoesStartAtBlockBoundary( range ) ) { if ( range.collapsed && rangeDoesStartAtBlockBoundary( range, root ) ) {
// Break list // Break list
var node = range.startContainer; node = range.startContainer;
if ( getNearest( node, 'UL' ) || getNearest( node, 'OL' ) ) { if ( getNearest( node, root, 'UL' ) ||
getNearest( node, root, 'OL' ) ) {
event.preventDefault(); event.preventDefault();
self.modifyBlocks( decreaseListLevel, range ); 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 ) { function isLeaf ( node ) {
return node.nodeType === ELEMENT_NODE && return node.nodeType === ELEMENT_NODE &&
!!leafNodeNames[ node.nodeName ]; !!leafNodeNames[ node.nodeName ];
@ -59,48 +38,80 @@ function isContainer ( node ) {
!isInline( node ) && !isBlock( node ); !isInline( node ) && !isBlock( node );
} }
function getBlockWalker ( node ) { function getBlockWalker ( node, root ) {
var doc = node.ownerDocument, var walker = new TreeWalker( root, SHOW_ELEMENT, isBlock, false );
walker = new TreeWalker(
doc.body, SHOW_ELEMENT, isBlock, false );
walker.currentNode = node; walker.currentNode = node;
return walker; 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 ) { function areAlike ( node, node2 ) {
return getBlockWalker( node ).previousNode(); 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 ) { function hasTagAttributes ( node, tag, attributes ) {
return getBlockWalker( node ).nextNode(); 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 ) { function getNearest ( node, root, tag, attributes ) {
do { while ( node && node !== root ) {
if ( hasTagAttributes( node, tag, attributes ) ) { if ( hasTagAttributes( node, tag, attributes ) ) {
return node; return node;
} }
} while ( node = node.parentNode ); node = node.parentNode;
}
return null; 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, var parent = node.parentNode,
path, id, className, classNames, dir; path, id, className, classNames, dir;
if ( !parent || node.nodeType !== ELEMENT_NODE ) { if ( node === root ) {
path = parent ? getPath( parent ) : ''; path = '';
} else { } else {
path = getPath( parent ); path = getPath( parent, root );
path += ( path ? '>' : '' ) + node.nodeName; if ( node.nodeType === ELEMENT_NODE ) {
if ( id = node.id ) { path += ( path ? '>' : '' ) + node.nodeName;
path += '#' + id; if ( id = node.id ) {
} path += '#' + id;
if ( className = node.className.trim() ) { }
classNames = className.split( /\s\s*/ ); if ( className = node.className.trim() ) {
classNames.sort(); classNames = className.split( /\s\s*/ );
path += '.'; classNames.sort();
path += classNames.join( '.' ); path += '.';
} path += classNames.join( '.' );
if ( dir = node.dir ) { }
path += '[dir=' + dir + ']'; if ( dir = node.dir ) {
path += '[dir=' + dir + ']';
}
} }
} }
return path; return path;
@ -158,16 +169,16 @@ function createElement ( doc, tag, props, children ) {
return el; return el;
} }
function fixCursor ( node ) { function fixCursor ( node, root ) {
// In Webkit and Gecko, block level elements are collapsed and // In Webkit and Gecko, block level elements are collapsed and
// unfocussable if they have no content. To remedy this, a <BR> must be // 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 // inserted. In Opera and IE, we just need a textnode in order for the
// cursor to appear. // cursor to appear.
var doc = node.ownerDocument, var doc = node.ownerDocument,
root = node, originalNode = node,
fixer, child; fixer, child;
if ( node.nodeName === 'BODY' ) { if ( node === root ) {
if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) { if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) {
fixer = getSquireInstance( doc ).createDefaultBlock(); fixer = getSquireInstance( doc ).createDefaultBlock();
if ( child ) { if ( child ) {
@ -227,11 +238,11 @@ function fixCursor ( node ) {
node.appendChild( fixer ); node.appendChild( fixer );
} }
return root; return originalNode;
} }
// Recursively examine container nodes and wrap any inline children. // Recursively examine container nodes and wrap any inline children.
function fixContainer ( container ) { function fixContainer ( container, root ) {
var children = container.childNodes, var children = container.childNodes,
doc = container.ownerDocument, doc = container.ownerDocument,
wrapper = null, wrapper = null,
@ -254,7 +265,7 @@ function fixContainer ( container ) {
wrapper = createElement( doc, wrapper = createElement( doc,
config.blockTag, config.blockAttributes ); config.blockTag, config.blockAttributes );
} }
fixCursor( wrapper ); fixCursor( wrapper, root );
if ( isBR ) { if ( isBR ) {
container.replaceChild( wrapper, child ); container.replaceChild( wrapper, child );
} else { } else {
@ -265,20 +276,21 @@ function fixContainer ( container ) {
wrapper = null; wrapper = null;
} }
if ( isContainer( child ) ) { if ( isContainer( child ) ) {
fixContainer( child ); fixContainer( child, root );
} }
} }
if ( wrapper ) { if ( wrapper ) {
container.appendChild( fixCursor( wrapper ) ); container.appendChild( fixCursor( wrapper, root ) );
} }
return container; return container;
} }
function split ( node, offset, stopNode ) { function split ( node, offset, stopNode, root ) {
var nodeType = node.nodeType, var nodeType = node.nodeType,
parent, clone, next; parent, clone, next;
if ( nodeType === TEXT_NODE && node !== stopNode ) { 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 ( nodeType === ELEMENT_NODE ) {
if ( typeof( offset ) === 'number' ) { if ( typeof( offset ) === 'number' ) {
@ -301,7 +313,8 @@ function split ( node, offset, stopNode ) {
} }
// Maintain li numbering if inside a quote. // 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; 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! // of a node lower down the tree!
// We need something in the element in order for the cursor to appear. // We need something in the element in order for the cursor to appear.
fixCursor( node ); fixCursor( node, root );
fixCursor( clone ); fixCursor( clone, root );
// Inject clone after original node // Inject clone after original node
if ( next = node.nextSibling ) { if ( next = node.nextSibling ) {
@ -320,7 +333,7 @@ function split ( node, offset, stopNode ) {
} }
// Keep on splitting up the tree // Keep on splitting up the tree
return split( parent, clone, stopNode ); return split( parent, clone, stopNode, root );
} }
return offset; return offset;
} }
@ -425,7 +438,7 @@ function mergeWithBlock ( block, next, range ) {
} }
} }
function mergeContainers ( node ) { function mergeContainers ( node, root ) {
var prev = node.previousSibling, var prev = node.previousSibling,
first = node.firstChild, first = node.firstChild,
doc = node.ownerDocument, doc = node.ownerDocument,
@ -451,14 +464,14 @@ function mergeContainers ( node ) {
needsFix = !isContainer( node ); needsFix = !isContainer( node );
prev.appendChild( empty( node ) ); prev.appendChild( empty( node ) );
if ( needsFix ) { if ( needsFix ) {
fixContainer( prev ); fixContainer( prev, root );
} }
if ( first ) { if ( first ) {
mergeContainers( first ); mergeContainers( first, root );
} }
} else if ( isListItem ) { } else if ( isListItem ) {
prev = createElement( doc, 'DIV' ); prev = createElement( doc, 'DIV' );
node.insertBefore( prev, first ); node.insertBefore( prev, first );
fixCursor( prev ); fixCursor( prev, root );
} }
} }

View file

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