UNPKG

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
/** * @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