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:
parent
e133f26db1
commit
6a348e084b
10 changed files with 643 additions and 665 deletions
124
Demo.html
124
Demo.html
|
@ -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
|
@ -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 );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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;
|
||||
|
|
256
source/Editor.js
256
source/Editor.js
|
@ -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:
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
|
145
source/Node.js
145
source/Node.js
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
Loading…
Reference in a new issue