mirror of
https://github.com/fastmail/Squire.git
synced 2024-12-22 15:23:29 -05:00
Add support for IE8.
This commit is contained in:
parent
746e86a3a8
commit
f0ba6216cc
13 changed files with 816 additions and 58 deletions
|
@ -26,6 +26,9 @@
|
|||
margin: 5px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if IE 8]>
|
||||
<script type="text/javascript" src="build/ie8.js"></script>
|
||||
<![endif]-->
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
var editor;
|
||||
document.addEventListener( 'click', function ( e ) {
|
||||
|
|
6
Makefile
6
Makefile
|
@ -3,7 +3,11 @@
|
|||
clean:
|
||||
rm -rf build
|
||||
|
||||
build: build/squire.js build/document.html
|
||||
build: build/ie8.js build/squire.js build/document.html
|
||||
|
||||
build/ie8.js: source/ie8types.js source/ie8dom.js source/ie8range.js
|
||||
mkdir -p $(@D)
|
||||
cat $^ | uglifyjs > $@
|
||||
|
||||
build/squire.js: source/TreeWalker.js source/Node.js source/Range.js source/Editor.js
|
||||
mkdir -p $(@D)
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
Squire
|
||||
======
|
||||
|
||||
Squire is an HTML5 rich text editor, which provides powerful cross-browser normalisation, whilst being supremely lightweight and flexible. It is built for the present and the future, and as such does not support ancient browsers. I'd guess it should work fine back to around Opera 10, Firefox 3.5, Safari 4, Chrome 9 and IE9, but I only test in the latest version of each of these browsers. Adding IE8 support should be possible by patching IE8 to support the W3C Range and TreeWalker APIs; patches are welcome. I am not interested in support for IE7 or below.
|
||||
Squire is an HTML5 rich text editor, which provides powerful cross-browser normalisation, whilst being supremely lightweight and flexible. It is built for the present and the future, and as such does not support truly ancient browsers. It should work fine back to around Opera 10, Firefox 3.5, Safari 4, Chrome 9 and IE8.
|
||||
|
||||
Unlike other HTML5 rich text editors, squire was written as a component for writing documents (emails, essays, etc.), not doing wysiwyg websites. If you are looking for support for inserting form controls or flash components or the like, you'll need to look elsewhere. However for many purposes, Squire may be just what you need, providing the power without the bloat. The key features are:
|
||||
Unlike other HTML5 rich text editors, Squire was written as a component for writing documents (emails, essays, etc.), not doing wysiwyg websites. If you are looking for support for inserting form controls or flash components or the like, you'll need to look elsewhere. However for many purposes, Squire may be just what you need, providing the power without the bloat. The key features are:
|
||||
|
||||
### Lightweight ###
|
||||
|
||||
* Only 8KB of JS after minification and gzip (26KB before gzip).
|
||||
* Only 9KB of JS after minification and gzip (27KB before gzip).
|
||||
* IE8 support does not add extra bloat to the core library; instead, a separate
|
||||
3KB (7KB before gzip) file patches the browser to support the W3C APIs.
|
||||
* Does not include its own XHR wrapper, widget library or lightbox overlays.
|
||||
* No dependencies.
|
||||
* No UI for a toolbar is supplied, allowing you to integrate seamlessly with the
|
||||
|
|
|
@ -49,6 +49,9 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!--[if IE 8]>
|
||||
<script type="text/javascript" src="ie8.js"></script>
|
||||
<![endif]-->
|
||||
<!--[if IE 9]>
|
||||
<script type="text/javascript">window.ie = 9;</script>
|
||||
<![endif]-->
|
||||
|
|
1
build/ie8.js
Normal file
1
build/ie8.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -150,9 +150,9 @@
|
|||
var lastFocusNode;
|
||||
var path = '';
|
||||
|
||||
var updatePath = function ( _, force ) {
|
||||
var anchor = sel.anchorNode,
|
||||
focus = sel.focusNode,
|
||||
var updatePath = function ( range, force ) {
|
||||
var anchor = range.startContainer,
|
||||
focus = range.endContainer,
|
||||
newPath;
|
||||
if ( force || anchor !== lastAnchorNode || focus !== lastFocusNode ) {
|
||||
lastAnchorNode = anchor;
|
||||
|
@ -168,8 +168,11 @@
|
|||
fireEvent( 'select' );
|
||||
}
|
||||
};
|
||||
addEventListener( 'keyup', updatePath );
|
||||
addEventListener( 'mouseup', updatePath );
|
||||
var updatePathOnEvent = function () {
|
||||
updatePath( getSelection() );
|
||||
};
|
||||
addEventListener( 'keyup', updatePathOnEvent );
|
||||
addEventListener( 'mouseup', updatePathOnEvent );
|
||||
|
||||
var setSelection = function ( range ) {
|
||||
if ( range ) {
|
||||
|
@ -223,7 +226,7 @@
|
|||
range._insertNode( el );
|
||||
range.setStartAfter( el );
|
||||
setSelection( range );
|
||||
updatePath();
|
||||
updatePath( range );
|
||||
};
|
||||
|
||||
// --- Bookmarking ---
|
||||
|
@ -329,6 +332,16 @@
|
|||
if ( !event.ctrlKey && !event.metaKey && !event.altKey &&
|
||||
( code < 16 || code > 20 ) &&
|
||||
( code < 33 || code > 45 ) ) {
|
||||
// If you select all in IE8 then type, it makes a P; replace it with
|
||||
// a DIV.
|
||||
var firstChild = body.firstChild;
|
||||
if ( win.ie === 8 && firstChild.nodeName === 'P' ) {
|
||||
saveRangeToBookmark( getSelection() );
|
||||
firstChild.replaceWith( createElement( 'DIV', [
|
||||
firstChild.empty()
|
||||
]) );
|
||||
setSelection( getRangeAndRemoveBookmark() );
|
||||
}
|
||||
docWasChanged();
|
||||
}
|
||||
});
|
||||
|
@ -530,7 +543,7 @@
|
|||
endContainer = range.endContainer,
|
||||
endOffset = range.endOffset,
|
||||
toWrap = [],
|
||||
examineNode = function examineNode ( node, exemplar ) {
|
||||
examineNode = function ( node, exemplar ) {
|
||||
// If the node is completely contained by the range then
|
||||
// we're going to remove all formatting so ignore it.
|
||||
if ( range.containsNode( node, false ) ) {
|
||||
|
@ -629,7 +642,7 @@
|
|||
}
|
||||
|
||||
setSelection( range );
|
||||
updatePath( 0, true );
|
||||
updatePath( range, true );
|
||||
|
||||
// We're not still in an undo state
|
||||
docWasChanged();
|
||||
|
@ -661,7 +674,7 @@
|
|||
setSelection( range );
|
||||
|
||||
// Path may have changed
|
||||
updatePath( 0, true );
|
||||
updatePath( range, true );
|
||||
|
||||
// We're not still in an undo state
|
||||
docWasChanged();
|
||||
|
@ -709,7 +722,7 @@
|
|||
|
||||
// 8. Restore selection
|
||||
setSelection( getRangeAndRemoveBookmark() );
|
||||
updatePath( 0, true );
|
||||
updatePath( range, true );
|
||||
|
||||
// 9. We're not still in an undo state
|
||||
docWasChanged();
|
||||
|
@ -742,7 +755,7 @@
|
|||
return frag;
|
||||
};
|
||||
|
||||
var makeList = function makeList ( nodes, type ) {
|
||||
var makeList = function ( nodes, type ) {
|
||||
var i, l, node, tag, prev, replacement;
|
||||
for ( i = 0, l = nodes.length; i < l; i += 1 ) {
|
||||
node = nodes[i];
|
||||
|
@ -1158,7 +1171,7 @@
|
|||
}
|
||||
|
||||
setSelection( range );
|
||||
updatePath( 0, true );
|
||||
updatePath( range, true );
|
||||
|
||||
awaitingPaste = false;
|
||||
}, 0 );
|
||||
|
@ -1223,7 +1236,7 @@
|
|||
range._insertNode( createElement( 'BR' ) );
|
||||
range.collapse( false );
|
||||
setSelection( range );
|
||||
updatePath( 0, true );
|
||||
updatePath( range, true );
|
||||
docWasChanged();
|
||||
return;
|
||||
}
|
||||
|
@ -1315,7 +1328,7 @@
|
|||
nodeAfterSplit = child;
|
||||
}
|
||||
setSelection( createRange( nodeAfterSplit, 0 ) );
|
||||
updatePath( 0, true );
|
||||
updatePath( range, true );
|
||||
|
||||
// Scroll into view
|
||||
if ( nodeAfterSplit.nodeType === TEXT_NODE ) {
|
||||
|
@ -1337,7 +1350,7 @@
|
|||
event.preventDefault();
|
||||
range._deleteContents();
|
||||
setSelection( range );
|
||||
updatePath( 0, true );
|
||||
updatePath( range, true );
|
||||
}
|
||||
// If at beginning of block, merge with previous
|
||||
else if ( range.startsAtBlockBoundary() ) {
|
||||
|
@ -1370,7 +1383,7 @@
|
|||
return modifyBlocks( decreaseBlockQuoteLevel, range );
|
||||
}
|
||||
setSelection( range );
|
||||
updatePath( 0, true );
|
||||
updatePath( range, true );
|
||||
}
|
||||
}
|
||||
// All other cases can be safely left to the browser (I hope!).
|
||||
|
@ -1382,7 +1395,7 @@
|
|||
event.preventDefault();
|
||||
range._deleteContents();
|
||||
setSelection( range );
|
||||
updatePath( 0, true );
|
||||
updatePath( range, true );
|
||||
}
|
||||
// If at end of block, merge next into this block
|
||||
else if ( range.endsAtBlockBoundary() ) {
|
||||
|
@ -1402,7 +1415,7 @@
|
|||
next.mergeContainers();
|
||||
}
|
||||
setSelection( range );
|
||||
updatePath( 0, true );
|
||||
updatePath( range, true );
|
||||
}
|
||||
}
|
||||
// All other cases can be safely left to the browser (I hope!).
|
||||
|
@ -1476,11 +1489,20 @@
|
|||
|
||||
addStyles: function ( styles ) {
|
||||
if ( styles ) {
|
||||
var style = createElement( 'STYLE', {
|
||||
var head = doc.documentElement.firstChild,
|
||||
style = createElement( 'STYLE', {
|
||||
type: 'text/css'
|
||||
});
|
||||
style.appendChild( doc.createTextNode( styles ) );
|
||||
doc.documentElement.firstChild.appendChild( style );
|
||||
if ( style.styleSheet ) {
|
||||
// IE8: must append to document BEFORE adding styles
|
||||
// or you get the IE7 CSS parser!
|
||||
head.appendChild( style );
|
||||
style.styleSheet.cssText = styles;
|
||||
} else {
|
||||
// Everyone else
|
||||
style.appendChild( doc.createTextNode( styles ) );
|
||||
head.appendChild( style );
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
@ -1546,7 +1568,7 @@
|
|||
var range = createRange( body.firstChild, 0 );
|
||||
recordUndoState( range );
|
||||
setSelection( getRangeAndRemoveBookmark( range ) );
|
||||
updatePath( 0, true );
|
||||
updatePath( range, true );
|
||||
|
||||
return this;
|
||||
},
|
||||
|
|
|
@ -2,15 +2,18 @@
|
|||
|
||||
( function () {
|
||||
|
||||
/*global Node, Text, Element, window, document */
|
||||
/*global Node, Text, Element, HTMLDocument, window, document */
|
||||
|
||||
"use strict";
|
||||
|
||||
var implement = function ( constructor, props ) {
|
||||
var proto = constructor.prototype,
|
||||
prop;
|
||||
for ( prop in props ) {
|
||||
proto[ prop ] = props[ prop ];
|
||||
var implement = function ( constructors, props ) {
|
||||
var l = constructors.length,
|
||||
proto, prop;
|
||||
while ( l-- ) {
|
||||
proto = constructors[l].prototype;
|
||||
for ( prop in props ) {
|
||||
proto[ prop ] = props[ prop ];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -54,7 +57,7 @@ var isBlock = function ( el ) {
|
|||
};
|
||||
var useTextFixer = !!( window.opera || window.ie );
|
||||
|
||||
implement( Node, {
|
||||
implement( window.Node ? [ Node ] : [ Text, Element, HTMLDocument ], {
|
||||
isInline: $False,
|
||||
isBlock: $False,
|
||||
isContainer: $False,
|
||||
|
@ -101,7 +104,7 @@ implement( Node, {
|
|||
mergeContainers: function () {}
|
||||
});
|
||||
|
||||
implement( Text, {
|
||||
implement([ Text ], {
|
||||
isLeaf: $True,
|
||||
isInline: $True,
|
||||
getLength: function () {
|
||||
|
@ -119,7 +122,7 @@ implement( Text, {
|
|||
}
|
||||
});
|
||||
|
||||
implement( Element, {
|
||||
implement([ Element ], {
|
||||
isLeaf: function () {
|
||||
return !!leafNodeNames[ this.nodeName ];
|
||||
},
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
var implement = function ( constructor, props ) {
|
||||
var proto = constructor.prototype,
|
||||
prop;
|
||||
var implement = function ( proto, props ) {
|
||||
var prop;
|
||||
for ( prop in props ) {
|
||||
proto[ prop ] = props[ prop ];
|
||||
}
|
||||
|
@ -50,7 +49,7 @@ var getNodeAfter = function ( node, offset ) {
|
|||
return node;
|
||||
};
|
||||
|
||||
implement( Range, {
|
||||
implement( Range.prototype, {
|
||||
|
||||
forEachTextNode: function ( fn ) {
|
||||
var range = this.cloneRange();
|
||||
|
@ -97,21 +96,30 @@ implement( Range, {
|
|||
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 ) {
|
||||
afterSplit = startContainer.splitText( startOffset );
|
||||
if ( endContainer === startContainer ) {
|
||||
endOffset -= startOffset;
|
||||
endContainer = afterSplit;
|
||||
if ( startOffset === startContainer.length ) {
|
||||
startOffset = indexOf.call( children, startContainer ) + 1;
|
||||
if ( this.collapsed ) {
|
||||
endContainer = parent;
|
||||
endOffset = startOffset;
|
||||
}
|
||||
else if ( endContainer === parent ) {
|
||||
endOffset += 1;
|
||||
} else {
|
||||
if ( startOffset ) {
|
||||
afterSplit = startContainer.splitText( startOffset );
|
||||
if ( endContainer === startContainer ) {
|
||||
endOffset -= startOffset;
|
||||
endContainer = afterSplit;
|
||||
}
|
||||
else if ( endContainer === parent ) {
|
||||
endOffset += 1;
|
||||
}
|
||||
startContainer = afterSplit;
|
||||
}
|
||||
startContainer = afterSplit;
|
||||
startOffset = indexOf.call( children, startContainer );
|
||||
}
|
||||
startOffset = indexOf.call( children, startContainer );
|
||||
startContainer = parent;
|
||||
} else {
|
||||
children = startContainer.childNodes;
|
||||
|
|
|
@ -49,6 +49,9 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!--[if IE 8]>
|
||||
<script type="text/javascript" src="ie8.js"></script>
|
||||
<![endif]-->
|
||||
<!--[if IE 9]>
|
||||
<script type="text/javascript">window.ie = 9;</script>
|
||||
<![endif]-->
|
||||
|
|
157
source/ie8dom.js
Normal file
157
source/ie8dom.js
Normal file
|
@ -0,0 +1,157 @@
|
|||
/* Copyright © 2011 by Neil Jenkins. Licensed under the MIT license. */
|
||||
|
||||
( function () {
|
||||
|
||||
/*global window, document, Element, HTMLDocument */
|
||||
/*jshint strict: false */
|
||||
|
||||
var doc = document;
|
||||
|
||||
// Add JS hook
|
||||
window.ie = 8;
|
||||
|
||||
// Add defaultView property to document
|
||||
doc.defaultView = window;
|
||||
|
||||
// Fake W3C events support
|
||||
var translate = {
|
||||
focus: 'focusin',
|
||||
blur: 'focusout'
|
||||
};
|
||||
|
||||
var returnTrue = function () { return true; };
|
||||
var returnFalse = function () { return false; };
|
||||
|
||||
var toCopy = 'altKey ctrlKey metaKey shiftKey clientX clientY charCode keyCode'.split( ' ' );
|
||||
|
||||
var DOMEvent = function ( event ) {
|
||||
var type = event.type,
|
||||
doc = document,
|
||||
target = event.srcElement || doc,
|
||||
html = ( target.ownerDocument || doc ).documentElement,
|
||||
l = toCopy.length,
|
||||
property;
|
||||
|
||||
while ( l-- ) {
|
||||
property = toCopy[l];
|
||||
this[ property ] = event[ property ];
|
||||
}
|
||||
|
||||
if ( type === 'propertychange' ) {
|
||||
type = ( target.nodeName === 'INPUT' &&
|
||||
target.type !== 'text' && target.type !== 'password' ) ?
|
||||
'change' : 'input';
|
||||
}
|
||||
|
||||
this.type = Object.keyOf( translate, type ) || type;
|
||||
this.target = target;
|
||||
this.pageX = event.clientX + html.scrollLeft;
|
||||
this.pageY = event.clientY + html.scrollTop;
|
||||
|
||||
if ( event.button ) {
|
||||
this.button = ( event.button & 4 ? 1 :
|
||||
( event.button & 2 ? 2 : 0 ) );
|
||||
this.which = this.button + 1;
|
||||
}
|
||||
|
||||
this.relatedTarget = event.fromElement === target ?
|
||||
event.toElement : event.fromElement;
|
||||
this._event = event;
|
||||
};
|
||||
|
||||
DOMEvent.prototype = {
|
||||
constructor: DOMEvent,
|
||||
isEvent: true,
|
||||
preventDefault: function () {
|
||||
this.isDefaultPrevented = returnTrue;
|
||||
this._event.returnValue = false;
|
||||
},
|
||||
stopPropagation: function () {
|
||||
this.isPropagationStopped = returnTrue;
|
||||
this._event.cancelBubble = true;
|
||||
},
|
||||
isDefaultPrevented: returnFalse,
|
||||
isPropagationStopped: returnFalse
|
||||
};
|
||||
|
||||
// Add W3C event add/remove methods to elements and document.
|
||||
[ doc, Element.prototype ].forEach(
|
||||
function ( dom ) {
|
||||
dom.addEventListener = function ( type, handler, capture ) {
|
||||
var fn = handler._ie_handleEvent || ( handler._ie_handleEvent =
|
||||
function () {
|
||||
var event = new DOMEvent( window.event );
|
||||
if ( typeof handler === 'object' ) {
|
||||
handler.handleEvent( event );
|
||||
} else {
|
||||
handler.call( this, event );
|
||||
}
|
||||
}
|
||||
),
|
||||
node = /paste|cut/.test( type ) ? this.body || this : this;
|
||||
|
||||
handler._ie_registeredCount = ( handler._ie_registeredCount || 0 ) + 1;
|
||||
|
||||
node.attachEvent( 'on' + ( translate[ type ] || type ), fn );
|
||||
};
|
||||
dom.addEventListener.isFake = true;
|
||||
|
||||
dom.removeEventListener = function ( type, handler, capture ) {
|
||||
var fn = handler._ie_handleEvent,
|
||||
node = /paste|cut/.test( type ) ? this.body || this : this;
|
||||
if ( !( handler._ie_registeredCount -= 1 ) ) {
|
||||
delete handler._ie_handleEvent;
|
||||
}
|
||||
if ( fn ) {
|
||||
node.detachEvent( 'on' + ( translate[ type ] || type ), fn );
|
||||
}
|
||||
};
|
||||
dom.removeEventListener.isFake = true;
|
||||
});
|
||||
|
||||
// The events that we normally attach to the window object, IE8 wants on the
|
||||
// body.
|
||||
doc.defaultView.addEventListener = function ( type, handler, capture ) {
|
||||
return doc.addEventListener( type, handler, capture );
|
||||
};
|
||||
|
||||
// Add textContent property to elements.
|
||||
Object.defineProperty( Element.prototype, 'textContent', {
|
||||
get: function () {
|
||||
return this.innerText;
|
||||
},
|
||||
|
||||
set: function ( text ) {
|
||||
this.innerText = text;
|
||||
}
|
||||
});
|
||||
|
||||
// Add compareDocumentPosition method to elements.
|
||||
Element.prototype.compareDocumentPosition = function ( b ) {
|
||||
if ( b.nodeType !== 1 ) { b = b.parentNode; }
|
||||
var a = this,
|
||||
different = ( a !== b ),
|
||||
aIndex = a.sourceIndex,
|
||||
bIndex = b.sourceIndex;
|
||||
|
||||
return ( different && a.contains( b ) ? 16 : 0 ) +
|
||||
( different && b.contains( a ) ? 8 : 0 ) +
|
||||
( aIndex < bIndex ? 4 : 0 ) +
|
||||
( bIndex < aIndex ? 2 : 0 );
|
||||
};
|
||||
|
||||
// Add normalize method to document fragments
|
||||
HTMLDocument.prototype.normalize = function () {
|
||||
var children = this.childNodes,
|
||||
l = children.length,
|
||||
child;
|
||||
|
||||
while ( l-- ) {
|
||||
child = children[l];
|
||||
if ( child.nodeType === 1 ) {
|
||||
child.normalize();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}() );
|
489
source/ie8range.js
Normal file
489
source/ie8range.js
Normal file
|
@ -0,0 +1,489 @@
|
|||
/* Copyright © 2011 by Neil Jenkins. Licensed under the MIT license.
|
||||
|
||||
IE TextRange <-> W3C Range code adapted from Rangy:
|
||||
http://code.google.com/p/rangy/
|
||||
Copyright 2012, Tim Down. Licensed under the MIT license.
|
||||
*/
|
||||
|
||||
var Range;
|
||||
|
||||
( function () {
|
||||
|
||||
/*global window, document */
|
||||
/*jshint strict: false */
|
||||
|
||||
var indexOf = Array.prototype.indexOf;
|
||||
|
||||
var START_TO_START = 0;
|
||||
var START_TO_END = 1;
|
||||
var END_TO_END = 2;
|
||||
var END_TO_START = 3;
|
||||
|
||||
var contains = function ( a, b ) {
|
||||
while ( b = b.parentNode ) {
|
||||
if ( a === b ) { return true; }
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
var getCommonAncestor = function ( a, b ) {
|
||||
var commonAncestor,
|
||||
aParents, bParents,
|
||||
aL, bL;
|
||||
|
||||
if ( a === b || contains( a, b ) ) {
|
||||
commonAncestor = a;
|
||||
} else if ( contains( b, a ) ) {
|
||||
commonAncestor = b;
|
||||
} else {
|
||||
aParents = [];
|
||||
bParents = [];
|
||||
while ( a = a.parentNode ) {
|
||||
aParents.push( a );
|
||||
}
|
||||
while ( b = b.parentNode ) {
|
||||
bParents.push( b );
|
||||
}
|
||||
aL = aParents.length;
|
||||
bL = bParents.length;
|
||||
while ( aL-- && bL-- ) {
|
||||
if ( aParents[ aL ] !== bParents[ bL ] ) {
|
||||
commonAncestor = aParents[ aL + 1 ];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( !commonAncestor ) {
|
||||
commonAncestor = ( aL === -1 ? aParents[0] : bParents[0] );
|
||||
}
|
||||
}
|
||||
return commonAncestor;
|
||||
};
|
||||
|
||||
Range = function ( startContainer, startOffset, endContainer, endOffset ) {
|
||||
startContainer = startContainer || document;
|
||||
startOffset = startOffset || 0;
|
||||
|
||||
this.startContainer = startContainer;
|
||||
this.startOffset = startOffset;
|
||||
this.endContainer = endContainer || startContainer;
|
||||
this.endOffset = endOffset !== undefined ? endOffset : startOffset;
|
||||
this._updateCollapsedAndAncestor();
|
||||
};
|
||||
|
||||
Range.prototype = {
|
||||
constructor: Range,
|
||||
|
||||
_updateCollapsedAndAncestor: function () {
|
||||
this.collapsed = (
|
||||
this.startContainer === this.endContainer &&
|
||||
this.startOffset === this.endOffset
|
||||
);
|
||||
this.commonAncestorContainer =
|
||||
getCommonAncestor( this.startContainer, this.endContainer );
|
||||
},
|
||||
setStart: function ( node, offset ) {
|
||||
this.startContainer = node;
|
||||
this.startOffset = offset;
|
||||
this._updateCollapsedAndAncestor();
|
||||
},
|
||||
setEnd: function ( node, offset ) {
|
||||
this.endContainer = node;
|
||||
this.endOffset = offset;
|
||||
this._updateCollapsedAndAncestor();
|
||||
},
|
||||
setStartAfter: function ( node ) {
|
||||
var parent = node.parentNode;
|
||||
this.setStart( parent, indexOf.call( parent.childNodes, node ) + 1 );
|
||||
},
|
||||
setEndBefore: function ( node ) {
|
||||
var parent = node.parentNode;
|
||||
this.setEnd( parent, indexOf.call( parent.childNodes, node ) );
|
||||
},
|
||||
selectNode: function ( node ) {
|
||||
var parent = node.parentNode,
|
||||
offset = indexOf.call( parent.childNodes, node );
|
||||
this.setStart( parent, offset );
|
||||
this.setEnd( parent, offset + 1 );
|
||||
},
|
||||
selectNodeContents: function ( node ) {
|
||||
this.setStart( node, 0 );
|
||||
this.setEnd( node, node.childNodes.length );
|
||||
},
|
||||
cloneRange: function () {
|
||||
return new Range(
|
||||
this.startContainer,
|
||||
this.startOffset,
|
||||
this.endContainer,
|
||||
this.endOffset
|
||||
);
|
||||
},
|
||||
collapse: function ( toStart ) {
|
||||
if ( toStart ) {
|
||||
this.setEnd( this.startContainer, this.startOffset );
|
||||
} else {
|
||||
this.setStart( this.endContainer, this.endOffset );
|
||||
}
|
||||
},
|
||||
compareBoundaryPoints: function ( how, sourceRange ) {
|
||||
var aContainer, aOffset, bContainer, bOffset, node, parent;
|
||||
if ( how === START_TO_START || how === END_TO_START ) {
|
||||
aContainer = this.startContainer;
|
||||
aOffset = this.startOffset;
|
||||
} else {
|
||||
aContainer = this.endContainer;
|
||||
aOffset = this.endOffset;
|
||||
}
|
||||
if ( how === START_TO_START || how === START_TO_END ) {
|
||||
bContainer = sourceRange.startContainer;
|
||||
bOffset = sourceRange.startOffset;
|
||||
} else {
|
||||
bContainer = sourceRange.endContainer;
|
||||
bOffset = sourceRange.endOffset;
|
||||
}
|
||||
if ( aContainer === bContainer ) {
|
||||
return aOffset < bOffset ? -1 :
|
||||
aOffset > bOffset ? 1 : 0;
|
||||
}
|
||||
|
||||
node = aContainer;
|
||||
while ( parent = node.parentNode ) {
|
||||
if ( parent === bContainer ) {
|
||||
return indexOf.call( parent.childNodes, node ) < bOffset ?
|
||||
-1 : 1;
|
||||
}
|
||||
node = parent;
|
||||
}
|
||||
node = bContainer;
|
||||
while ( parent = node.parentNode ) {
|
||||
if ( parent === aContainer ) {
|
||||
return indexOf.call( parent.childNodes, node ) < aOffset ?
|
||||
1 : -1;
|
||||
}
|
||||
node = parent;
|
||||
}
|
||||
if ( aContainer.nodeType !== 1 ) {
|
||||
aContainer = aContainer.parentNode;
|
||||
}
|
||||
if ( bContainer.nodeType !== 1 ) {
|
||||
bContainer = bContainer.parentNode;
|
||||
}
|
||||
return aContainer.sourceIndex < bContainer.sourceIndex ? -1 :
|
||||
aContainer.sourceIndex > bContainer.sourceIndex ? 1 : 0;
|
||||
}
|
||||
};
|
||||
|
||||
document.createRange = function () {
|
||||
return new Range();
|
||||
};
|
||||
|
||||
// ---
|
||||
|
||||
var isAncestorOf = function ( ancestor, descendant ) {
|
||||
return ancestor === descendant || contains( ancestor, descendant );
|
||||
};
|
||||
|
||||
var isCharacterDataNode = function ( node ) {
|
||||
var nodeType = node.nodeType;
|
||||
// Text, CDataSection or Comment
|
||||
return nodeType === 3 || nodeType === 4 || nodeType === 8;
|
||||
};
|
||||
|
||||
var DomPosition = function ( node, offset ) {
|
||||
this.node = node;
|
||||
this.offset = offset;
|
||||
};
|
||||
|
||||
var getTextRangeContainerElement = function ( textRange ) {
|
||||
var parentEl = textRange.parentElement(),
|
||||
range, startEl, endEl, startEndContainer;
|
||||
|
||||
range = textRange.duplicate();
|
||||
range.collapse( true );
|
||||
startEl = range.parentElement();
|
||||
range = textRange.duplicate();
|
||||
range.collapse( false );
|
||||
endEl = range.parentElement();
|
||||
startEndContainer = ( startEl === endEl ) ?
|
||||
startEl : getCommonAncestor( startEl, endEl );
|
||||
|
||||
return startEndContainer === parentEl ?
|
||||
startEndContainer : getCommonAncestor( parentEl, startEndContainer );
|
||||
};
|
||||
|
||||
// Gets the boundary of a TextRange expressed as a node and an offset within
|
||||
// that node. This function started out as an improved version of code found in
|
||||
// Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has grown,
|
||||
// fixing problems with line breaks in preformatted text, adding workaround for
|
||||
// IE TextRange bugs, handling for inputs and images, plus optimizations.
|
||||
var getTextRangeBoundaryPosition = function (
|
||||
textRange, wholeRangeContainerElement, isStart, isCollapsed ) {
|
||||
var workingRange = textRange.duplicate();
|
||||
|
||||
workingRange.collapse( isStart );
|
||||
|
||||
var containerElement = workingRange.parentElement();
|
||||
|
||||
// Sometimes collapsing a TextRange that's at the start of a text node can
|
||||
// move it into the previous node, so check for that TODO: Find out when.
|
||||
// Workaround for wholeRangeContainerElement may break this
|
||||
if ( !isAncestorOf( wholeRangeContainerElement, containerElement ) ) {
|
||||
containerElement = wholeRangeContainerElement;
|
||||
}
|
||||
|
||||
// Deal with nodes that cannot "contain rich HTML markup". In practice, this
|
||||
// means form inputs, images and similar. See
|
||||
// http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
|
||||
if ( !containerElement.canHaveHTML ) {
|
||||
return new DomPosition(
|
||||
containerElement.parentNode,
|
||||
indexOf.call(
|
||||
containerElement.parentNode.childNodes, containerElement )
|
||||
);
|
||||
}
|
||||
|
||||
var workingNode = document.createElement( 'span' ),
|
||||
workingComparisonType = isStart ? 'StartToStart' : 'StartToEnd',
|
||||
comparison, previousNode, nextNode, boundaryPosition, boundaryNode;
|
||||
|
||||
// Move the working range through the container's children, starting at the
|
||||
// end and working backwards, until the working range reaches or goes past
|
||||
// the boundary we're interested in
|
||||
do {
|
||||
containerElement.insertBefore(
|
||||
workingNode, workingNode.previousSibling );
|
||||
workingRange.moveToElementText( workingNode );
|
||||
comparison =
|
||||
workingRange.compareEndPoints( workingComparisonType, textRange );
|
||||
} while ( comparison > 0 && workingNode.previousSibling );
|
||||
|
||||
// We've now reached or gone past the boundary of the text range we're
|
||||
// interested in so have identified the node we want
|
||||
boundaryNode = workingNode.nextSibling;
|
||||
|
||||
if ( comparison === -1 && boundaryNode &&
|
||||
isCharacterDataNode( boundaryNode ) ) {
|
||||
// This is a character data node (text, comment, cdata). The working
|
||||
// range is collapsed at the start of the node containing the text
|
||||
// range's boundary, so we move the end of the working range to the
|
||||
// boundary point and measure the length of its text to get the
|
||||
// boundary's offset within the node.
|
||||
workingRange.setEndPoint(
|
||||
isStart ? 'EndToStart' : 'EndToEnd', textRange );
|
||||
|
||||
var offset;
|
||||
|
||||
if ( /[\r\n]/.test( boundaryNode.data ) ||
|
||||
/[\r\n]/.test( workingRange.text ) ) {
|
||||
/*
|
||||
For the particular case of a boundary within a text node containing
|
||||
line breaks (within a <pre> element, for example), we need a
|
||||
slightly complicated approach to get the boundary's offset in IE.
|
||||
The facts:
|
||||
|
||||
- Each line break is represented as \r in the text node's
|
||||
data/nodeValue properties
|
||||
- Each line break is represented as \r\n in the TextRange's 'text'
|
||||
property
|
||||
- The 'text' property of the TextRange does not contain trailing
|
||||
line breaks
|
||||
|
||||
To get round the problem presented by the final fact above, we can
|
||||
use the fact that TextRange's moveStart() and moveEnd() methods
|
||||
return the actual number of characters moved, which is not
|
||||
necessarily the same as the number of characters it was instructed
|
||||
to move. The simplest approach is to use this to store the
|
||||
characters moved when moving both the start and end of the range to
|
||||
the start of the document body and subtracting the start offset from
|
||||
the end offset (the "move-negative-gazillion" method). However, this
|
||||
is extremely slow when the document is large and the range is near
|
||||
the end of it. Clearly doing the mirror image (i.e. moving the range
|
||||
boundaries to the end of the document) has the same problem.
|
||||
|
||||
Another approach that works is to use moveStart() to move the start
|
||||
boundary of the range up to the end boundary one character at a time
|
||||
and incrementing a counter with the value returned by the
|
||||
moveStart() call. However, the check for whether the start boundary
|
||||
has reached the end boundary is expensive, so this method is slow
|
||||
(although unlike "move-negative-gazillion" is largely unaffected by
|
||||
the location of the range within the document).
|
||||
|
||||
The method below is a hybrid of the two methods above. It uses the
|
||||
fact that a string containing the TextRange's 'text' property with
|
||||
each \r\n converted to a single \r character cannot be longer than
|
||||
the text of the TextRange, so the start of the range is moved that
|
||||
length initially and then a character at a time to make up for any
|
||||
trailing line breaks not contained in the 'text' property. This has
|
||||
good performance in most situations compared to the previous two
|
||||
methods.
|
||||
*/
|
||||
var tempRange = workingRange.duplicate();
|
||||
var rangeLength = tempRange.text.replace( /\r\n/g, '\r' ).length;
|
||||
|
||||
offset = tempRange.moveStart( 'character', rangeLength);
|
||||
while ( ( comparison =
|
||||
tempRange.compareEndPoints( 'StartToEnd', tempRange )
|
||||
) === -1 ) {
|
||||
offset += 1;
|
||||
tempRange.moveStart( 'character', 1 );
|
||||
}
|
||||
} else {
|
||||
offset = workingRange.text.length;
|
||||
}
|
||||
boundaryPosition = new DomPosition( boundaryNode, offset );
|
||||
}
|
||||
else {
|
||||
// If the boundary immediately follows a character data node and this is
|
||||
// the end boundary, we should favour a position within that, and
|
||||
// likewise for a start boundary preceding a character data node
|
||||
previousNode = ( isCollapsed || !isStart ) &&
|
||||
workingNode.previousSibling;
|
||||
nextNode = ( isCollapsed || isStart ) && workingNode.nextSibling;
|
||||
|
||||
if ( nextNode && isCharacterDataNode( nextNode ) ) {
|
||||
boundaryPosition = new DomPosition( nextNode, 0 );
|
||||
} else if ( previousNode && isCharacterDataNode( previousNode ) ) {
|
||||
// Strange bug; if we don't read the data property, the length
|
||||
// property is often returned incorrectly as 0. Don't ask me why.
|
||||
/*jshint expr: true */
|
||||
previousNode.data;
|
||||
/*jshint expr: false */
|
||||
boundaryPosition = new DomPosition(
|
||||
previousNode, previousNode.length );
|
||||
} else {
|
||||
boundaryPosition = new DomPosition(
|
||||
containerElement,
|
||||
indexOf.call( containerElement.childNodes, workingNode )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
workingNode.parentNode.removeChild( workingNode );
|
||||
|
||||
return boundaryPosition;
|
||||
};
|
||||
|
||||
// Returns a TextRange representing the boundary of a TextRange expressed as a
|
||||
// node and an offset within that node. This function started out as an
|
||||
// optimized version of code found in Tim Cameron Ryan's IERange
|
||||
// (http://code.google.com/p/ierange/)
|
||||
var createBoundaryTextRange = function ( boundaryPosition, isStart ) {
|
||||
var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
|
||||
var doc = document;
|
||||
var workingNode, childNodes, workingRange = doc.body.createTextRange();
|
||||
var nodeIsDataNode = isCharacterDataNode( boundaryPosition.node );
|
||||
|
||||
if ( nodeIsDataNode ) {
|
||||
boundaryNode = boundaryPosition.node;
|
||||
boundaryParent = boundaryNode.parentNode;
|
||||
} else {
|
||||
childNodes = boundaryPosition.node.childNodes;
|
||||
boundaryNode = ( boundaryOffset < childNodes.length ) ?
|
||||
childNodes[ boundaryOffset ] : null;
|
||||
boundaryParent = boundaryPosition.node;
|
||||
}
|
||||
|
||||
// Position the range immediately before the node containing the boundary
|
||||
workingNode = doc.createElement( 'span' );
|
||||
|
||||
// Making the working element non-empty element persuades IE to consider the
|
||||
// TextRange boundary to be within the element rather than immediately
|
||||
// before or after it, which is what we want
|
||||
workingNode.innerHTML = '';
|
||||
|
||||
// insertBefore is supposed to work like appendChild if the second parameter
|
||||
// is null. However, a bug report for IERange suggests that it can crash the
|
||||
// browser: http://code.google.com/p/ierange/issues/detail?id=12
|
||||
if ( boundaryNode ) {
|
||||
boundaryParent.insertBefore( workingNode, boundaryNode );
|
||||
} else {
|
||||
boundaryParent.appendChild( workingNode );
|
||||
}
|
||||
|
||||
workingRange.moveToElementText( workingNode );
|
||||
workingRange.collapse( !isStart );
|
||||
|
||||
// Clean up
|
||||
boundaryParent.removeChild( workingNode );
|
||||
|
||||
// Move the working range to the text offset, if required
|
||||
if ( nodeIsDataNode ) {
|
||||
workingRange[ isStart ? 'moveStart' : 'moveEnd' ](
|
||||
'character', boundaryOffset );
|
||||
}
|
||||
|
||||
return workingRange;
|
||||
};
|
||||
|
||||
var toDOMRange = function ( textRange ) {
|
||||
var rangeContainerElement = getTextRangeContainerElement( textRange ),
|
||||
start, end;
|
||||
|
||||
if ( textRange.compareEndPoints( 'StartToEnd', textRange ) === 0 ) {
|
||||
start = end = getTextRangeBoundaryPosition(
|
||||
textRange, rangeContainerElement, true, true );
|
||||
} else {
|
||||
start = getTextRangeBoundaryPosition(
|
||||
textRange, rangeContainerElement, true, false );
|
||||
end = getTextRangeBoundaryPosition(
|
||||
textRange, rangeContainerElement, false, false );
|
||||
}
|
||||
return new Range(
|
||||
start.node,
|
||||
start.offset,
|
||||
end.node,
|
||||
end.offset
|
||||
);
|
||||
};
|
||||
|
||||
var toTextRange = function ( range ) {
|
||||
var textRange, startRange, endRange;
|
||||
if ( range.collapsed ) {
|
||||
textRange = createBoundaryTextRange(
|
||||
new DomPosition( range.startContainer, range.startOffset ), true);
|
||||
} else {
|
||||
startRange = createBoundaryTextRange(
|
||||
new DomPosition( range.startContainer, range.startOffset ), true);
|
||||
endRange = createBoundaryTextRange(
|
||||
new DomPosition( range.endContainer, range.endOffset ), false );
|
||||
textRange = document.body.createTextRange();
|
||||
textRange.setEndPoint( 'StartToStart', startRange);
|
||||
textRange.setEndPoint( 'EndToEnd', endRange);
|
||||
}
|
||||
return textRange;
|
||||
};
|
||||
|
||||
var selection = {
|
||||
rangeCount: 1,
|
||||
getRangeAt: function ( index ) {
|
||||
if ( index !== 0 ) { return undefined; }
|
||||
var sel = document.selection.createRange();
|
||||
// Check if we have a control range.
|
||||
if ( sel.add ) {
|
||||
var range = document.createRange();
|
||||
range.moveToElementText( sel.item( 0 ) );
|
||||
range.collapse( false );
|
||||
range.select();
|
||||
sel = range;
|
||||
}
|
||||
return toDOMRange( sel );
|
||||
},
|
||||
removeAllRanges: function () {},
|
||||
addRange: function ( range ) {
|
||||
toTextRange( range ).select();
|
||||
}
|
||||
};
|
||||
|
||||
document.attachEvent( 'onbeforeactivate', function () {
|
||||
selection.rangeCount = 1;
|
||||
});
|
||||
|
||||
document.attachEvent( 'ondeactivate', function () {
|
||||
selection.rangeCount = 0;
|
||||
});
|
||||
|
||||
window.getSelection = function () {
|
||||
return selection;
|
||||
};
|
||||
|
||||
}() );
|
63
source/ie8types.js
Normal file
63
source/ie8types.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
/* Copyright © 2011 by Neil Jenkins. Licensed under the MIT license. */
|
||||
|
||||
( function ( undefined ) {
|
||||
|
||||
/*jshint strict: false */
|
||||
|
||||
// Note: Does not inclue the `if ( i in this ) {}` check these function should
|
||||
// have, as IE8 will return false if this[i] is undefined (at least if the array
|
||||
// was defined with a literal, e.g. `[ undefined, undefined ]`).
|
||||
|
||||
Array.prototype.indexOf = function ( item, from ) {
|
||||
var l = this.length;
|
||||
for ( var i = ( from < 0 ) ? Math.max( 0, l + from ) : from || 0;
|
||||
i < l; i += 1 ) {
|
||||
if ( this[i] === item ) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
Array.prototype.forEach = function ( fn, bind ) {
|
||||
var l = this.length >>> 0;
|
||||
if ( typeof fn !== 'function' ) {
|
||||
throw new TypeError();
|
||||
}
|
||||
for ( var i = 0; i < l; i += 1 ) {
|
||||
fn.call( bind, this[i], i, this );
|
||||
}
|
||||
};
|
||||
|
||||
Array.prototype.filter = function ( fn, bind ) {
|
||||
var results = [];
|
||||
for ( var i = 0, l = this.length; i < l; i += 1 ) {
|
||||
var value = this[i];
|
||||
if ( fn.call( bind, value, i, this ) ) {
|
||||
results.push( value );
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
Object.keyOf = function ( object, value ) {
|
||||
for ( var key in object ) {
|
||||
if ( object[ key ] === value ) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Date.now = function () {
|
||||
return +( new Date() );
|
||||
};
|
||||
|
||||
String.prototype.trim = function () {
|
||||
var str = this.replace( /^\s\s*/, '' ),
|
||||
ws = /\s/,
|
||||
i = str.length;
|
||||
while ( ws.test( str.charAt( i -= 1 ) ) ) {/* Empty! */}
|
||||
return str.slice( 0, i + 1 );
|
||||
};
|
||||
|
||||
}() );
|
Loading…
Reference in a new issue