mirror of
https://github.com/fastmail/Squire.git
synced 2025-01-03 05:00:13 -05:00
Replace Node prototype extensions with normal fns.
* Firefox was sometimes not finding the extensions on elements. * This minifies to a smaller target.
This commit is contained in:
parent
10abc3faf8
commit
af1720282c
11 changed files with 6130 additions and 2951 deletions
5
Makefile
5
Makefile
|
@ -11,8 +11,11 @@ build/ie8.js: source/ie8types.js source/ie8dom.js source/ie8range.js
|
||||||
mkdir -p $(@D)
|
mkdir -p $(@D)
|
||||||
uglifyjs $^ -c -m -o $@
|
uglifyjs $^ -c -m -o $@
|
||||||
|
|
||||||
build/squire.js: source/UA.js source/TreeWalker.js source/Node.js source/Range.js source/Editor.js
|
build/squire-raw.js: source/intro.js source/Constants.js source/TreeWalker.js source/Node.js source/Range.js source/Editor.js source/outro.js
|
||||||
mkdir -p $(@D)
|
mkdir -p $(@D)
|
||||||
|
cat $^ >$@
|
||||||
|
|
||||||
|
build/squire.js: build/squire-raw.js
|
||||||
uglifyjs $^ -c -m -o $@
|
uglifyjs $^ -c -m -o $@
|
||||||
|
|
||||||
build/document.html: source/document.html
|
build/document.html: source/document.html
|
||||||
|
|
3200
build/squire-raw.js
Normal file
3200
build/squire-raw.js
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
33
source/Constants.js
Normal file
33
source/Constants.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/*global doc, navigator */
|
||||||
|
|
||||||
|
var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING
|
||||||
|
var ELEMENT_NODE = 1; // Node.ELEMENT_NODE;
|
||||||
|
var TEXT_NODE = 3; // Node.TEXT_NODE;
|
||||||
|
var SHOW_ELEMENT = 1; // NodeFilter.SHOW_ELEMENT;
|
||||||
|
var SHOW_TEXT = 4; // NodeFilter.SHOW_TEXT;
|
||||||
|
var FILTER_ACCEPT = 1; // NodeFilter.FILTER_ACCEPT;
|
||||||
|
var FILTER_SKIP = 3; // NodeFilter.FILTER_SKIP;
|
||||||
|
|
||||||
|
var START_TO_START = 0; // Range.START_TO_START
|
||||||
|
var START_TO_END = 1; // Range.START_TO_END
|
||||||
|
var END_TO_END = 2; // Range.END_TO_END
|
||||||
|
var END_TO_START = 3; // Range.END_TO_START
|
||||||
|
|
||||||
|
var win = doc.defaultView;
|
||||||
|
var body = doc.body;
|
||||||
|
|
||||||
|
var ua = navigator.userAgent;
|
||||||
|
var isGecko = /Gecko\//.test( ua );
|
||||||
|
var isIE = /Trident\//.test( ua );
|
||||||
|
var isIE8 = ( win.ie === 8 );
|
||||||
|
var isIOS = /iP(?:ad|hone|od)/.test( ua );
|
||||||
|
var isOpera = !!win.opera;
|
||||||
|
var isWebKit = /WebKit\//.test( ua );
|
||||||
|
|
||||||
|
var useTextFixer = isIE || isOpera;
|
||||||
|
var cantFocusEmptyTextNodes = isIE || isWebKit;
|
||||||
|
var losesSelectionOnBlur = isIE;
|
||||||
|
|
||||||
|
var notWS = /\S/;
|
||||||
|
|
||||||
|
var indexOf = Array.prototype.indexOf;
|
4008
source/Editor.js
4008
source/Editor.js
File diff suppressed because it is too large
Load diff
708
source/Node.js
708
source/Node.js
|
@ -1,37 +1,23 @@
|
||||||
/* Copyright © 2011-2012 by Neil Jenkins. Licensed under the MIT license. */
|
/*global
|
||||||
|
ELEMENT_NODE,
|
||||||
|
TEXT_NODE,
|
||||||
|
SHOW_ELEMENT,
|
||||||
|
FILTER_ACCEPT,
|
||||||
|
FILTER_SKIP,
|
||||||
|
doc,
|
||||||
|
isOpera,
|
||||||
|
useTextFixer,
|
||||||
|
cantFocusEmptyTextNodes,
|
||||||
|
|
||||||
/*global Node, Text, Element, HTMLDocument, window, document,
|
TreeWalker,
|
||||||
editor, UA, DOMTreeWalker */
|
|
||||||
|
|
||||||
( function ( UA, TreeWalker ) {
|
Text,
|
||||||
|
|
||||||
"use strict";
|
setPlaceholderTextNode
|
||||||
|
*/
|
||||||
|
/*jshint strict:false */
|
||||||
|
|
||||||
var implement = function ( constructors, props ) {
|
var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TRONG|AMP)|U)$/;
|
||||||
var l = constructors.length,
|
|
||||||
proto, prop;
|
|
||||||
while ( l-- ) {
|
|
||||||
proto = constructors[l].prototype;
|
|
||||||
for ( prop in props ) {
|
|
||||||
proto[ prop ] = props[ prop ];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var every = function ( nodeList, fn ) {
|
|
||||||
var l = nodeList.length;
|
|
||||||
while ( l-- ) {
|
|
||||||
if ( !fn( nodeList[l] ) ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
var $False = function () { return false; };
|
|
||||||
var $True = function () { return true; };
|
|
||||||
|
|
||||||
var inlineNodeNames = /^(?:A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TRONG|AMP)|U)$/;
|
|
||||||
|
|
||||||
var leafNodeNames = {
|
var leafNodeNames = {
|
||||||
BR: 1,
|
BR: 1,
|
||||||
|
@ -39,305 +25,231 @@ var leafNodeNames = {
|
||||||
INPUT: 1
|
INPUT: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
var swap = function ( node, node2 ) {
|
function every ( nodeList, fn ) {
|
||||||
var parent = node2.parentNode;
|
var l = nodeList.length;
|
||||||
if ( parent ) {
|
while ( l-- ) {
|
||||||
parent.replaceChild( node, node2 );
|
if ( !fn( nodeList[l] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return node;
|
return true;
|
||||||
};
|
}
|
||||||
|
|
||||||
var ELEMENT_NODE = 1, // Node.ELEMENT_NODE,
|
// ---
|
||||||
TEXT_NODE = 3, // Node.TEXT_NODE,
|
|
||||||
SHOW_ELEMENT = 1, // NodeFilter.SHOW_ELEMENT,
|
|
||||||
FILTER_ACCEPT = 1, // NodeFilter.FILTER_ACCEPT,
|
|
||||||
FILTER_SKIP = 3; // NodeFilter.FILTER_SKIP;
|
|
||||||
|
|
||||||
var isBlock = function ( el ) {
|
function hasTagAttributes ( node, tag, attributes ) {
|
||||||
return el.isBlock() ? FILTER_ACCEPT : FILTER_SKIP;
|
if ( node.nodeName !== tag ) {
|
||||||
};
|
return false;
|
||||||
|
|
||||||
implement( window.Node ? [ Node ] : [ Text, Element, HTMLDocument ], {
|
|
||||||
isLeaf: $False,
|
|
||||||
isInline: $False,
|
|
||||||
isBlock: $False,
|
|
||||||
isContainer: $False,
|
|
||||||
getPath: function () {
|
|
||||||
var parent = this.parentNode;
|
|
||||||
return parent ? parent.getPath() : '';
|
|
||||||
},
|
|
||||||
detach: function () {
|
|
||||||
var parent = this.parentNode;
|
|
||||||
if ( parent ) {
|
|
||||||
parent.removeChild( this );
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
replaceWith: function ( node ) {
|
|
||||||
swap( node, this );
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
replaces: function ( node ) {
|
|
||||||
swap( this, node );
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
nearest: function ( tag, attributes ) {
|
|
||||||
var parent = this.parentNode;
|
|
||||||
return parent ? parent.nearest( tag, attributes ) : null;
|
|
||||||
},
|
|
||||||
getPreviousBlock: function () {
|
|
||||||
var doc = this.ownerDocument,
|
|
||||||
walker = new TreeWalker(
|
|
||||||
doc.body, SHOW_ELEMENT, isBlock, false );
|
|
||||||
walker.currentNode = this;
|
|
||||||
return walker.previousNode();
|
|
||||||
},
|
|
||||||
getNextBlock: function () {
|
|
||||||
var doc = this.ownerDocument,
|
|
||||||
walker = new TreeWalker(
|
|
||||||
doc.body, SHOW_ELEMENT, isBlock, false );
|
|
||||||
walker.currentNode = this;
|
|
||||||
return walker.nextNode();
|
|
||||||
},
|
|
||||||
split: function ( node, stopNode ) {
|
|
||||||
return node;
|
|
||||||
},
|
|
||||||
mergeContainers: function () {}
|
|
||||||
});
|
|
||||||
|
|
||||||
implement([ Text ], {
|
|
||||||
isInline: $True,
|
|
||||||
getLength: function () {
|
|
||||||
return this.length;
|
|
||||||
},
|
|
||||||
isLike: function ( node ) {
|
|
||||||
return node.nodeType === TEXT_NODE;
|
|
||||||
},
|
|
||||||
split: function ( offset, stopNode ) {
|
|
||||||
var node = this;
|
|
||||||
if ( node === stopNode ) {
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
return node.parentNode.split( node.splitText( offset ), stopNode );
|
|
||||||
}
|
}
|
||||||
});
|
for ( var attr in attributes ) {
|
||||||
|
if ( node.getAttribute( attr ) !== attributes[ attr ] ) {
|
||||||
implement([ Element ], {
|
return false;
|
||||||
isLeaf: function () {
|
|
||||||
return !!leafNodeNames[ this.nodeName ];
|
|
||||||
},
|
|
||||||
isInline: function () {
|
|
||||||
return inlineNodeNames.test( this.nodeName );
|
|
||||||
},
|
|
||||||
isBlock: function () {
|
|
||||||
return !this.isInline() && every( this.childNodes, function ( child ) {
|
|
||||||
return child.isInline();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
isContainer: function () {
|
|
||||||
return !this.isInline() && !this.isBlock();
|
|
||||||
},
|
|
||||||
getLength: function () {
|
|
||||||
return this.childNodes.length;
|
|
||||||
},
|
|
||||||
getPath: function () {
|
|
||||||
var parent = this.parentNode,
|
|
||||||
path, id, className, classNames;
|
|
||||||
if ( !parent ) {
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
path = parent.getPath();
|
}
|
||||||
path += ( path ? '>' : '' ) + this.nodeName;
|
return true;
|
||||||
if ( id = this.id ) {
|
}
|
||||||
|
function areAlike ( node, node2 ) {
|
||||||
|
return (
|
||||||
|
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 ];
|
||||||
|
}
|
||||||
|
function isInline ( node ) {
|
||||||
|
return inlineNodeNames.test( node.nodeName );
|
||||||
|
}
|
||||||
|
function isBlock ( node ) {
|
||||||
|
return node.nodeType === ELEMENT_NODE &&
|
||||||
|
!isInline( node ) && every( node.childNodes, isInline );
|
||||||
|
}
|
||||||
|
function isContainer ( node ) {
|
||||||
|
return node.nodeType === ELEMENT_NODE &&
|
||||||
|
!isInline( node ) && !isBlock( node );
|
||||||
|
}
|
||||||
|
|
||||||
|
function acceptIfBlock ( el ) {
|
||||||
|
return isBlock( el ) ? FILTER_ACCEPT : FILTER_SKIP;
|
||||||
|
}
|
||||||
|
function getBlockWalker ( node ) {
|
||||||
|
var doc = node.ownerDocument,
|
||||||
|
walker = new TreeWalker(
|
||||||
|
doc.body, SHOW_ELEMENT, acceptIfBlock, false );
|
||||||
|
walker.currentNode = node;
|
||||||
|
return walker;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreviousBlock ( node ) {
|
||||||
|
return getBlockWalker( node ).previousNode();
|
||||||
|
}
|
||||||
|
function getNextBlock ( node ) {
|
||||||
|
return getBlockWalker( node ).nextNode();
|
||||||
|
}
|
||||||
|
function getNearest ( node, tag, attributes ) {
|
||||||
|
do {
|
||||||
|
if ( hasTagAttributes( node, tag, attributes ) ) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
} while ( node = node.parentNode );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPath ( node ) {
|
||||||
|
var parent = node.parentNode,
|
||||||
|
path, id, className, classNames;
|
||||||
|
if ( !parent || node.nodeType !== ELEMENT_NODE ) {
|
||||||
|
path = parent ? getPath( parent ) : '';
|
||||||
|
} else {
|
||||||
|
path = getPath( parent );
|
||||||
|
path += ( path ? '>' : '' ) + node.nodeName;
|
||||||
|
if ( id = node.id ) {
|
||||||
path += '#' + id;
|
path += '#' + id;
|
||||||
}
|
}
|
||||||
if ( className = this.className.trim() ) {
|
if ( className = node.className.trim() ) {
|
||||||
classNames = className.split( /\s\s*/ );
|
classNames = className.split( /\s\s*/ );
|
||||||
classNames.sort();
|
classNames.sort();
|
||||||
path += '.';
|
path += '.';
|
||||||
path += classNames.join( '.' );
|
path += classNames.join( '.' );
|
||||||
}
|
}
|
||||||
return path;
|
}
|
||||||
},
|
return path;
|
||||||
wraps: function ( node ) {
|
}
|
||||||
swap( this, node ).appendChild( node );
|
|
||||||
return this;
|
function getLength ( node ) {
|
||||||
},
|
var nodeType = node.nodeType;
|
||||||
empty: function () {
|
return nodeType === ELEMENT_NODE ?
|
||||||
var frag = this.ownerDocument.createDocumentFragment(),
|
node.childNodes.length : node.length || 0;
|
||||||
l = this.childNodes.length;
|
}
|
||||||
while ( l-- ) {
|
|
||||||
frag.appendChild( this.firstChild );
|
function detach ( node ) {
|
||||||
|
var parent = node.parentNode;
|
||||||
|
if ( parent ) {
|
||||||
|
parent.removeChild( node );
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
function replaceWith ( node, node2 ) {
|
||||||
|
var parent = node.parentNode;
|
||||||
|
if ( parent ) {
|
||||||
|
parent.replaceChild( node2, node );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function empty ( node ) {
|
||||||
|
var frag = node.ownerDocument.createDocumentFragment(),
|
||||||
|
childNodes = node.childNodes,
|
||||||
|
l = childNodes ? childNodes.length : 0;
|
||||||
|
while ( l-- ) {
|
||||||
|
frag.appendChild( node.firstChild );
|
||||||
|
}
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixCursor ( node ) {
|
||||||
|
// 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,
|
||||||
|
fixer, child;
|
||||||
|
|
||||||
|
if ( node.nodeName === 'BODY' ) {
|
||||||
|
if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) {
|
||||||
|
fixer = doc.createElement( 'DIV' );
|
||||||
|
if ( child ) {
|
||||||
|
node.replaceChild( fixer, child );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
node.appendChild( fixer );
|
||||||
|
}
|
||||||
|
node = fixer;
|
||||||
|
fixer = null;
|
||||||
}
|
}
|
||||||
return frag;
|
}
|
||||||
},
|
|
||||||
is: function ( tag, attributes ) {
|
if ( isInline( node ) ) {
|
||||||
if ( this.nodeName !== tag ) { return false; }
|
if ( !node.firstChild ) {
|
||||||
var attr;
|
if ( cantFocusEmptyTextNodes ) {
|
||||||
for ( attr in attributes ) {
|
fixer = doc.createTextNode( '\u200B' );
|
||||||
if ( this.getAttribute( attr ) !== attributes[ attr ] ) {
|
setPlaceholderTextNode( fixer );
|
||||||
return false;
|
} else {
|
||||||
|
fixer = doc.createTextNode( '' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
} else {
|
||||||
},
|
if ( useTextFixer ) {
|
||||||
nearest: function ( tag, attributes ) {
|
while ( node.nodeType !== TEXT_NODE && !isLeaf( node ) ) {
|
||||||
var el = this;
|
child = node.firstChild;
|
||||||
do {
|
if ( !child ) {
|
||||||
if ( el.is( tag, attributes ) ) {
|
fixer = doc.createTextNode( '' );
|
||||||
return el;
|
break;
|
||||||
|
}
|
||||||
|
node = child;
|
||||||
}
|
}
|
||||||
} while ( ( el = el.parentNode ) &&
|
if ( node.nodeType === TEXT_NODE ) {
|
||||||
( el.nodeType === ELEMENT_NODE ) );
|
// Opera will collapse the block element if it contains
|
||||||
return null;
|
// just spaces (but not if it contains no data at all).
|
||||||
},
|
if ( /^ +$/.test( node.data ) ) {
|
||||||
isLike: function ( node ) {
|
node.data = '';
|
||||||
return (
|
|
||||||
node.nodeType === ELEMENT_NODE &&
|
|
||||||
node.nodeName === this.nodeName &&
|
|
||||||
node.className === this.className &&
|
|
||||||
node.style.cssText === this.style.cssText
|
|
||||||
);
|
|
||||||
},
|
|
||||||
mergeInlines: function ( range ) {
|
|
||||||
var children = this.childNodes,
|
|
||||||
l = children.length,
|
|
||||||
frags = [],
|
|
||||||
child, prev, len;
|
|
||||||
while ( l-- ) {
|
|
||||||
child = children[l];
|
|
||||||
prev = l && children[ l - 1 ];
|
|
||||||
if ( l && child.isInline() && child.isLike( prev ) &&
|
|
||||||
!leafNodeNames[ child.nodeName ] ) {
|
|
||||||
if ( range.startContainer === child ) {
|
|
||||||
range.startContainer = prev;
|
|
||||||
range.startOffset += prev.getLength();
|
|
||||||
}
|
}
|
||||||
if ( range.endContainer === child ) {
|
} else if ( isLeaf( node ) ) {
|
||||||
range.endContainer = prev;
|
node.parentNode.insertBefore( doc.createTextNode( '' ), node );
|
||||||
range.endOffset += prev.getLength();
|
|
||||||
}
|
|
||||||
if ( range.startContainer === this ) {
|
|
||||||
if ( range.startOffset > l ) {
|
|
||||||
range.startOffset -= 1;
|
|
||||||
}
|
|
||||||
else if ( range.startOffset === l ) {
|
|
||||||
range.startContainer = prev;
|
|
||||||
range.startOffset = prev.getLength();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( range.endContainer === this ) {
|
|
||||||
if ( range.endOffset > l ) {
|
|
||||||
range.endOffset -= 1;
|
|
||||||
}
|
|
||||||
else if ( range.endOffset === l ) {
|
|
||||||
range.endContainer = prev;
|
|
||||||
range.endOffset = prev.getLength();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
child.detach();
|
|
||||||
if ( child.nodeType === TEXT_NODE ) {
|
|
||||||
prev.appendData( child.data.replace( /\u200B/g, '' ) );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
frags.push( child.empty() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if ( child.nodeType === ELEMENT_NODE ) {
|
|
||||||
len = frags.length;
|
|
||||||
while ( len-- ) {
|
|
||||||
child.appendChild( frags.pop() );
|
|
||||||
}
|
|
||||||
child.mergeInlines( range );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
else if ( !node.querySelector( 'BR' ) ) {
|
||||||
mergeWithBlock: function ( next, range ) {
|
fixer = doc.createElement( 'BR' );
|
||||||
var block = this,
|
while ( ( child = node.lastElementChild ) && !isInline( child ) ) {
|
||||||
container = next,
|
node = child;
|
||||||
last, offset, _range;
|
|
||||||
while ( container.parentNode.childNodes.length === 1 ) {
|
|
||||||
container = container.parentNode;
|
|
||||||
}
|
|
||||||
container.detach();
|
|
||||||
|
|
||||||
offset = block.childNodes.length;
|
|
||||||
|
|
||||||
// Remove extra <BR> fixer if present.
|
|
||||||
last = block.lastChild;
|
|
||||||
if ( last && last.nodeName === 'BR' ) {
|
|
||||||
block.removeChild( last );
|
|
||||||
offset -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_range = {
|
|
||||||
startContainer: block,
|
|
||||||
startOffset: offset,
|
|
||||||
endContainer: block,
|
|
||||||
endOffset: offset
|
|
||||||
};
|
|
||||||
|
|
||||||
block.appendChild( next.empty() );
|
|
||||||
block.mergeInlines( _range );
|
|
||||||
|
|
||||||
range.setStart(
|
|
||||||
_range.startContainer, _range.startOffset );
|
|
||||||
range.collapse( true );
|
|
||||||
|
|
||||||
// Opera inserts a BR if you delete the last piece of text
|
|
||||||
// in a block-level element. Unfortunately, it then gets
|
|
||||||
// confused when setting the selection subsequently and
|
|
||||||
// refuses to accept the range that finishes just before the
|
|
||||||
// BR. Removing the BR fixes the bug.
|
|
||||||
// Steps to reproduce bug: Type "a-b-c" (where - is return)
|
|
||||||
// then backspace twice. The cursor goes to the top instead
|
|
||||||
// of after "b".
|
|
||||||
if ( window.opera && ( last = block.lastChild ) &&
|
|
||||||
last.nodeName === 'BR' ) {
|
|
||||||
block.removeChild( last );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mergeContainers: function () {
|
|
||||||
var prev = this.previousSibling,
|
|
||||||
first = this.firstChild;
|
|
||||||
if ( prev && prev.isLike( this ) && prev.isContainer() ) {
|
|
||||||
prev.appendChild( this.detach().empty() );
|
|
||||||
if ( first ) {
|
|
||||||
first.mergeContainers();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
split: function ( childNodeToSplitBefore, stopNode ) {
|
if ( fixer ) {
|
||||||
var node = this;
|
node.appendChild( fixer );
|
||||||
|
}
|
||||||
|
|
||||||
if ( typeof( childNodeToSplitBefore ) === 'number' ) {
|
return node;
|
||||||
childNodeToSplitBefore =
|
}
|
||||||
childNodeToSplitBefore < node.childNodes.length ?
|
|
||||||
node.childNodes[ childNodeToSplitBefore ] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
function split ( node, offset, stopNode ) {
|
||||||
|
var nodeType = node.nodeType,
|
||||||
|
parent, clone, next;
|
||||||
|
if ( nodeType === TEXT_NODE ) {
|
||||||
if ( node === stopNode ) {
|
if ( node === stopNode ) {
|
||||||
return childNodeToSplitBefore;
|
return offset;
|
||||||
|
}
|
||||||
|
return split( node.parentNode, node.splitText( offset ), stopNode );
|
||||||
|
}
|
||||||
|
if ( nodeType === ELEMENT_NODE ) {
|
||||||
|
if ( typeof( offset ) === 'number' ) {
|
||||||
|
offset = offset < node.childNodes.length ?
|
||||||
|
node.childNodes[ offset ] : null;
|
||||||
|
}
|
||||||
|
if ( node === stopNode ) {
|
||||||
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone node without children
|
// Clone node without children
|
||||||
var parent = node.parentNode,
|
parent = node.parentNode,
|
||||||
clone = node.cloneNode( false ),
|
clone = node.cloneNode( false );
|
||||||
next;
|
|
||||||
|
|
||||||
// Add right-hand siblings to the clone
|
// Add right-hand siblings to the clone
|
||||||
while ( childNodeToSplitBefore ) {
|
while ( offset ) {
|
||||||
next = childNodeToSplitBefore.nextSibling;
|
next = offset.nextSibling;
|
||||||
clone.appendChild( childNodeToSplitBefore );
|
clone.appendChild( offset );
|
||||||
childNodeToSplitBefore = next;
|
offset = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DO NOT NORMALISE. This may undo the fixCursor() call
|
// DO NOT NORMALISE. This may undo the fixCursor() call
|
||||||
// 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.
|
||||||
node.fixCursor();
|
fixCursor( node );
|
||||||
clone.fixCursor();
|
fixCursor( clone );
|
||||||
|
|
||||||
// Inject clone after original node
|
// Inject clone after original node
|
||||||
if ( next = node.nextSibling ) {
|
if ( next = node.nextSibling ) {
|
||||||
|
@ -347,74 +259,142 @@ implement([ Element ], {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep on splitting up the tree
|
// Keep on splitting up the tree
|
||||||
return parent.split( clone, stopNode );
|
return split( parent, clone, stopNode );
|
||||||
},
|
|
||||||
fixCursor: function () {
|
|
||||||
// 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 el = this,
|
|
||||||
doc = el.ownerDocument,
|
|
||||||
fixer, child;
|
|
||||||
|
|
||||||
if ( el.nodeName === 'BODY' ) {
|
|
||||||
if ( !( child = el.firstChild ) || child.nodeName === 'BR' ) {
|
|
||||||
fixer = doc.createElement( 'DIV' );
|
|
||||||
if ( child ) {
|
|
||||||
el.replaceChild( fixer, child );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
el.appendChild( fixer );
|
|
||||||
}
|
|
||||||
el = fixer;
|
|
||||||
fixer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( el.isInline() ) {
|
|
||||||
if ( !el.firstChild ) {
|
|
||||||
if ( UA.cantFocusEmptyTextNodes ) {
|
|
||||||
fixer = doc.createTextNode( '\u200B' );
|
|
||||||
editor._setPlaceholderTextNode( fixer );
|
|
||||||
} else {
|
|
||||||
fixer = doc.createTextNode( '' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ( UA.useTextFixer ) {
|
|
||||||
while ( el.nodeType !== TEXT_NODE && !el.isLeaf() ) {
|
|
||||||
child = el.firstChild;
|
|
||||||
if ( !child ) {
|
|
||||||
fixer = doc.createTextNode( '' );
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
el = child;
|
|
||||||
}
|
|
||||||
if ( el.nodeType === TEXT_NODE ) {
|
|
||||||
// Opera will collapse the block element if it contains
|
|
||||||
// just spaces (but not if it contains no data at all).
|
|
||||||
if ( /^ +$/.test( el.data ) ) {
|
|
||||||
el.data = '';
|
|
||||||
}
|
|
||||||
} else if ( el.isLeaf() ) {
|
|
||||||
el.parentNode.insertBefore( doc.createTextNode( '' ), el );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if ( !el.querySelector( 'BR' ) ) {
|
|
||||||
fixer = doc.createElement( 'BR' );
|
|
||||||
while ( ( child = el.lastElementChild ) && !child.isInline() ) {
|
|
||||||
el = child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( fixer ) {
|
|
||||||
el.appendChild( fixer );
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
});
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeInlines ( node, range ) {
|
||||||
|
if ( node.nodeType !== ELEMENT_NODE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var children = node.childNodes,
|
||||||
|
l = children.length,
|
||||||
|
frags = [],
|
||||||
|
child, prev, len;
|
||||||
|
while ( l-- ) {
|
||||||
|
child = children[l];
|
||||||
|
prev = l && children[ l - 1 ];
|
||||||
|
if ( l && isInline( child ) && areAlike( child, prev ) &&
|
||||||
|
!leafNodeNames[ child.nodeName ] ) {
|
||||||
|
if ( range.startContainer === child ) {
|
||||||
|
range.startContainer = prev;
|
||||||
|
range.startOffset += getLength( prev );
|
||||||
|
}
|
||||||
|
if ( range.endContainer === child ) {
|
||||||
|
range.endContainer = prev;
|
||||||
|
range.endOffset += getLength( prev );
|
||||||
|
}
|
||||||
|
if ( range.startContainer === node ) {
|
||||||
|
if ( range.startOffset > l ) {
|
||||||
|
range.startOffset -= 1;
|
||||||
|
}
|
||||||
|
else if ( range.startOffset === l ) {
|
||||||
|
range.startContainer = prev;
|
||||||
|
range.startOffset = getLength( prev );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( range.endContainer === node ) {
|
||||||
|
if ( range.endOffset > l ) {
|
||||||
|
range.endOffset -= 1;
|
||||||
|
}
|
||||||
|
else if ( range.endOffset === l ) {
|
||||||
|
range.endContainer = prev;
|
||||||
|
range.endOffset = getLength( prev );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
detach( child );
|
||||||
|
if ( child.nodeType === TEXT_NODE ) {
|
||||||
|
prev.appendData( child.data.replace( /\u200B/g, '' ) );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
frags.push( empty( child ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ( child.nodeType === ELEMENT_NODE ) {
|
||||||
|
len = frags.length;
|
||||||
|
while ( len-- ) {
|
||||||
|
child.appendChild( frags.pop() );
|
||||||
|
}
|
||||||
|
mergeInlines( child, range );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeWithBlock ( block, next, range ) {
|
||||||
|
var container = next,
|
||||||
|
last, offset, _range;
|
||||||
|
while ( container.parentNode.childNodes.length === 1 ) {
|
||||||
|
container = container.parentNode;
|
||||||
|
}
|
||||||
|
detach( container );
|
||||||
|
|
||||||
|
offset = block.childNodes.length;
|
||||||
|
|
||||||
|
// Remove extra <BR> fixer if present.
|
||||||
|
last = block.lastChild;
|
||||||
|
if ( last && last.nodeName === 'BR' ) {
|
||||||
|
block.removeChild( last );
|
||||||
|
offset -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_range = {
|
||||||
|
startContainer: block,
|
||||||
|
startOffset: offset,
|
||||||
|
endContainer: block,
|
||||||
|
endOffset: offset
|
||||||
|
};
|
||||||
|
|
||||||
|
block.appendChild( empty( next ) );
|
||||||
|
mergeInlines( block, _range );
|
||||||
|
|
||||||
|
range.setStart( _range.startContainer, _range.startOffset );
|
||||||
|
range.collapse( true );
|
||||||
|
|
||||||
|
// Opera inserts a BR if you delete the last piece of text
|
||||||
|
// in a block-level element. Unfortunately, it then gets
|
||||||
|
// confused when setting the selection subsequently and
|
||||||
|
// refuses to accept the range that finishes just before the
|
||||||
|
// BR. Removing the BR fixes the bug.
|
||||||
|
// Steps to reproduce bug: Type "a-b-c" (where - is return)
|
||||||
|
// then backspace twice. The cursor goes to the top instead
|
||||||
|
// of after "b".
|
||||||
|
if ( isOpera && ( last = block.lastChild ) && last.nodeName === 'BR' ) {
|
||||||
|
block.removeChild( last );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeContainers ( node ) {
|
||||||
|
var prev = node.previousSibling,
|
||||||
|
first = node.firstChild;
|
||||||
|
if ( prev && areAlike( prev, node ) && isContainer( prev ) ) {
|
||||||
|
detach( node );
|
||||||
|
prev.appendChild( empty( node ) );
|
||||||
|
if ( first ) {
|
||||||
|
mergeContainers( first );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createElement ( tag, props, children ) {
|
||||||
|
var el = doc.createElement( tag ),
|
||||||
|
attr, i, l;
|
||||||
|
if ( props instanceof Array ) {
|
||||||
|
children = props;
|
||||||
|
props = null;
|
||||||
|
}
|
||||||
|
if ( props ) {
|
||||||
|
for ( attr in props ) {
|
||||||
|
el.setAttribute( attr, props[ attr ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( children ) {
|
||||||
|
for ( i = 0, l = children.length; i < l; i += 1 ) {
|
||||||
|
el.appendChild( children[i] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
// Fix IE8/9's buggy implementation of Text#splitText.
|
// Fix IE8/9's buggy implementation of Text#splitText.
|
||||||
// If the split is at the end of the node, it doesn't insert the newly split
|
// If the split is at the end of the node, it doesn't insert the newly split
|
||||||
|
@ -423,8 +403,8 @@ implement([ Element ], {
|
||||||
// the document and replaced by another, rather than just having its data
|
// the document and replaced by another, rather than just having its data
|
||||||
// shortened.
|
// shortened.
|
||||||
if ( function () {
|
if ( function () {
|
||||||
var div = document.createElement( 'div' ),
|
var div = doc.createElement( 'div' ),
|
||||||
text = document.createTextNode( '12' );
|
text = doc.createTextNode( '12' );
|
||||||
div.appendChild( text );
|
div.appendChild( text );
|
||||||
text.splitText( 2 );
|
text.splitText( 2 );
|
||||||
return div.childNodes.length !== 2;
|
return div.childNodes.length !== 2;
|
||||||
|
@ -446,5 +426,3 @@ if ( function () {
|
||||||
return afterSplit;
|
return afterSplit;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}( UA, DOMTreeWalker ) );
|
|
||||||
|
|
933
source/Range.js
933
source/Range.js
|
@ -1,21 +1,30 @@
|
||||||
/* Copyright © 2011-2012 by Neil Jenkins. Licensed under the MIT license. */
|
/*global
|
||||||
|
ELEMENT_NODE,
|
||||||
|
TEXT_NODE,
|
||||||
|
SHOW_TEXT,
|
||||||
|
FILTER_ACCEPT,
|
||||||
|
START_TO_START,
|
||||||
|
START_TO_END,
|
||||||
|
END_TO_END,
|
||||||
|
END_TO_START,
|
||||||
|
indexOf,
|
||||||
|
|
||||||
/*global Range, DOMTreeWalker */
|
TreeWalker,
|
||||||
|
|
||||||
( function ( TreeWalker ) {
|
isLeaf,
|
||||||
|
isInline,
|
||||||
|
isBlock,
|
||||||
|
getPreviousBlock,
|
||||||
|
getNextBlock,
|
||||||
|
getLength,
|
||||||
|
fixCursor,
|
||||||
|
split,
|
||||||
|
mergeWithBlock,
|
||||||
|
mergeContainers,
|
||||||
|
|
||||||
"use strict";
|
Range
|
||||||
|
*/
|
||||||
var indexOf = Array.prototype.indexOf;
|
/*jshint strict:false */
|
||||||
|
|
||||||
var ELEMENT_NODE = 1, // Node.ELEMENT_NODE
|
|
||||||
TEXT_NODE = 3, // Node.TEXT_NODE
|
|
||||||
SHOW_TEXT = 4, // NodeFilter.SHOW_TEXT,
|
|
||||||
FILTER_ACCEPT = 1, // NodeFilter.FILTER_ACCEPT,
|
|
||||||
START_TO_START = 0, // Range.START_TO_START
|
|
||||||
START_TO_END = 1, // Range.START_TO_END
|
|
||||||
END_TO_END = 2, // Range.END_TO_END
|
|
||||||
END_TO_START = 3; // Range.END_TO_START
|
|
||||||
|
|
||||||
var getNodeBefore = function ( node, offset ) {
|
var getNodeBefore = function ( node, offset ) {
|
||||||
var children = node.childNodes;
|
var children = node.childNodes;
|
||||||
|
@ -42,482 +51,474 @@ var getNodeAfter = function ( node, offset ) {
|
||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
|
|
||||||
var RangePrototypeExtensions = {
|
var RangePrototype = Range.prototype;
|
||||||
|
|
||||||
forEachTextNode: function ( fn ) {
|
RangePrototype.forEachTextNode = function ( fn ) {
|
||||||
var range = this.cloneRange();
|
var range = this.cloneRange();
|
||||||
range.moveBoundariesDownTree();
|
range.moveBoundariesDownTree();
|
||||||
|
|
||||||
var startContainer = range.startContainer,
|
var startContainer = range.startContainer,
|
||||||
endContainer = range.endContainer,
|
endContainer = range.endContainer,
|
||||||
root = range.commonAncestorContainer,
|
root = range.commonAncestorContainer,
|
||||||
walker = new TreeWalker(
|
walker = new TreeWalker(
|
||||||
root, SHOW_TEXT, function ( node ) {
|
root, SHOW_TEXT, function ( node ) {
|
||||||
return FILTER_ACCEPT;
|
return FILTER_ACCEPT;
|
||||||
}, false ),
|
}, false ),
|
||||||
textnode = walker.currentNode = startContainer;
|
textnode = walker.currentNode = startContainer;
|
||||||
|
|
||||||
while ( !fn( textnode, range ) &&
|
while ( !fn( textnode, range ) &&
|
||||||
textnode !== endContainer &&
|
textnode !== endContainer &&
|
||||||
( textnode = walker.nextNode() ) ) {}
|
( textnode = walker.nextNode() ) ) {}
|
||||||
},
|
};
|
||||||
|
|
||||||
getTextContent: function () {
|
RangePrototype.getTextContent = function () {
|
||||||
var textContent = '';
|
var textContent = '';
|
||||||
this.forEachTextNode( function ( textnode, range ) {
|
this.forEachTextNode( function ( textnode, range ) {
|
||||||
var value = textnode.data;
|
var value = textnode.data;
|
||||||
if ( value && ( /\S/.test( value ) ) ) {
|
if ( value && ( /\S/.test( value ) ) ) {
|
||||||
if ( textnode === range.endContainer ) {
|
if ( textnode === range.endContainer ) {
|
||||||
value = value.slice( 0, range.endOffset );
|
value = value.slice( 0, range.endOffset );
|
||||||
}
|
|
||||||
if ( textnode === range.startContainer ) {
|
|
||||||
value = value.slice( range.startOffset );
|
|
||||||
}
|
|
||||||
textContent += value;
|
|
||||||
}
|
}
|
||||||
});
|
if ( textnode === range.startContainer ) {
|
||||||
return textContent;
|
value = value.slice( range.startOffset );
|
||||||
},
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
_insertNode: function ( node ) {
|
|
||||||
// Insert at start.
|
|
||||||
var startContainer = this.startContainer,
|
|
||||||
startOffset = this.startOffset,
|
|
||||||
endContainer = this.endContainer,
|
|
||||||
endOffset = this.endOffset,
|
|
||||||
parent, children, childCount, afterSplit;
|
|
||||||
|
|
||||||
// If part way through a text node, split it.
|
|
||||||
if ( startContainer.nodeType === TEXT_NODE ) {
|
|
||||||
parent = startContainer.parentNode;
|
|
||||||
children = parent.childNodes;
|
|
||||||
if ( startOffset === startContainer.length ) {
|
|
||||||
startOffset = indexOf.call( children, startContainer ) + 1;
|
|
||||||
if ( this.collapsed ) {
|
|
||||||
endContainer = parent;
|
|
||||||
endOffset = startOffset;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ( startOffset ) {
|
|
||||||
afterSplit = startContainer.splitText( startOffset );
|
|
||||||
if ( endContainer === startContainer ) {
|
|
||||||
endOffset -= startOffset;
|
|
||||||
endContainer = afterSplit;
|
|
||||||
}
|
|
||||||
else if ( endContainer === parent ) {
|
|
||||||
endOffset += 1;
|
|
||||||
}
|
|
||||||
startContainer = afterSplit;
|
|
||||||
}
|
|
||||||
startOffset = indexOf.call( children, startContainer );
|
|
||||||
}
|
}
|
||||||
startContainer = parent;
|
textContent += value;
|
||||||
} else {
|
|
||||||
children = startContainer.childNodes;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
return textContent;
|
||||||
|
};
|
||||||
|
|
||||||
childCount = children.length;
|
// ---
|
||||||
|
|
||||||
if ( startOffset === childCount) {
|
RangePrototype._insertNode = function ( node ) {
|
||||||
startContainer.appendChild( node );
|
// Insert at start.
|
||||||
} else {
|
var startContainer = this.startContainer,
|
||||||
startContainer.insertBefore( node, children[ startOffset ] );
|
startOffset = this.startOffset,
|
||||||
}
|
endContainer = this.endContainer,
|
||||||
if ( startContainer === endContainer ) {
|
endOffset = this.endOffset,
|
||||||
endOffset += children.length - childCount;
|
parent, children, childCount, afterSplit;
|
||||||
}
|
|
||||||
|
|
||||||
this.setStart( startContainer, startOffset );
|
// If part way through a text node, split it.
|
||||||
this.setEnd( endContainer, endOffset );
|
if ( startContainer.nodeType === TEXT_NODE ) {
|
||||||
|
parent = startContainer.parentNode;
|
||||||
return this;
|
children = parent.childNodes;
|
||||||
},
|
if ( startOffset === startContainer.length ) {
|
||||||
|
startOffset = indexOf.call( children, startContainer ) + 1;
|
||||||
_extractContents: function ( common ) {
|
if ( this.collapsed ) {
|
||||||
var startContainer = this.startContainer,
|
endContainer = parent;
|
||||||
startOffset = this.startOffset,
|
endOffset = startOffset;
|
||||||
endContainer = this.endContainer,
|
|
||||||
endOffset = this.endOffset;
|
|
||||||
|
|
||||||
if ( !common ) {
|
|
||||||
common = this.commonAncestorContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( common.nodeType === TEXT_NODE ) {
|
|
||||||
common = common.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
var endNode = endContainer.split( endOffset, common ),
|
|
||||||
startNode = startContainer.split( startOffset, common ),
|
|
||||||
frag = common.ownerDocument.createDocumentFragment(),
|
|
||||||
next;
|
|
||||||
|
|
||||||
// End node will be null if at end of child nodes list.
|
|
||||||
while ( startNode !== endNode ) {
|
|
||||||
next = startNode.nextSibling;
|
|
||||||
frag.appendChild( startNode );
|
|
||||||
startNode = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setStart( common, endNode ?
|
|
||||||
indexOf.call( common.childNodes, endNode ) :
|
|
||||||
common.childNodes.length );
|
|
||||||
this.collapse( true );
|
|
||||||
|
|
||||||
common.fixCursor();
|
|
||||||
|
|
||||||
return frag;
|
|
||||||
},
|
|
||||||
|
|
||||||
_deleteContents: function () {
|
|
||||||
// Move boundaries up as much as possible to reduce need to split.
|
|
||||||
this.moveBoundariesUpTree();
|
|
||||||
|
|
||||||
// Remove selected range
|
|
||||||
this._extractContents();
|
|
||||||
|
|
||||||
// If we split into two different blocks, merge the blocks.
|
|
||||||
var startBlock = this.getStartBlock(),
|
|
||||||
endBlock = this.getEndBlock();
|
|
||||||
if ( startBlock && endBlock && startBlock !== endBlock ) {
|
|
||||||
startBlock.mergeWithBlock( endBlock, this );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure block has necessary children
|
|
||||||
if ( startBlock ) {
|
|
||||||
startBlock.fixCursor();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure body has a block-level element in it.
|
|
||||||
var body = this.endContainer.ownerDocument.body,
|
|
||||||
child = body.firstChild;
|
|
||||||
if ( !child || child.nodeName === 'BR' ) {
|
|
||||||
body.fixCursor();
|
|
||||||
this.selectNodeContents( body.firstChild );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure valid range (must have only block or inline containers)
|
|
||||||
var isCollapsed = this.collapsed;
|
|
||||||
this.moveBoundariesDownTree();
|
|
||||||
if ( isCollapsed ) {
|
|
||||||
// Collapse
|
|
||||||
this.collapse( true );
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
insertTreeFragment: function ( frag ) {
|
|
||||||
// Check if it's all inline content
|
|
||||||
var isInline = true,
|
|
||||||
children = frag.childNodes,
|
|
||||||
l = children.length;
|
|
||||||
while ( l-- ) {
|
|
||||||
if ( !children[l].isInline() ) {
|
|
||||||
isInline = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete any selected content
|
|
||||||
if ( !this.collapsed ) {
|
|
||||||
this._deleteContents();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move range down into text ndoes
|
|
||||||
this.moveBoundariesDownTree();
|
|
||||||
|
|
||||||
// If inline, just insert at the current position.
|
|
||||||
if ( isInline ) {
|
|
||||||
this._insertNode( frag );
|
|
||||||
this.collapse( false );
|
|
||||||
}
|
|
||||||
// Otherwise, split up to body, insert inline before and after split
|
|
||||||
// and insert block in between split, then merge containers.
|
|
||||||
else {
|
|
||||||
var nodeAfterSplit = this.startContainer.split( this.startOffset,
|
|
||||||
this.startContainer.ownerDocument.body ),
|
|
||||||
nodeBeforeSplit = nodeAfterSplit.previousSibling,
|
|
||||||
startContainer = nodeBeforeSplit,
|
|
||||||
startOffset = startContainer.childNodes.length,
|
|
||||||
endContainer = nodeAfterSplit,
|
|
||||||
endOffset = 0,
|
|
||||||
parent = nodeAfterSplit.parentNode,
|
|
||||||
child, node;
|
|
||||||
|
|
||||||
while ( ( child = startContainer.lastChild ) &&
|
|
||||||
child.nodeType === ELEMENT_NODE &&
|
|
||||||
child.nodeName !== 'BR' ) {
|
|
||||||
startContainer = child;
|
|
||||||
startOffset = startContainer.childNodes.length;
|
|
||||||
}
|
|
||||||
while ( ( child = endContainer.firstChild ) &&
|
|
||||||
child.nodeType === ELEMENT_NODE &&
|
|
||||||
child.nodeName !== 'BR' ) {
|
|
||||||
endContainer = child;
|
|
||||||
}
|
|
||||||
while ( ( child = frag.firstChild ) && child.isInline() ) {
|
|
||||||
startContainer.appendChild( child );
|
|
||||||
}
|
|
||||||
while ( ( child = frag.lastChild ) && child.isInline() ) {
|
|
||||||
endContainer.insertBefore( child, endContainer.firstChild );
|
|
||||||
endOffset += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix cursor then insert block(s)
|
|
||||||
node = frag;
|
|
||||||
while ( node = node.getNextBlock() ) {
|
|
||||||
node.fixCursor();
|
|
||||||
}
|
|
||||||
parent.insertBefore( frag, nodeAfterSplit );
|
|
||||||
|
|
||||||
// Remove empty nodes created by split and merge inserted containers
|
|
||||||
// with edges of split
|
|
||||||
node = nodeAfterSplit.previousSibling;
|
|
||||||
if ( !nodeAfterSplit.textContent ) {
|
|
||||||
parent.removeChild( nodeAfterSplit );
|
|
||||||
} else {
|
|
||||||
nodeAfterSplit.mergeContainers();
|
|
||||||
}
|
|
||||||
if ( !nodeAfterSplit.parentNode ) {
|
|
||||||
endContainer = node;
|
|
||||||
endOffset = endContainer.getLength();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( !nodeBeforeSplit.textContent) {
|
|
||||||
startContainer = nodeBeforeSplit.nextSibling;
|
|
||||||
startOffset = 0;
|
|
||||||
parent.removeChild( nodeBeforeSplit );
|
|
||||||
} else {
|
|
||||||
nodeBeforeSplit.mergeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setStart( startContainer, startOffset );
|
|
||||||
this.setEnd( endContainer, endOffset );
|
|
||||||
this.moveBoundariesDownTree();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
containsNode: function ( node, partial ) {
|
|
||||||
var range = this,
|
|
||||||
nodeRange = node.ownerDocument.createRange();
|
|
||||||
|
|
||||||
nodeRange.selectNode( node );
|
|
||||||
|
|
||||||
if ( partial ) {
|
|
||||||
// Node must not finish before range starts or start after range
|
|
||||||
// finishes.
|
|
||||||
var nodeEndBeforeStart = ( range.compareBoundaryPoints(
|
|
||||||
END_TO_START, nodeRange ) > -1 ),
|
|
||||||
nodeStartAfterEnd = ( range.compareBoundaryPoints(
|
|
||||||
START_TO_END, nodeRange ) < 1 );
|
|
||||||
return ( !nodeEndBeforeStart && !nodeStartAfterEnd );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Node must start after range starts and finish before range
|
|
||||||
// finishes
|
|
||||||
var nodeStartAfterStart = ( range.compareBoundaryPoints(
|
|
||||||
START_TO_START, nodeRange ) < 1 ),
|
|
||||||
nodeEndBeforeEnd = ( range.compareBoundaryPoints(
|
|
||||||
END_TO_END, nodeRange ) > -1 );
|
|
||||||
return ( nodeStartAfterStart && nodeEndBeforeEnd );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
moveBoundariesDownTree: function () {
|
|
||||||
var startContainer = this.startContainer,
|
|
||||||
startOffset = this.startOffset,
|
|
||||||
endContainer = this.endContainer,
|
|
||||||
endOffset = this.endOffset,
|
|
||||||
child;
|
|
||||||
|
|
||||||
while ( startContainer.nodeType !== TEXT_NODE ) {
|
|
||||||
child = startContainer.childNodes[ startOffset ];
|
|
||||||
if ( !child || child.isLeaf() ) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
startContainer = child;
|
|
||||||
startOffset = 0;
|
|
||||||
}
|
|
||||||
if ( endOffset ) {
|
|
||||||
while ( endContainer.nodeType !== TEXT_NODE ) {
|
|
||||||
child = endContainer.childNodes[ endOffset - 1 ];
|
|
||||||
if ( !child || child.isLeaf() ) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
endContainer = child;
|
|
||||||
endOffset = endContainer.getLength();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
while ( endContainer.nodeType !== TEXT_NODE ) {
|
|
||||||
child = endContainer.firstChild;
|
|
||||||
if ( !child || child.isLeaf() ) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
endContainer = child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If collapsed, this algorithm finds the nearest text node positions
|
|
||||||
// *outside* the range rather than inside, but also it flips which is
|
|
||||||
// assigned to which.
|
|
||||||
if ( this.collapsed ) {
|
|
||||||
this.setStart( endContainer, endOffset );
|
|
||||||
this.setEnd( startContainer, startOffset );
|
|
||||||
} else {
|
|
||||||
this.setStart( startContainer, startOffset );
|
|
||||||
this.setEnd( endContainer, endOffset );
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
moveBoundariesUpTree: function ( common ) {
|
|
||||||
var startContainer = this.startContainer,
|
|
||||||
startOffset = this.startOffset,
|
|
||||||
endContainer = this.endContainer,
|
|
||||||
endOffset = this.endOffset,
|
|
||||||
parent;
|
|
||||||
|
|
||||||
if ( !common ) {
|
|
||||||
common = this.commonAncestorContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
while ( startContainer !== common && !startOffset ) {
|
|
||||||
parent = startContainer.parentNode;
|
|
||||||
startOffset = indexOf.call( parent.childNodes, startContainer );
|
|
||||||
startContainer = parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
while ( endContainer !== common &&
|
|
||||||
endOffset === endContainer.getLength() ) {
|
|
||||||
parent = endContainer.parentNode;
|
|
||||||
endOffset = indexOf.call( parent.childNodes, endContainer ) + 1;
|
|
||||||
endContainer = parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setStart( startContainer, startOffset );
|
|
||||||
this.setEnd( endContainer, endOffset );
|
|
||||||
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Returns the first block at least partially contained by the range,
|
|
||||||
// or null if no block is contained by the range.
|
|
||||||
getStartBlock: function () {
|
|
||||||
var container = this.startContainer,
|
|
||||||
block;
|
|
||||||
|
|
||||||
// If inline, get the containing block.
|
|
||||||
if ( container.isInline() ) {
|
|
||||||
block = container.getPreviousBlock();
|
|
||||||
} else if ( container.isBlock() ) {
|
|
||||||
block = container;
|
|
||||||
} else {
|
|
||||||
block = getNodeBefore( container, this.startOffset );
|
|
||||||
block = block.getNextBlock();
|
|
||||||
}
|
|
||||||
// Check the block actually intersects the range
|
|
||||||
return block && this.containsNode( block, true ) ? block : null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Returns the last block at least partially contained by the range,
|
|
||||||
// or null if no block is contained by the range.
|
|
||||||
getEndBlock: function () {
|
|
||||||
var container = this.endContainer,
|
|
||||||
block, child;
|
|
||||||
|
|
||||||
// If inline, get the containing block.
|
|
||||||
if ( container.isInline() ) {
|
|
||||||
block = container.getPreviousBlock();
|
|
||||||
} else if ( container.isBlock() ) {
|
|
||||||
block = container;
|
|
||||||
} else {
|
|
||||||
block = getNodeAfter( container, this.endOffset );
|
|
||||||
if ( !block ) {
|
|
||||||
block = container.ownerDocument.body;
|
|
||||||
while ( child = block.lastChild ) {
|
|
||||||
block = child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
block = block.getPreviousBlock();
|
|
||||||
|
|
||||||
}
|
|
||||||
// Check the block actually intersects the range
|
|
||||||
return block && this.containsNode( block, true ) ? block : null;
|
|
||||||
},
|
|
||||||
|
|
||||||
startsAtBlockBoundary: function () {
|
|
||||||
var startContainer = this.startContainer,
|
|
||||||
startOffset = this.startOffset,
|
|
||||||
parent, child;
|
|
||||||
|
|
||||||
while ( startContainer.isInline() ) {
|
|
||||||
if ( startOffset ) {
|
if ( startOffset ) {
|
||||||
return false;
|
afterSplit = startContainer.splitText( startOffset );
|
||||||
|
if ( endContainer === startContainer ) {
|
||||||
|
endOffset -= startOffset;
|
||||||
|
endContainer = afterSplit;
|
||||||
|
}
|
||||||
|
else if ( endContainer === parent ) {
|
||||||
|
endOffset += 1;
|
||||||
|
}
|
||||||
|
startContainer = afterSplit;
|
||||||
}
|
}
|
||||||
parent = startContainer.parentNode;
|
startOffset = indexOf.call( children, startContainer );
|
||||||
startOffset = indexOf.call( parent.childNodes, startContainer );
|
|
||||||
startContainer = parent;
|
|
||||||
}
|
}
|
||||||
// Skip empty text nodes and <br>s.
|
startContainer = parent;
|
||||||
while ( startOffset &&
|
} else {
|
||||||
( child = startContainer.childNodes[ startOffset - 1 ] ) &&
|
children = startContainer.childNodes;
|
||||||
( child.data === '' || child.nodeName === 'BR' ) ) {
|
}
|
||||||
startOffset -= 1;
|
|
||||||
}
|
|
||||||
return !startOffset;
|
|
||||||
},
|
|
||||||
|
|
||||||
endsAtBlockBoundary: function () {
|
childCount = children.length;
|
||||||
var endContainer = this.endContainer,
|
|
||||||
endOffset = this.endOffset,
|
|
||||||
length = endContainer.getLength(),
|
|
||||||
parent, child;
|
|
||||||
|
|
||||||
while ( endContainer.isInline() ) {
|
if ( startOffset === childCount) {
|
||||||
if ( endOffset !== length ) {
|
startContainer.appendChild( node );
|
||||||
return false;
|
} else {
|
||||||
}
|
startContainer.insertBefore( node, children[ startOffset ] );
|
||||||
parent = endContainer.parentNode;
|
}
|
||||||
endOffset = indexOf.call( parent.childNodes, endContainer ) + 1;
|
if ( startContainer === endContainer ) {
|
||||||
endContainer = parent;
|
endOffset += children.length - childCount;
|
||||||
length = endContainer.childNodes.length;
|
}
|
||||||
|
|
||||||
|
this.setStart( startContainer, startOffset );
|
||||||
|
this.setEnd( endContainer, endOffset );
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
RangePrototype._extractContents = function ( common ) {
|
||||||
|
var startContainer = this.startContainer,
|
||||||
|
startOffset = this.startOffset,
|
||||||
|
endContainer = this.endContainer,
|
||||||
|
endOffset = this.endOffset;
|
||||||
|
|
||||||
|
if ( !common ) {
|
||||||
|
common = this.commonAncestorContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( common.nodeType === TEXT_NODE ) {
|
||||||
|
common = common.parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
var endNode = split( endContainer, endOffset, common ),
|
||||||
|
startNode = split( startContainer, startOffset, common ),
|
||||||
|
frag = common.ownerDocument.createDocumentFragment(),
|
||||||
|
next;
|
||||||
|
|
||||||
|
// End node will be null if at end of child nodes list.
|
||||||
|
while ( startNode !== endNode ) {
|
||||||
|
next = startNode.nextSibling;
|
||||||
|
frag.appendChild( startNode );
|
||||||
|
startNode = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStart( common, endNode ?
|
||||||
|
indexOf.call( common.childNodes, endNode ) :
|
||||||
|
common.childNodes.length );
|
||||||
|
this.collapse( true );
|
||||||
|
|
||||||
|
fixCursor( common );
|
||||||
|
|
||||||
|
return frag;
|
||||||
|
};
|
||||||
|
|
||||||
|
RangePrototype._deleteContents = function () {
|
||||||
|
// Move boundaries up as much as possible to reduce need to split.
|
||||||
|
this.moveBoundariesUpTree();
|
||||||
|
|
||||||
|
// Remove selected range
|
||||||
|
this._extractContents();
|
||||||
|
|
||||||
|
// If we split into two different blocks, merge the blocks.
|
||||||
|
var startBlock = this.getStartBlock(),
|
||||||
|
endBlock = this.getEndBlock();
|
||||||
|
if ( startBlock && endBlock && startBlock !== endBlock ) {
|
||||||
|
mergeWithBlock( startBlock, endBlock, this );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure block has necessary children
|
||||||
|
if ( startBlock ) {
|
||||||
|
fixCursor( startBlock );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure body has a block-level element in it.
|
||||||
|
var body = this.endContainer.ownerDocument.body,
|
||||||
|
child = body.firstChild;
|
||||||
|
if ( !child || child.nodeName === 'BR' ) {
|
||||||
|
fixCursor( body );
|
||||||
|
this.selectNodeContents( body.firstChild );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure valid range (must have only block or inline containers)
|
||||||
|
var isCollapsed = this.collapsed;
|
||||||
|
this.moveBoundariesDownTree();
|
||||||
|
if ( isCollapsed ) {
|
||||||
|
// Collapse
|
||||||
|
this.collapse( true );
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
RangePrototype.insertTreeFragment = function ( frag ) {
|
||||||
|
// Check if it's all inline content
|
||||||
|
var allInline = true,
|
||||||
|
children = frag.childNodes,
|
||||||
|
l = children.length;
|
||||||
|
while ( l-- ) {
|
||||||
|
if ( !isInline( children[l] ) ) {
|
||||||
|
allInline = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
// Skip empty text nodes and <br>s.
|
}
|
||||||
while ( endOffset < length &&
|
|
||||||
( child = endContainer.childNodes[ endOffset ] ) &&
|
// Delete any selected content
|
||||||
( child.data === '' || child.nodeName === 'BR' ) ) {
|
if ( !this.collapsed ) {
|
||||||
|
this._deleteContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move range down into text ndoes
|
||||||
|
this.moveBoundariesDownTree();
|
||||||
|
|
||||||
|
// If inline, just insert at the current position.
|
||||||
|
if ( allInline ) {
|
||||||
|
this._insertNode( frag );
|
||||||
|
this.collapse( false );
|
||||||
|
}
|
||||||
|
// Otherwise, split up to body, insert inline before and after split
|
||||||
|
// and insert block in between split, then merge containers.
|
||||||
|
else {
|
||||||
|
var nodeAfterSplit = split( this.startContainer, this.startOffset,
|
||||||
|
this.startContainer.ownerDocument.body ),
|
||||||
|
nodeBeforeSplit = nodeAfterSplit.previousSibling,
|
||||||
|
startContainer = nodeBeforeSplit,
|
||||||
|
startOffset = startContainer.childNodes.length,
|
||||||
|
endContainer = nodeAfterSplit,
|
||||||
|
endOffset = 0,
|
||||||
|
parent = nodeAfterSplit.parentNode,
|
||||||
|
child, node;
|
||||||
|
|
||||||
|
while ( ( child = startContainer.lastChild ) &&
|
||||||
|
child.nodeType === ELEMENT_NODE &&
|
||||||
|
child.nodeName !== 'BR' ) {
|
||||||
|
startContainer = child;
|
||||||
|
startOffset = startContainer.childNodes.length;
|
||||||
|
}
|
||||||
|
while ( ( child = endContainer.firstChild ) &&
|
||||||
|
child.nodeType === ELEMENT_NODE &&
|
||||||
|
child.nodeName !== 'BR' ) {
|
||||||
|
endContainer = child;
|
||||||
|
}
|
||||||
|
while ( ( child = frag.firstChild ) && isInline( child ) ) {
|
||||||
|
startContainer.appendChild( child );
|
||||||
|
}
|
||||||
|
while ( ( child = frag.lastChild ) && isInline( child ) ) {
|
||||||
|
endContainer.insertBefore( child, endContainer.firstChild );
|
||||||
endOffset += 1;
|
endOffset += 1;
|
||||||
}
|
}
|
||||||
return endOffset === length;
|
|
||||||
},
|
|
||||||
|
|
||||||
expandToBlockBoundaries: function () {
|
// Fix cursor then insert block(s)
|
||||||
var start = this.getStartBlock(),
|
node = frag;
|
||||||
end = this.getEndBlock(),
|
while ( node = getNextBlock( node ) ) {
|
||||||
parent;
|
fixCursor( node );
|
||||||
|
}
|
||||||
|
parent.insertBefore( frag, nodeAfterSplit );
|
||||||
|
|
||||||
if ( start && end ) {
|
// Remove empty nodes created by split and merge inserted containers
|
||||||
parent = start.parentNode;
|
// with edges of split
|
||||||
this.setStart( parent, indexOf.call( parent.childNodes, start ) );
|
node = nodeAfterSplit.previousSibling;
|
||||||
parent = end.parentNode;
|
if ( !nodeAfterSplit.textContent ) {
|
||||||
this.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 );
|
parent.removeChild( nodeAfterSplit );
|
||||||
|
} else {
|
||||||
|
mergeContainers( nodeAfterSplit );
|
||||||
|
}
|
||||||
|
if ( !nodeAfterSplit.parentNode ) {
|
||||||
|
endContainer = node;
|
||||||
|
endOffset = getLength( endContainer );
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
if ( !nodeBeforeSplit.textContent) {
|
||||||
|
startContainer = nodeBeforeSplit.nextSibling;
|
||||||
|
startOffset = 0;
|
||||||
|
parent.removeChild( nodeBeforeSplit );
|
||||||
|
} else {
|
||||||
|
mergeContainers( nodeBeforeSplit );
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStart( startContainer, startOffset );
|
||||||
|
this.setEnd( endContainer, endOffset );
|
||||||
|
this.moveBoundariesDownTree();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var prop;
|
// ---
|
||||||
for ( prop in RangePrototypeExtensions ) {
|
|
||||||
Range.prototype[ prop ] = RangePrototypeExtensions[ prop ];
|
|
||||||
}
|
|
||||||
|
|
||||||
}( DOMTreeWalker ) );
|
RangePrototype.containsNode = function ( node, partial ) {
|
||||||
|
var range = this,
|
||||||
|
nodeRange = node.ownerDocument.createRange();
|
||||||
|
|
||||||
|
nodeRange.selectNode( node );
|
||||||
|
|
||||||
|
if ( partial ) {
|
||||||
|
// Node must not finish before range starts or start after range
|
||||||
|
// finishes.
|
||||||
|
var nodeEndBeforeStart = ( range.compareBoundaryPoints(
|
||||||
|
END_TO_START, nodeRange ) > -1 ),
|
||||||
|
nodeStartAfterEnd = ( range.compareBoundaryPoints(
|
||||||
|
START_TO_END, nodeRange ) < 1 );
|
||||||
|
return ( !nodeEndBeforeStart && !nodeStartAfterEnd );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Node must start after range starts and finish before range
|
||||||
|
// finishes
|
||||||
|
var nodeStartAfterStart = ( range.compareBoundaryPoints(
|
||||||
|
START_TO_START, nodeRange ) < 1 ),
|
||||||
|
nodeEndBeforeEnd = ( range.compareBoundaryPoints(
|
||||||
|
END_TO_END, nodeRange ) > -1 );
|
||||||
|
return ( nodeStartAfterStart && nodeEndBeforeEnd );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RangePrototype.moveBoundariesDownTree = function () {
|
||||||
|
var startContainer = this.startContainer,
|
||||||
|
startOffset = this.startOffset,
|
||||||
|
endContainer = this.endContainer,
|
||||||
|
endOffset = this.endOffset,
|
||||||
|
child;
|
||||||
|
|
||||||
|
while ( startContainer.nodeType !== TEXT_NODE ) {
|
||||||
|
child = startContainer.childNodes[ startOffset ];
|
||||||
|
if ( !child || isLeaf( child ) ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
startContainer = child;
|
||||||
|
startOffset = 0;
|
||||||
|
}
|
||||||
|
if ( endOffset ) {
|
||||||
|
while ( endContainer.nodeType !== TEXT_NODE ) {
|
||||||
|
child = endContainer.childNodes[ endOffset - 1 ];
|
||||||
|
if ( !child || isLeaf( child ) ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
endContainer = child;
|
||||||
|
endOffset = getLength( endContainer );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while ( endContainer.nodeType !== TEXT_NODE ) {
|
||||||
|
child = endContainer.firstChild;
|
||||||
|
if ( !child || isLeaf( child ) ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
endContainer = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If collapsed, this algorithm finds the nearest text node positions
|
||||||
|
// *outside* the range rather than inside, but also it flips which is
|
||||||
|
// assigned to which.
|
||||||
|
if ( this.collapsed ) {
|
||||||
|
this.setStart( endContainer, endOffset );
|
||||||
|
this.setEnd( startContainer, startOffset );
|
||||||
|
} else {
|
||||||
|
this.setStart( startContainer, startOffset );
|
||||||
|
this.setEnd( endContainer, endOffset );
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
RangePrototype.moveBoundariesUpTree = function ( common ) {
|
||||||
|
var startContainer = this.startContainer,
|
||||||
|
startOffset = this.startOffset,
|
||||||
|
endContainer = this.endContainer,
|
||||||
|
endOffset = this.endOffset,
|
||||||
|
parent;
|
||||||
|
|
||||||
|
if ( !common ) {
|
||||||
|
common = this.commonAncestorContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ( startContainer !== common && !startOffset ) {
|
||||||
|
parent = startContainer.parentNode;
|
||||||
|
startOffset = indexOf.call( parent.childNodes, startContainer );
|
||||||
|
startContainer = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ( endContainer !== common &&
|
||||||
|
endOffset === getLength( endContainer ) ) {
|
||||||
|
parent = endContainer.parentNode;
|
||||||
|
endOffset = indexOf.call( parent.childNodes, endContainer ) + 1;
|
||||||
|
endContainer = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStart( startContainer, startOffset );
|
||||||
|
this.setEnd( endContainer, endOffset );
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns the first block at least partially contained by the range,
|
||||||
|
// or null if no block is contained by the range.
|
||||||
|
RangePrototype.getStartBlock = function () {
|
||||||
|
var container = this.startContainer,
|
||||||
|
block;
|
||||||
|
|
||||||
|
// If inline, get the containing block.
|
||||||
|
if ( isInline( container ) ) {
|
||||||
|
block = getPreviousBlock( container );
|
||||||
|
} else if ( isBlock( container ) ) {
|
||||||
|
block = container;
|
||||||
|
} else {
|
||||||
|
block = getNodeBefore( container, this.startOffset );
|
||||||
|
block = getNextBlock( block );
|
||||||
|
}
|
||||||
|
// Check the block actually intersects the range
|
||||||
|
return block && this.containsNode( block, true ) ? block : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns the last block at least partially contained by the range,
|
||||||
|
// or null if no block is contained by the range.
|
||||||
|
RangePrototype.getEndBlock = function () {
|
||||||
|
var container = this.endContainer,
|
||||||
|
block, child;
|
||||||
|
|
||||||
|
// If inline, get the containing block.
|
||||||
|
if ( isInline( container ) ) {
|
||||||
|
block = getPreviousBlock( container );
|
||||||
|
} else if ( isBlock( container ) ) {
|
||||||
|
block = container;
|
||||||
|
} else {
|
||||||
|
block = getNodeAfter( container, this.endOffset );
|
||||||
|
if ( !block ) {
|
||||||
|
block = container.ownerDocument.body;
|
||||||
|
while ( child = block.lastChild ) {
|
||||||
|
block = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
block = getPreviousBlock( block );
|
||||||
|
|
||||||
|
}
|
||||||
|
// Check the block actually intersects the range
|
||||||
|
return block && this.containsNode( block, true ) ? block : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
RangePrototype.startsAtBlockBoundary = function () {
|
||||||
|
var startContainer = this.startContainer,
|
||||||
|
startOffset = this.startOffset,
|
||||||
|
parent, child;
|
||||||
|
|
||||||
|
while ( isInline( startContainer ) ) {
|
||||||
|
if ( startOffset ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
parent = startContainer.parentNode;
|
||||||
|
startOffset = indexOf.call( parent.childNodes, startContainer );
|
||||||
|
startContainer = parent;
|
||||||
|
}
|
||||||
|
// Skip empty text nodes and <br>s.
|
||||||
|
while ( startOffset &&
|
||||||
|
( child = startContainer.childNodes[ startOffset - 1 ] ) &&
|
||||||
|
( child.data === '' || child.nodeName === 'BR' ) ) {
|
||||||
|
startOffset -= 1;
|
||||||
|
}
|
||||||
|
return !startOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
RangePrototype.endsAtBlockBoundary = function () {
|
||||||
|
var endContainer = this.endContainer,
|
||||||
|
endOffset = this.endOffset,
|
||||||
|
length = getLength( endContainer ),
|
||||||
|
parent, child;
|
||||||
|
|
||||||
|
while ( isInline( endContainer ) ) {
|
||||||
|
if ( endOffset !== length ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
parent = endContainer.parentNode;
|
||||||
|
endOffset = indexOf.call( parent.childNodes, endContainer ) + 1;
|
||||||
|
endContainer = parent;
|
||||||
|
length = endContainer.childNodes.length;
|
||||||
|
}
|
||||||
|
// Skip empty text nodes and <br>s.
|
||||||
|
while ( endOffset < length &&
|
||||||
|
( child = endContainer.childNodes[ endOffset ] ) &&
|
||||||
|
( child.data === '' || child.nodeName === 'BR' ) ) {
|
||||||
|
endOffset += 1;
|
||||||
|
}
|
||||||
|
return endOffset === length;
|
||||||
|
};
|
||||||
|
|
||||||
|
RangePrototype.expandToBlockBoundaries = function () {
|
||||||
|
var start = this.getStartBlock(),
|
||||||
|
end = this.getEndBlock(),
|
||||||
|
parent;
|
||||||
|
|
||||||
|
if ( start && end ) {
|
||||||
|
parent = start.parentNode;
|
||||||
|
this.setStart( parent, indexOf.call( parent.childNodes, start ) );
|
||||||
|
parent = end.parentNode;
|
||||||
|
this.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* Copyright © 2011-2012 by Neil Jenkins. Licensed under the MIT license. */
|
/*global FILTER_ACCEPT */
|
||||||
|
/*jshint strict:false */
|
||||||
/*global document, window */
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Native TreeWalker is buggy in IE and Opera:
|
Native TreeWalker is buggy in IE and Opera:
|
||||||
|
@ -13,90 +12,80 @@
|
||||||
(subset) of the spec in all browsers.
|
(subset) of the spec in all browsers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var DOMTreeWalker = (function () {
|
var typeToBitArray = {
|
||||||
|
// ELEMENT_NODE
|
||||||
|
1: 1,
|
||||||
|
// ATTRIBUTE_NODE
|
||||||
|
2: 2,
|
||||||
|
// TEXT_NODE
|
||||||
|
3: 4,
|
||||||
|
// COMMENT_NODE
|
||||||
|
8: 128,
|
||||||
|
// DOCUMENT_NODE
|
||||||
|
9: 256,
|
||||||
|
// DOCUMENT_FRAGMENT_NODE
|
||||||
|
11: 1024
|
||||||
|
};
|
||||||
|
|
||||||
"use strict";
|
function TreeWalker ( root, nodeType, filter ) {
|
||||||
|
this.root = this.currentNode = root;
|
||||||
|
this.nodeType = nodeType;
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
var typeToBitArray = {
|
TreeWalker.prototype.nextNode = function () {
|
||||||
// ELEMENT_NODE
|
var current = this.currentNode,
|
||||||
1: 1,
|
root = this.root,
|
||||||
// ATTRIBUTE_NODE
|
nodeType = this.nodeType,
|
||||||
2: 2,
|
filter = this.filter,
|
||||||
// TEXT_NODE
|
node;
|
||||||
3: 4,
|
while ( true ) {
|
||||||
// COMMENT_NODE
|
node = current.firstChild;
|
||||||
8: 128,
|
while ( !node && current ) {
|
||||||
// DOCUMENT_NODE
|
|
||||||
9: 256,
|
|
||||||
// DOCUMENT_FRAGMENT_NODE
|
|
||||||
11: 1024
|
|
||||||
};
|
|
||||||
|
|
||||||
var FILTER_ACCEPT = 1;
|
|
||||||
|
|
||||||
var TreeWalker = function ( root, nodeType, filter ) {
|
|
||||||
this.root = this.currentNode = root;
|
|
||||||
this.nodeType = nodeType;
|
|
||||||
this.filter = filter;
|
|
||||||
};
|
|
||||||
|
|
||||||
TreeWalker.prototype.nextNode = function () {
|
|
||||||
var current = this.currentNode,
|
|
||||||
root = this.root,
|
|
||||||
nodeType = this.nodeType,
|
|
||||||
filter = this.filter,
|
|
||||||
node;
|
|
||||||
while ( true ) {
|
|
||||||
node = current.firstChild;
|
|
||||||
while ( !node && current ) {
|
|
||||||
if ( current === root ) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
node = current.nextSibling;
|
|
||||||
if ( !node ) { current = current.parentNode; }
|
|
||||||
}
|
|
||||||
if ( !node ) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if ( ( typeToBitArray[ node.nodeType ] & nodeType ) &&
|
|
||||||
filter( node ) === FILTER_ACCEPT ) {
|
|
||||||
this.currentNode = node;
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
current = node;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TreeWalker.prototype.previousNode = function () {
|
|
||||||
var current = this.currentNode,
|
|
||||||
root = this.root,
|
|
||||||
nodeType = this.nodeType,
|
|
||||||
filter = this.filter,
|
|
||||||
node;
|
|
||||||
while ( true ) {
|
|
||||||
if ( current === root ) {
|
if ( current === root ) {
|
||||||
return null;
|
break;
|
||||||
}
|
}
|
||||||
node = current.previousSibling;
|
node = current.nextSibling;
|
||||||
if ( node ) {
|
if ( !node ) { current = current.parentNode; }
|
||||||
while ( current = node.lastChild ) {
|
|
||||||
node = current;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
node = current.parentNode;
|
|
||||||
}
|
|
||||||
if ( !node ) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if ( ( typeToBitArray[ node.nodeType ] & nodeType ) &&
|
|
||||||
filter( node ) === FILTER_ACCEPT ) {
|
|
||||||
this.currentNode = node;
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
current = node;
|
|
||||||
}
|
}
|
||||||
};
|
if ( !node ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ( ( typeToBitArray[ node.nodeType ] & nodeType ) &&
|
||||||
|
filter( node ) === FILTER_ACCEPT ) {
|
||||||
|
this.currentNode = node;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
current = node;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return TreeWalker;
|
TreeWalker.prototype.previousNode = function () {
|
||||||
|
var current = this.currentNode,
|
||||||
})();
|
root = this.root,
|
||||||
|
nodeType = this.nodeType,
|
||||||
|
filter = this.filter,
|
||||||
|
node;
|
||||||
|
while ( true ) {
|
||||||
|
if ( current === root ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
node = current.previousSibling;
|
||||||
|
if ( node ) {
|
||||||
|
while ( current = node.lastChild ) {
|
||||||
|
node = current;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node = current.parentNode;
|
||||||
|
}
|
||||||
|
if ( !node ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ( ( typeToBitArray[ node.nodeType ] & nodeType ) &&
|
||||||
|
filter( node ) === FILTER_ACCEPT ) {
|
||||||
|
this.currentNode = node;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
current = node;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
29
source/UA.js
29
source/UA.js
|
@ -1,29 +0,0 @@
|
||||||
/* Copyright © 2011-2012 by Neil Jenkins. Licensed under the MIT license. */
|
|
||||||
|
|
||||||
/*global navigator, window */
|
|
||||||
|
|
||||||
var UA = (function ( win ) {
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var ua = navigator.userAgent;
|
|
||||||
var isOpera = !!win.opera;
|
|
||||||
var isIE = /Trident\//.test( ua );
|
|
||||||
var isWebKit = /WebKit\//.test( ua );
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Browser sniffing. Unfortunately necessary.
|
|
||||||
isOpera: isOpera,
|
|
||||||
isIE8: ( win.ie === 8 ),
|
|
||||||
isIE: isIE,
|
|
||||||
isGecko: /Gecko\//.test( ua ),
|
|
||||||
isWebKit: isWebKit,
|
|
||||||
isIOS: /iP(?:ad|hone|od)/.test( ua ),
|
|
||||||
|
|
||||||
// Browser quirks
|
|
||||||
useTextFixer: isIE || isOpera,
|
|
||||||
cantFocusEmptyTextNodes: isIE || isWebKit,
|
|
||||||
losesSelectionOnBlur: isIE
|
|
||||||
};
|
|
||||||
|
|
||||||
})( window );
|
|
5
source/intro.js
Normal file
5
source/intro.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/* Copyright © 2011-2013 by Neil Jenkins. MIT Licensed. */
|
||||||
|
|
||||||
|
( function ( doc ) {
|
||||||
|
|
||||||
|
"use strict";
|
1
source/outro.js
Normal file
1
source/outro.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
}( document ) );
|
Loading…
Reference in a new issue