ripple-core
Version:
Ripple is an interactive audience response system that allows presenters to survey audience members in real time communication through their mobile devices.
1,437 lines (1,192 loc) • 332 kB
JavaScript
/**
* @license wysihtml5 v0.3.0
* https://github.com/xing/wysihtml5
*
* Author: Christopher Blum (https://github.com/tiff)
*
* Copyright (C) 2012 XING AG
* Licensed under the MIT license (MIT)
*
*/
var wysihtml5 = {
version: "0.3.0",
// namespaces
commands: {},
dom: {},
quirks: {},
toolbar: {},
lang: {},
selection: {},
views: {},
INVISIBLE_SPACE: "\uFEFF",
EMPTY_FUNCTION: function() {},
ELEMENT_NODE: 1,
TEXT_NODE: 3,
BACKSPACE_KEY: 8,
ENTER_KEY: 13,
ESCAPE_KEY: 27,
SPACE_KEY: 32,
DELETE_KEY: 46
};/**
* @license Rangy, a cross-browser JavaScript range and selection library
* http://code.google.com/p/rangy/
*
* Copyright 2011, Tim Down
* Licensed under the MIT license.
* Version: 1.2.2
* Build date: 13 November 2011
*/
window['rangy'] = (function() {
var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
"commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"];
var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
"setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
"extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
// Subset of TextRange's full set of methods that we're interested in
var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark",
"moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"];
/*----------------------------------------------------------------------------------------------------------------*/
// Trio of functions taken from Peter Michaux's article:
// http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
function isHostMethod(o, p) {
var t = typeof o[p];
return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
}
function isHostObject(o, p) {
return !!(typeof o[p] == OBJECT && o[p]);
}
function isHostProperty(o, p) {
return typeof o[p] != UNDEFINED;
}
// Creates a convenience function to save verbose repeated calls to tests functions
function createMultiplePropertyTest(testFunc) {
return function(o, props) {
var i = props.length;
while (i--) {
if (!testFunc(o, props[i])) {
return false;
}
}
return true;
};
}
// Next trio of functions are a convenience to save verbose repeated calls to previous two functions
var areHostMethods = createMultiplePropertyTest(isHostMethod);
var areHostObjects = createMultiplePropertyTest(isHostObject);
var areHostProperties = createMultiplePropertyTest(isHostProperty);
function isTextRange(range) {
return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
}
var api = {
version: "1.2.2",
initialized: false,
supported: true,
util: {
isHostMethod: isHostMethod,
isHostObject: isHostObject,
isHostProperty: isHostProperty,
areHostMethods: areHostMethods,
areHostObjects: areHostObjects,
areHostProperties: areHostProperties,
isTextRange: isTextRange
},
features: {},
modules: {},
config: {
alertOnWarn: false,
preferTextRange: false
}
};
function fail(reason) {
window.alert("Rangy not supported in your browser. Reason: " + reason);
api.initialized = true;
api.supported = false;
}
api.fail = fail;
function warn(msg) {
var warningMessage = "Rangy warning: " + msg;
if (api.config.alertOnWarn) {
window.alert(warningMessage);
} else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {
window.console.log(warningMessage);
}
}
api.warn = warn;
if ({}.hasOwnProperty) {
api.util.extend = function(o, props) {
for (var i in props) {
if (props.hasOwnProperty(i)) {
o[i] = props[i];
}
}
};
} else {
fail("hasOwnProperty not supported");
}
var initListeners = [];
var moduleInitializers = [];
// Initialization
function init() {
if (api.initialized) {
return;
}
var testRange;
var implementsDomRange = false, implementsTextRange = false;
// First, perform basic feature tests
if (isHostMethod(document, "createRange")) {
testRange = document.createRange();
if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
implementsDomRange = true;
}
testRange.detach();
}
var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
if (body && isHostMethod(body, "createTextRange")) {
testRange = body.createTextRange();
if (isTextRange(testRange)) {
implementsTextRange = true;
}
}
if (!implementsDomRange && !implementsTextRange) {
fail("Neither Range nor TextRange are implemented");
}
api.initialized = true;
api.features = {
implementsDomRange: implementsDomRange,
implementsTextRange: implementsTextRange
};
// Initialize modules and call init listeners
var allListeners = moduleInitializers.concat(initListeners);
for (var i = 0, len = allListeners.length; i < len; ++i) {
try {
allListeners[i](api);
} catch (ex) {
if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {
window.console.log("Init listener threw an exception. Continuing.", ex);
}
}
}
}
// Allow external scripts to initialize this library in case it's loaded after the document has loaded
api.init = init;
// Execute listener immediately if already initialized
api.addInitListener = function(listener) {
if (api.initialized) {
listener(api);
} else {
initListeners.push(listener);
}
};
var createMissingNativeApiListeners = [];
api.addCreateMissingNativeApiListener = function(listener) {
createMissingNativeApiListeners.push(listener);
};
function createMissingNativeApi(win) {
win = win || window;
init();
// Notify listeners
for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {
createMissingNativeApiListeners[i](win);
}
}
api.createMissingNativeApi = createMissingNativeApi;
/**
* @constructor
*/
function Module(name) {
this.name = name;
this.initialized = false;
this.supported = false;
}
Module.prototype.fail = function(reason) {
this.initialized = true;
this.supported = false;
throw new Error("Module '" + this.name + "' failed to load: " + reason);
};
Module.prototype.warn = function(msg) {
api.warn("Module " + this.name + ": " + msg);
};
Module.prototype.createError = function(msg) {
return new Error("Error in Rangy " + this.name + " module: " + msg);
};
api.createModule = function(name, initFunc) {
var module = new Module(name);
api.modules[name] = module;
moduleInitializers.push(function(api) {
initFunc(api, module);
module.initialized = true;
module.supported = true;
});
};
api.requireModules = function(modules) {
for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) {
moduleName = modules[i];
module = api.modules[moduleName];
if (!module || !(module instanceof Module)) {
throw new Error("Module '" + moduleName + "' not found");
}
if (!module.supported) {
throw new Error("Module '" + moduleName + "' not supported");
}
}
};
/*----------------------------------------------------------------------------------------------------------------*/
// Wait for document to load before running tests
var docReady = false;
var loadHandler = function(e) {
if (!docReady) {
docReady = true;
if (!api.initialized) {
init();
}
}
};
// Test whether we have window and document objects that we will need
if (typeof window == UNDEFINED) {
fail("No window found");
return;
}
if (typeof document == UNDEFINED) {
fail("No document found");
return;
}
if (isHostMethod(document, "addEventListener")) {
document.addEventListener("DOMContentLoaded", loadHandler, false);
}
// Add a fallback in case the DOMContentLoaded event isn't supported
if (isHostMethod(window, "addEventListener")) {
window.addEventListener("load", loadHandler, false);
} else if (isHostMethod(window, "attachEvent")) {
window.attachEvent("onload", loadHandler);
} else {
fail("Window does not have required addEventListener or attachEvent method");
}
return api;
})();
rangy.createModule("DomUtil", function(api, module) {
var UNDEF = "undefined";
var util = api.util;
// Perform feature tests
if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
module.fail("document missing a Node creation method");
}
if (!util.isHostMethod(document, "getElementsByTagName")) {
module.fail("document missing getElementsByTagName method");
}
var el = document.createElement("div");
if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
!util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
module.fail("Incomplete Element implementation");
}
// innerHTML is required for Range's createContextualFragment method
if (!util.isHostProperty(el, "innerHTML")) {
module.fail("Element is missing innerHTML property");
}
var textNode = document.createTextNode("test");
if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
!util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
!util.areHostProperties(textNode, ["data"]))) {
module.fail("Incomplete Text Node implementation");
}
/*----------------------------------------------------------------------------------------------------------------*/
// Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
// able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
// contains just the document as a single element and the value searched for is the document.
var arrayContains = /*Array.prototype.indexOf ?
function(arr, val) {
return arr.indexOf(val) > -1;
}:*/
function(arr, val) {
var i = arr.length;
while (i--) {
if (arr[i] === val) {
return true;
}
}
return false;
};
// Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
function isHtmlNamespace(node) {
var ns;
return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
}
function parentElement(node) {
var parent = node.parentNode;
return (parent.nodeType == 1) ? parent : null;
}
function getNodeIndex(node) {
var i = 0;
while( (node = node.previousSibling) ) {
i++;
}
return i;
}
function getNodeLength(node) {
var childNodes;
return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0);
}
function getCommonAncestor(node1, node2) {
var ancestors = [], n;
for (n = node1; n; n = n.parentNode) {
ancestors.push(n);
}
for (n = node2; n; n = n.parentNode) {
if (arrayContains(ancestors, n)) {
return n;
}
}
return null;
}
function isAncestorOf(ancestor, descendant, selfIsAncestor) {
var n = selfIsAncestor ? descendant : descendant.parentNode;
while (n) {
if (n === ancestor) {
return true;
} else {
n = n.parentNode;
}
}
return false;
}
function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
var p, n = selfIsAncestor ? node : node.parentNode;
while (n) {
p = n.parentNode;
if (p === ancestor) {
return n;
}
n = p;
}
return null;
}
function isCharacterDataNode(node) {
var t = node.nodeType;
return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
}
function insertAfter(node, precedingNode) {
var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
if (nextNode) {
parent.insertBefore(node, nextNode);
} else {
parent.appendChild(node);
}
return node;
}
// Note that we cannot use splitText() because it is bugridden in IE 9.
function splitDataNode(node, index) {
var newNode = node.cloneNode(false);
newNode.deleteData(0, index);
node.deleteData(index, node.length - index);
insertAfter(newNode, node);
return newNode;
}
function getDocument(node) {
if (node.nodeType == 9) {
return node;
} else if (typeof node.ownerDocument != UNDEF) {
return node.ownerDocument;
} else if (typeof node.document != UNDEF) {
return node.document;
} else if (node.parentNode) {
return getDocument(node.parentNode);
} else {
throw new Error("getDocument: no document found for node");
}
}
function getWindow(node) {
var doc = getDocument(node);
if (typeof doc.defaultView != UNDEF) {
return doc.defaultView;
} else if (typeof doc.parentWindow != UNDEF) {
return doc.parentWindow;
} else {
throw new Error("Cannot get a window object for node");
}
}
function getIframeDocument(iframeEl) {
if (typeof iframeEl.contentDocument != UNDEF) {
return iframeEl.contentDocument;
} else if (typeof iframeEl.contentWindow != UNDEF) {
return iframeEl.contentWindow.document;
} else {
throw new Error("getIframeWindow: No Document object found for iframe element");
}
}
function getIframeWindow(iframeEl) {
if (typeof iframeEl.contentWindow != UNDEF) {
return iframeEl.contentWindow;
} else if (typeof iframeEl.contentDocument != UNDEF) {
return iframeEl.contentDocument.defaultView;
} else {
throw new Error("getIframeWindow: No Window object found for iframe element");
}
}
function getBody(doc) {
return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
}
function getRootContainer(node) {
var parent;
while ( (parent = node.parentNode) ) {
node = parent;
}
return node;
}
function comparePoints(nodeA, offsetA, nodeB, offsetB) {
// See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
var nodeC, root, childA, childB, n;
if (nodeA == nodeB) {
// Case 1: nodes are the same
return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
} else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
// Case 2: node C (container B or an ancestor) is a child node of A
return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
} else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
// Case 3: node C (container A or an ancestor) is a child node of B
return getNodeIndex(nodeC) < offsetB ? -1 : 1;
} else {
// Case 4: containers are siblings or descendants of siblings
root = getCommonAncestor(nodeA, nodeB);
childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
if (childA === childB) {
// This shouldn't be possible
throw new Error("comparePoints got to case 4 and childA and childB are the same!");
} else {
n = root.firstChild;
while (n) {
if (n === childA) {
return -1;
} else if (n === childB) {
return 1;
}
n = n.nextSibling;
}
throw new Error("Should not be here!");
}
}
}
function fragmentFromNodeChildren(node) {
var fragment = getDocument(node).createDocumentFragment(), child;
while ( (child = node.firstChild) ) {
fragment.appendChild(child);
}
return fragment;
}
function inspectNode(node) {
if (!node) {
return "[No node]";
}
if (isCharacterDataNode(node)) {
return '"' + node.data + '"';
} else if (node.nodeType == 1) {
var idAttr = node.id ? ' id="' + node.id + '"' : "";
return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]";
} else {
return node.nodeName;
}
}
/**
* @constructor
*/
function NodeIterator(root) {
this.root = root;
this._next = root;
}
NodeIterator.prototype = {
_current: null,
hasNext: function() {
return !!this._next;
},
next: function() {
var n = this._current = this._next;
var child, next;
if (this._current) {
child = n.firstChild;
if (child) {
this._next = child;
} else {
next = null;
while ((n !== this.root) && !(next = n.nextSibling)) {
n = n.parentNode;
}
this._next = next;
}
}
return this._current;
},
detach: function() {
this._current = this._next = this.root = null;
}
};
function createIterator(root) {
return new NodeIterator(root);
}
/**
* @constructor
*/
function DomPosition(node, offset) {
this.node = node;
this.offset = offset;
}
DomPosition.prototype = {
equals: function(pos) {
return this.node === pos.node & this.offset == pos.offset;
},
inspect: function() {
return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
}
};
/**
* @constructor
*/
function DOMException(codeName) {
this.code = this[codeName];
this.codeName = codeName;
this.message = "DOMException: " + this.codeName;
}
DOMException.prototype = {
INDEX_SIZE_ERR: 1,
HIERARCHY_REQUEST_ERR: 3,
WRONG_DOCUMENT_ERR: 4,
NO_MODIFICATION_ALLOWED_ERR: 7,
NOT_FOUND_ERR: 8,
NOT_SUPPORTED_ERR: 9,
INVALID_STATE_ERR: 11
};
DOMException.prototype.toString = function() {
return this.message;
};
api.dom = {
arrayContains: arrayContains,
isHtmlNamespace: isHtmlNamespace,
parentElement: parentElement,
getNodeIndex: getNodeIndex,
getNodeLength: getNodeLength,
getCommonAncestor: getCommonAncestor,
isAncestorOf: isAncestorOf,
getClosestAncestorIn: getClosestAncestorIn,
isCharacterDataNode: isCharacterDataNode,
insertAfter: insertAfter,
splitDataNode: splitDataNode,
getDocument: getDocument,
getWindow: getWindow,
getIframeWindow: getIframeWindow,
getIframeDocument: getIframeDocument,
getBody: getBody,
getRootContainer: getRootContainer,
comparePoints: comparePoints,
inspectNode: inspectNode,
fragmentFromNodeChildren: fragmentFromNodeChildren,
createIterator: createIterator,
DomPosition: DomPosition
};
api.DOMException = DOMException;
});rangy.createModule("DomRange", function(api, module) {
api.requireModules( ["DomUtil"] );
var dom = api.dom;
var DomPosition = dom.DomPosition;
var DOMException = api.DOMException;
/*----------------------------------------------------------------------------------------------------------------*/
// Utility functions
function isNonTextPartiallySelected(node, range) {
return (node.nodeType != 3) &&
(dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true));
}
function getRangeDocument(range) {
return dom.getDocument(range.startContainer);
}
function dispatchEvent(range, type, args) {
var listeners = range._listeners[type];
if (listeners) {
for (var i = 0, len = listeners.length; i < len; ++i) {
listeners[i].call(range, {target: range, args: args});
}
}
}
function getBoundaryBeforeNode(node) {
return new DomPosition(node.parentNode, dom.getNodeIndex(node));
}
function getBoundaryAfterNode(node) {
return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1);
}
function insertNodeAtPosition(node, n, o) {
var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
if (dom.isCharacterDataNode(n)) {
if (o == n.length) {
dom.insertAfter(node, n);
} else {
n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o));
}
} else if (o >= n.childNodes.length) {
n.appendChild(node);
} else {
n.insertBefore(node, n.childNodes[o]);
}
return firstNodeInserted;
}
function cloneSubtree(iterator) {
var partiallySelected;
for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
partiallySelected = iterator.isPartiallySelectedSubtree();
node = node.cloneNode(!partiallySelected);
if (partiallySelected) {
subIterator = iterator.getSubtreeIterator();
node.appendChild(cloneSubtree(subIterator));
subIterator.detach(true);
}
if (node.nodeType == 10) { // DocumentType
throw new DOMException("HIERARCHY_REQUEST_ERR");
}
frag.appendChild(node);
}
return frag;
}
function iterateSubtree(rangeIterator, func, iteratorState) {
var it, n;
iteratorState = iteratorState || { stop: false };
for (var node, subRangeIterator; node = rangeIterator.next(); ) {
//log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
if (rangeIterator.isPartiallySelectedSubtree()) {
// The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the
// node selected by the Range.
if (func(node) === false) {
iteratorState.stop = true;
return;
} else {
subRangeIterator = rangeIterator.getSubtreeIterator();
iterateSubtree(subRangeIterator, func, iteratorState);
subRangeIterator.detach(true);
if (iteratorState.stop) {
return;
}
}
} else {
// The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
// descendant
it = dom.createIterator(node);
while ( (n = it.next()) ) {
if (func(n) === false) {
iteratorState.stop = true;
return;
}
}
}
}
}
function deleteSubtree(iterator) {
var subIterator;
while (iterator.next()) {
if (iterator.isPartiallySelectedSubtree()) {
subIterator = iterator.getSubtreeIterator();
deleteSubtree(subIterator);
subIterator.detach(true);
} else {
iterator.remove();
}
}
}
function extractSubtree(iterator) {
for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
if (iterator.isPartiallySelectedSubtree()) {
node = node.cloneNode(false);
subIterator = iterator.getSubtreeIterator();
node.appendChild(extractSubtree(subIterator));
subIterator.detach(true);
} else {
iterator.remove();
}
if (node.nodeType == 10) { // DocumentType
throw new DOMException("HIERARCHY_REQUEST_ERR");
}
frag.appendChild(node);
}
return frag;
}
function getNodesInRange(range, nodeTypes, filter) {
//log.info("getNodesInRange, " + nodeTypes.join(","));
var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
var filterExists = !!filter;
if (filterNodeTypes) {
regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
}
var nodes = [];
iterateSubtree(new RangeIterator(range, false), function(node) {
if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {
nodes.push(node);
}
});
return nodes;
}
function inspect(range) {
var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
}
/*----------------------------------------------------------------------------------------------------------------*/
// RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
/**
* @constructor
*/
function RangeIterator(range, clonePartiallySelectedTextNodes) {
this.range = range;
this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
if (!range.collapsed) {
this.sc = range.startContainer;
this.so = range.startOffset;
this.ec = range.endContainer;
this.eo = range.endOffset;
var root = range.commonAncestorContainer;
if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {
this.isSingleCharacterDataNode = true;
this._first = this._last = this._next = this.sc;
} else {
this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?
this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true);
this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?
this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true);
}
}
}
RangeIterator.prototype = {
_current: null,
_next: null,
_first: null,
_last: null,
isSingleCharacterDataNode: false,
reset: function() {
this._current = null;
this._next = this._first;
},
hasNext: function() {
return !!this._next;
},
next: function() {
// Move to next node
var current = this._current = this._next;
if (current) {
this._next = (current !== this._last) ? current.nextSibling : null;
// Check for partially selected text nodes
if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
if (current === this.ec) {
(current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
}
if (this._current === this.sc) {
(current = current.cloneNode(true)).deleteData(0, this.so);
}
}
}
return current;
},
remove: function() {
var current = this._current, start, end;
if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
start = (current === this.sc) ? this.so : 0;
end = (current === this.ec) ? this.eo : current.length;
if (start != end) {
current.deleteData(start, end - start);
}
} else {
if (current.parentNode) {
current.parentNode.removeChild(current);
} else {
}
}
},
// Checks if the current node is partially selected
isPartiallySelectedSubtree: function() {
var current = this._current;
return isNonTextPartiallySelected(current, this.range);
},
getSubtreeIterator: function() {
var subRange;
if (this.isSingleCharacterDataNode) {
subRange = this.range.cloneRange();
subRange.collapse();
} else {
subRange = new Range(getRangeDocument(this.range));
var current = this._current;
var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current);
if (dom.isAncestorOf(current, this.sc, true)) {
startContainer = this.sc;
startOffset = this.so;
}
if (dom.isAncestorOf(current, this.ec, true)) {
endContainer = this.ec;
endOffset = this.eo;
}
updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
}
return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
},
detach: function(detachRange) {
if (detachRange) {
this.range.detach();
}
this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
}
};
/*----------------------------------------------------------------------------------------------------------------*/
// Exceptions
/**
* @constructor
*/
function RangeException(codeName) {
this.code = this[codeName];
this.codeName = codeName;
this.message = "RangeException: " + this.codeName;
}
RangeException.prototype = {
BAD_BOUNDARYPOINTS_ERR: 1,
INVALID_NODE_TYPE_ERR: 2
};
RangeException.prototype.toString = function() {
return this.message;
};
/*----------------------------------------------------------------------------------------------------------------*/
/**
* Currently iterates through all nodes in the range on creation until I think of a decent way to do it
* TODO: Look into making this a proper iterator, not requiring preloading everything first
* @constructor
*/
function RangeNodeIterator(range, nodeTypes, filter) {
this.nodes = getNodesInRange(range, nodeTypes, filter);
this._next = this.nodes[0];
this._position = 0;
}
RangeNodeIterator.prototype = {
_current: null,
hasNext: function() {
return !!this._next;
},
next: function() {
this._current = this._next;
this._next = this.nodes[ ++this._position ];
return this._current;
},
detach: function() {
this._current = this._next = this.nodes = null;
}
};
var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
var rootContainerNodeTypes = [2, 9, 11];
var readonlyNodeTypes = [5, 6, 10, 12];
var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
function createAncestorFinder(nodeTypes) {
return function(node, selfIsAncestor) {
var t, n = selfIsAncestor ? node : node.parentNode;
while (n) {
t = n.nodeType;
if (dom.arrayContains(nodeTypes, t)) {
return n;
}
n = n.parentNode;
}
return null;
};
}
var getRootContainer = dom.getRootContainer;
var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
throw new RangeException("INVALID_NODE_TYPE_ERR");
}
}
function assertNotDetached(range) {
if (!range.startContainer) {
throw new DOMException("INVALID_STATE_ERR");
}
}
function assertValidNodeType(node, invalidTypes) {
if (!dom.arrayContains(invalidTypes, node.nodeType)) {
throw new RangeException("INVALID_NODE_TYPE_ERR");
}
}
function assertValidOffset(node, offset) {
if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
throw new DOMException("INDEX_SIZE_ERR");
}
}
function assertSameDocumentOrFragment(node1, node2) {
if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
throw new DOMException("WRONG_DOCUMENT_ERR");
}
}
function assertNodeNotReadOnly(node) {
if (getReadonlyAncestor(node, true)) {
throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
}
}
function assertNode(node, codeName) {
if (!node) {
throw new DOMException(codeName);
}
}
function isOrphan(node) {
return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
}
function isValidOffset(node, offset) {
return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length);
}
function assertRangeValid(range) {
assertNotDetached(range);
if (isOrphan(range.startContainer) || isOrphan(range.endContainer) ||
!isValidOffset(range.startContainer, range.startOffset) ||
!isValidOffset(range.endContainer, range.endOffset)) {
throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
}
}
/*----------------------------------------------------------------------------------------------------------------*/
// Test the browser's innerHTML support to decide how to implement createContextualFragment
var styleEl = document.createElement("style");
var htmlParsingConforms = false;
try {
styleEl.innerHTML = "<b>x</b>";
htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
} catch (e) {
// IE 6 and 7 throw
}
api.features.htmlParsingConforms = htmlParsingConforms;
var createContextualFragment = htmlParsingConforms ?
// Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
// discussion and base code for this implementation at issue 67.
// Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
// Thanks to Aleks Williams.
function(fragmentStr) {
// "Let node the context object's start's node."
var node = this.startContainer;
var doc = dom.getDocument(node);
// "If the context object's start's node is null, raise an INVALID_STATE_ERR
// exception and abort these steps."
if (!node) {
throw new DOMException("INVALID_STATE_ERR");
}
// "Let element be as follows, depending on node's interface:"
// Document, Document Fragment: null
var el = null;
// "Element: node"
if (node.nodeType == 1) {
el = node;
// "Text, Comment: node's parentElement"
} else if (dom.isCharacterDataNode(node)) {
el = dom.parentElement(node);
}
// "If either element is null or element's ownerDocument is an HTML document
// and element's local name is "html" and element's namespace is the HTML
// namespace"
if (el === null || (
el.nodeName == "HTML"
&& dom.isHtmlNamespace(dom.getDocument(el).documentElement)
&& dom.isHtmlNamespace(el)
)) {
// "let element be a new Element with "body" as its local name and the HTML
// namespace as its namespace.""
el = doc.createElement("body");
} else {
el = el.cloneNode(false);
}
// "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
// "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
// "In either case, the algorithm must be invoked with fragment as the input
// and element as the context element."
el.innerHTML = fragmentStr;
// "If this raises an exception, then abort these steps. Otherwise, let new
// children be the nodes returned."
// "Let fragment be a new DocumentFragment."
// "Append all new children to fragment."
// "Return fragment."
return dom.fragmentFromNodeChildren(el);
} :
// In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
// previous versions of Rangy used (with the exception of using a body element rather than a div)
function(fragmentStr) {
assertNotDetached(this);
var doc = getRangeDocument(this);
var el = doc.createElement("body");
el.innerHTML = fragmentStr;
return dom.fragmentFromNodeChildren(el);
};
/*----------------------------------------------------------------------------------------------------------------*/
var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
"commonAncestorContainer"];
var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
function RangePrototype() {}
RangePrototype.prototype = {
attachListener: function(type, listener) {
this._listeners[type].push(listener);
},
compareBoundaryPoints: function(how, range) {
assertRangeValid(this);
assertSameDocumentOrFragment(this.startContainer, range.startContainer);
var nodeA, offsetA, nodeB, offsetB;
var prefixA = (how == e2s || how == s2s) ? "start" : "end";
var prefixB = (how == s2e || how == s2s) ? "start" : "end";
nodeA = this[prefixA + "Container"];
offsetA = this[prefixA + "Offset"];
nodeB = range[prefixB + "Container"];
offsetB = range[prefixB + "Offset"];
return dom.comparePoints(nodeA, offsetA, nodeB, offsetB);
},
insertNode: function(node) {
assertRangeValid(this);
assertValidNodeType(node, insertableNodeTypes);
assertNodeNotReadOnly(this.startContainer);
if (dom.isAncestorOf(node, this.startContainer, true)) {
throw new DOMException("HIERARCHY_REQUEST_ERR");
}
// No check for whether the container of the start of the Range is of a type that does not allow
// children of the type of node: the browser's DOM implementation should do this for us when we attempt
// to add the node
var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
this.setStartBefore(firstNodeInserted);
},
cloneContents: function() {
assertRangeValid(this);
var clone, frag;
if (this.collapsed) {
return getRangeDocument(this).createDocumentFragment();
} else {
if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {
clone = this.startContainer.cloneNode(true);
clone.data = clone.data.slice(this.startOffset, this.endOffset);
frag = getRangeDocument(this).createDocumentFragment();
frag.appendChild(clone);
return frag;
} else {
var iterator = new RangeIterator(this, true);
clone = cloneSubtree(iterator);
iterator.detach();
}
return clone;
}
},
canSurroundContents: function() {
assertRangeValid(this);
assertNodeNotReadOnly(this.startContainer);
assertNodeNotReadOnly(this.endContainer);
// Check if the contents can be surrounded. Specifically, this means whether the range partially selects
// no non-text nodes.
var iterator = new RangeIterator(this, true);
var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
(iterator._last && isNonTextPartiallySelected(iterator._last, this)));
iterator.detach();
return !boundariesInvalid;
},
surroundContents: function(node) {
assertValidNodeType(node, surroundNodeTypes);
if (!this.canSurroundContents()) {
throw new RangeException("BAD_BOUNDARYPOINTS_ERR");
}
// Extract the contents
var content = this.extractContents();
// Clear the children of the node
if (node.hasChildNodes()) {
while (node.lastChild) {
node.removeChild(node.lastChild);
}
}
// Insert the new node and add the extracted contents
insertNodeAtPosition(node, this.startContainer, this.startOffset);
node.appendChild(content);
this.selectNode(node);
},
cloneRange: function() {
assertRangeValid(this);
var range = new Range(getRangeDocument(this));
var i = rangeProperties.length, prop;
while (i--) {
prop = rangeProperties[i];
range[prop] = this[prop];
}
return range;
},
toString: function() {
assertRangeValid(this);
var sc = this.startContainer;
if (sc === this.endContainer && dom.isCharacterDataNode(sc)) {
return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
} else {
var textBits = [], iterator = new RangeIterator(this, true);
iterateSubtree(iterator, function(node) {
// Accept only text or CDATA nodes, not comments
if (node.nodeType == 3 || node.nodeType == 4) {
textBits.push(node.data);
}
});
iterator.detach();
return textBits.join("");
}
},
// The methods below are all non-standard. The following batch were introduced by Mozilla but have since
// been removed from Mozilla.
compareNode: function(node) {
assertRangeValid(this);
var parent = node.parentNode;
var nodeIndex = dom.getNodeIndex(node);
if (!parent) {
throw new DOMException("NOT_FOUND_ERR");
}
var startComparison = this.comparePoint(parent, nodeIndex),
endComparison = this.comparePoint(parent, nodeIndex + 1);
if (startComparison < 0) { // Node starts before
return (endComparison > 0) ? n_b_a : n_b;
} else {
return (endComparison > 0) ? n_a : n_i;
}
},
comparePoint: function(node, offset) {
assertRangeValid(this);
assertNode(node, "HIERARCHY_REQUEST_ERR");
assertSameDocumentOrFragment(node, this.startContainer);
if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
return -1;
} else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
return 1;
}
return 0;
},
createContextualFragment: createContextualFragment,
toHtml: function() {
assertRangeValid(this);
var container = getRangeDocument(this).createElement("div");
container.appendChild(this.cloneContents());
return container.innerHTML;
},
// touchingIsIntersecting determines whether this method considers a node that borders a range intersects
// with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
intersectsNode: function(node, touchingIsIntersecting) {
assertRangeValid(this);
assertNode(node, "NOT_FOUND_ERR");
if (dom.getDocument(node) !== getRangeDocument(this)) {
return false;
}
var parent = node.parentNode, offset = dom.getNodeIndex(node);
assertNode(parent, "NOT_FOUND_ERR");
var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset),
endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
},
isPointInRange: function(node, offset) {
assertRangeValid(this);
assertNode(node, "HIERARCHY_REQUEST_ERR");
assertSameDocumentOrFragment(node, this.startContainer);
return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
(dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
},
// The methods below are non-standard and invented by me.
// Sharing a boundary start-to-end or end-to-start does not count as intersection.
intersectsRange: function(range, touchingIsIntersecting) {
assertRangeValid(this);
if (getRangeDocument(range) != getRangeDocument(this)) {
throw new DOMException("WRONG_DOCUMENT_ERR");
}
var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset),
endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset);
return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
},
inte