r110899 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r110898‎ | r110899 | r110900 >
Date:00:02, 8 February 2012
Author:inez
Status:deferred
Tags:
Comment:
Get getSelection in contenteditable working
Modified paths:
  • /trunk/extensions/VisualEditor/demos/ce/index.php (modified) (history)
  • /trunk/extensions/VisualEditor/modules/rangy (added) (history)
  • /trunk/extensions/VisualEditor/modules/rangy/rangy-core.js (added) (history)
  • /trunk/extensions/VisualEditor/modules/rangy/rangy-cssclassapplier.js (added) (history)
  • /trunk/extensions/VisualEditor/modules/rangy/rangy-position.js (added) (history)
  • /trunk/extensions/VisualEditor/modules/rangy/rangy-selectionsaverestore.js (added) (history)
  • /trunk/extensions/VisualEditor/modules/rangy/rangy-serializer.js (added) (history)
  • /trunk/extensions/VisualEditor/modules/ve/ce/ve.es.Surface.js (modified) (history)

Diff [purge]

Index: trunk/extensions/VisualEditor/demos/ce/index.php
@@ -79,6 +79,10 @@
8080 include( '../../modules/sandbox/base.php' );
8181
8282 ?>
 83+ <!-- Rangy -->
 84+ <script src="../../modules/rangy/rangy-core.js"></script>
 85+ <script src="../../modules/rangy/rangy-position.js"></script>
 86+
8387 <!-- ve -->
8488 <script src="../../modules/jquery/jquery.js"></script>
8589 <script src="../../modules/ve/ve.js"></script>
Index: trunk/extensions/VisualEditor/modules/rangy/rangy-core.js
@@ -0,0 +1,3211 @@
 2+/**
 3+ * @license Rangy, a cross-browser JavaScript range and selection library
 4+ * http://code.google.com/p/rangy/
 5+ *
 6+ * Copyright 2011, Tim Down
 7+ * Licensed under the MIT license.
 8+ * Version: 1.2.2
 9+ * Build date: 13 November 2011
 10+ */
 11+window['rangy'] = (function() {
 12+
 13+
 14+ var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
 15+
 16+ var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
 17+ "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"];
 18+
 19+ var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
 20+ "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
 21+ "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
 22+
 23+ var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
 24+
 25+ // Subset of TextRange's full set of methods that we're interested in
 26+ var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark",
 27+ "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"];
 28+
 29+ /*----------------------------------------------------------------------------------------------------------------*/
 30+
 31+ // Trio of functions taken from Peter Michaux's article:
 32+ // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
 33+ function isHostMethod(o, p) {
 34+ var t = typeof o[p];
 35+ return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
 36+ }
 37+
 38+ function isHostObject(o, p) {
 39+ return !!(typeof o[p] == OBJECT && o[p]);
 40+ }
 41+
 42+ function isHostProperty(o, p) {
 43+ return typeof o[p] != UNDEFINED;
 44+ }
 45+
 46+ // Creates a convenience function to save verbose repeated calls to tests functions
 47+ function createMultiplePropertyTest(testFunc) {
 48+ return function(o, props) {
 49+ var i = props.length;
 50+ while (i--) {
 51+ if (!testFunc(o, props[i])) {
 52+ return false;
 53+ }
 54+ }
 55+ return true;
 56+ };
 57+ }
 58+
 59+ // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
 60+ var areHostMethods = createMultiplePropertyTest(isHostMethod);
 61+ var areHostObjects = createMultiplePropertyTest(isHostObject);
 62+ var areHostProperties = createMultiplePropertyTest(isHostProperty);
 63+
 64+ function isTextRange(range) {
 65+ return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
 66+ }
 67+
 68+ var api = {
 69+ version: "1.2.2",
 70+ initialized: false,
 71+ supported: true,
 72+
 73+ util: {
 74+ isHostMethod: isHostMethod,
 75+ isHostObject: isHostObject,
 76+ isHostProperty: isHostProperty,
 77+ areHostMethods: areHostMethods,
 78+ areHostObjects: areHostObjects,
 79+ areHostProperties: areHostProperties,
 80+ isTextRange: isTextRange
 81+ },
 82+
 83+ features: {},
 84+
 85+ modules: {},
 86+ config: {
 87+ alertOnWarn: false,
 88+ preferTextRange: false
 89+ }
 90+ };
 91+
 92+ function fail(reason) {
 93+ window.alert("Rangy not supported in your browser. Reason: " + reason);
 94+ api.initialized = true;
 95+ api.supported = false;
 96+ }
 97+
 98+ api.fail = fail;
 99+
 100+ function warn(msg) {
 101+ var warningMessage = "Rangy warning: " + msg;
 102+ if (api.config.alertOnWarn) {
 103+ window.alert(warningMessage);
 104+ } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {
 105+ window.console.log(warningMessage);
 106+ }
 107+ }
 108+
 109+ api.warn = warn;
 110+
 111+ if ({}.hasOwnProperty) {
 112+ api.util.extend = function(o, props) {
 113+ for (var i in props) {
 114+ if (props.hasOwnProperty(i)) {
 115+ o[i] = props[i];
 116+ }
 117+ }
 118+ };
 119+ } else {
 120+ fail("hasOwnProperty not supported");
 121+ }
 122+
 123+ var initListeners = [];
 124+ var moduleInitializers = [];
 125+
 126+ // Initialization
 127+ function init() {
 128+ if (api.initialized) {
 129+ return;
 130+ }
 131+ var testRange;
 132+ var implementsDomRange = false, implementsTextRange = false;
 133+
 134+ // First, perform basic feature tests
 135+
 136+ if (isHostMethod(document, "createRange")) {
 137+ testRange = document.createRange();
 138+ if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
 139+ implementsDomRange = true;
 140+ }
 141+ testRange.detach();
 142+ }
 143+
 144+ var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
 145+
 146+ if (body && isHostMethod(body, "createTextRange")) {
 147+ testRange = body.createTextRange();
 148+ if (isTextRange(testRange)) {
 149+ implementsTextRange = true;
 150+ }
 151+ }
 152+
 153+ if (!implementsDomRange && !implementsTextRange) {
 154+ fail("Neither Range nor TextRange are implemented");
 155+ }
 156+
 157+ api.initialized = true;
 158+ api.features = {
 159+ implementsDomRange: implementsDomRange,
 160+ implementsTextRange: implementsTextRange
 161+ };
 162+
 163+ // Initialize modules and call init listeners
 164+ var allListeners = moduleInitializers.concat(initListeners);
 165+ for (var i = 0, len = allListeners.length; i < len; ++i) {
 166+ try {
 167+ allListeners[i](api);
 168+ } catch (ex) {
 169+ if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {
 170+ window.console.log("Init listener threw an exception. Continuing.", ex);
 171+ }
 172+
 173+ }
 174+ }
 175+ }
 176+
 177+ // Allow external scripts to initialize this library in case it's loaded after the document has loaded
 178+ api.init = init;
 179+
 180+ // Execute listener immediately if already initialized
 181+ api.addInitListener = function(listener) {
 182+ if (api.initialized) {
 183+ listener(api);
 184+ } else {
 185+ initListeners.push(listener);
 186+ }
 187+ };
 188+
 189+ var createMissingNativeApiListeners = [];
 190+
 191+ api.addCreateMissingNativeApiListener = function(listener) {
 192+ createMissingNativeApiListeners.push(listener);
 193+ };
 194+
 195+ function createMissingNativeApi(win) {
 196+ win = win || window;
 197+ init();
 198+
 199+ // Notify listeners
 200+ for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {
 201+ createMissingNativeApiListeners[i](win);
 202+ }
 203+ }
 204+
 205+ api.createMissingNativeApi = createMissingNativeApi;
 206+
 207+ /**
 208+ * @constructor
 209+ */
 210+ function Module(name) {
 211+ this.name = name;
 212+ this.initialized = false;
 213+ this.supported = false;
 214+ }
 215+
 216+ Module.prototype.fail = function(reason) {
 217+ this.initialized = true;
 218+ this.supported = false;
 219+
 220+ throw new Error("Module '" + this.name + "' failed to load: " + reason);
 221+ };
 222+
 223+ Module.prototype.warn = function(msg) {
 224+ api.warn("Module " + this.name + ": " + msg);
 225+ };
 226+
 227+ Module.prototype.createError = function(msg) {
 228+ return new Error("Error in Rangy " + this.name + " module: " + msg);
 229+ };
 230+
 231+ api.createModule = function(name, initFunc) {
 232+ var module = new Module(name);
 233+ api.modules[name] = module;
 234+
 235+ moduleInitializers.push(function(api) {
 236+ initFunc(api, module);
 237+ module.initialized = true;
 238+ module.supported = true;
 239+ });
 240+ };
 241+
 242+ api.requireModules = function(modules) {
 243+ for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) {
 244+ moduleName = modules[i];
 245+ module = api.modules[moduleName];
 246+ if (!module || !(module instanceof Module)) {
 247+ throw new Error("Module '" + moduleName + "' not found");
 248+ }
 249+ if (!module.supported) {
 250+ throw new Error("Module '" + moduleName + "' not supported");
 251+ }
 252+ }
 253+ };
 254+
 255+ /*----------------------------------------------------------------------------------------------------------------*/
 256+
 257+ // Wait for document to load before running tests
 258+
 259+ var docReady = false;
 260+
 261+ var loadHandler = function(e) {
 262+
 263+ if (!docReady) {
 264+ docReady = true;
 265+ if (!api.initialized) {
 266+ init();
 267+ }
 268+ }
 269+ };
 270+
 271+ // Test whether we have window and document objects that we will need
 272+ if (typeof window == UNDEFINED) {
 273+ fail("No window found");
 274+ return;
 275+ }
 276+ if (typeof document == UNDEFINED) {
 277+ fail("No document found");
 278+ return;
 279+ }
 280+
 281+ if (isHostMethod(document, "addEventListener")) {
 282+ document.addEventListener("DOMContentLoaded", loadHandler, false);
 283+ }
 284+
 285+ // Add a fallback in case the DOMContentLoaded event isn't supported
 286+ if (isHostMethod(window, "addEventListener")) {
 287+ window.addEventListener("load", loadHandler, false);
 288+ } else if (isHostMethod(window, "attachEvent")) {
 289+ window.attachEvent("onload", loadHandler);
 290+ } else {
 291+ fail("Window does not have required addEventListener or attachEvent method");
 292+ }
 293+
 294+ return api;
 295+})();
 296+rangy.createModule("DomUtil", function(api, module) {
 297+
 298+ var UNDEF = "undefined";
 299+ var util = api.util;
 300+
 301+ // Perform feature tests
 302+ if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
 303+ module.fail("document missing a Node creation method");
 304+ }
 305+
 306+ if (!util.isHostMethod(document, "getElementsByTagName")) {
 307+ module.fail("document missing getElementsByTagName method");
 308+ }
 309+
 310+ var el = document.createElement("div");
 311+ if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
 312+ !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
 313+ module.fail("Incomplete Element implementation");
 314+ }
 315+
 316+ // innerHTML is required for Range's createContextualFragment method
 317+ if (!util.isHostProperty(el, "innerHTML")) {
 318+ module.fail("Element is missing innerHTML property");
 319+ }
 320+
 321+ var textNode = document.createTextNode("test");
 322+ if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
 323+ !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
 324+ !util.areHostProperties(textNode, ["data"]))) {
 325+ module.fail("Incomplete Text Node implementation");
 326+ }
 327+
 328+ /*----------------------------------------------------------------------------------------------------------------*/
 329+
 330+ // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
 331+ // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
 332+ // contains just the document as a single element and the value searched for is the document.
 333+ var arrayContains = /*Array.prototype.indexOf ?
 334+ function(arr, val) {
 335+ return arr.indexOf(val) > -1;
 336+ }:*/
 337+
 338+ function(arr, val) {
 339+ var i = arr.length;
 340+ while (i--) {
 341+ if (arr[i] === val) {
 342+ return true;
 343+ }
 344+ }
 345+ return false;
 346+ };
 347+
 348+ // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
 349+ function isHtmlNamespace(node) {
 350+ var ns;
 351+ return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
 352+ }
 353+
 354+ function parentElement(node) {
 355+ var parent = node.parentNode;
 356+ return (parent.nodeType == 1) ? parent : null;
 357+ }
 358+
 359+ function getNodeIndex(node) {
 360+ var i = 0;
 361+ while( (node = node.previousSibling) ) {
 362+ i++;
 363+ }
 364+ return i;
 365+ }
 366+
 367+ function getNodeLength(node) {
 368+ var childNodes;
 369+ return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0);
 370+ }
 371+
 372+ function getCommonAncestor(node1, node2) {
 373+ var ancestors = [], n;
 374+ for (n = node1; n; n = n.parentNode) {
 375+ ancestors.push(n);
 376+ }
 377+
 378+ for (n = node2; n; n = n.parentNode) {
 379+ if (arrayContains(ancestors, n)) {
 380+ return n;
 381+ }
 382+ }
 383+
 384+ return null;
 385+ }
 386+
 387+ function isAncestorOf(ancestor, descendant, selfIsAncestor) {
 388+ var n = selfIsAncestor ? descendant : descendant.parentNode;
 389+ while (n) {
 390+ if (n === ancestor) {
 391+ return true;
 392+ } else {
 393+ n = n.parentNode;
 394+ }
 395+ }
 396+ return false;
 397+ }
 398+
 399+ function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
 400+ var p, n = selfIsAncestor ? node : node.parentNode;
 401+ while (n) {
 402+ p = n.parentNode;
 403+ if (p === ancestor) {
 404+ return n;
 405+ }
 406+ n = p;
 407+ }
 408+ return null;
 409+ }
 410+
 411+ function isCharacterDataNode(node) {
 412+ var t = node.nodeType;
 413+ return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
 414+ }
 415+
 416+ function insertAfter(node, precedingNode) {
 417+ var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
 418+ if (nextNode) {
 419+ parent.insertBefore(node, nextNode);
 420+ } else {
 421+ parent.appendChild(node);
 422+ }
 423+ return node;
 424+ }
 425+
 426+ // Note that we cannot use splitText() because it is bugridden in IE 9.
 427+ function splitDataNode(node, index) {
 428+ var newNode = node.cloneNode(false);
 429+ newNode.deleteData(0, index);
 430+ node.deleteData(index, node.length - index);
 431+ insertAfter(newNode, node);
 432+ return newNode;
 433+ }
 434+
 435+ function getDocument(node) {
 436+ if (node.nodeType == 9) {
 437+ return node;
 438+ } else if (typeof node.ownerDocument != UNDEF) {
 439+ return node.ownerDocument;
 440+ } else if (typeof node.document != UNDEF) {
 441+ return node.document;
 442+ } else if (node.parentNode) {
 443+ return getDocument(node.parentNode);
 444+ } else {
 445+ throw new Error("getDocument: no document found for node");
 446+ }
 447+ }
 448+
 449+ function getWindow(node) {
 450+ var doc = getDocument(node);
 451+ if (typeof doc.defaultView != UNDEF) {
 452+ return doc.defaultView;
 453+ } else if (typeof doc.parentWindow != UNDEF) {
 454+ return doc.parentWindow;
 455+ } else {
 456+ throw new Error("Cannot get a window object for node");
 457+ }
 458+ }
 459+
 460+ function getIframeDocument(iframeEl) {
 461+ if (typeof iframeEl.contentDocument != UNDEF) {
 462+ return iframeEl.contentDocument;
 463+ } else if (typeof iframeEl.contentWindow != UNDEF) {
 464+ return iframeEl.contentWindow.document;
 465+ } else {
 466+ throw new Error("getIframeWindow: No Document object found for iframe element");
 467+ }
 468+ }
 469+
 470+ function getIframeWindow(iframeEl) {
 471+ if (typeof iframeEl.contentWindow != UNDEF) {
 472+ return iframeEl.contentWindow;
 473+ } else if (typeof iframeEl.contentDocument != UNDEF) {
 474+ return iframeEl.contentDocument.defaultView;
 475+ } else {
 476+ throw new Error("getIframeWindow: No Window object found for iframe element");
 477+ }
 478+ }
 479+
 480+ function getBody(doc) {
 481+ return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
 482+ }
 483+
 484+ function getRootContainer(node) {
 485+ var parent;
 486+ while ( (parent = node.parentNode) ) {
 487+ node = parent;
 488+ }
 489+ return node;
 490+ }
 491+
 492+ function comparePoints(nodeA, offsetA, nodeB, offsetB) {
 493+ // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
 494+ var nodeC, root, childA, childB, n;
 495+ if (nodeA == nodeB) {
 496+
 497+ // Case 1: nodes are the same
 498+ return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
 499+ } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
 500+
 501+ // Case 2: node C (container B or an ancestor) is a child node of A
 502+ return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
 503+ } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
 504+
 505+ // Case 3: node C (container A or an ancestor) is a child node of B
 506+ return getNodeIndex(nodeC) < offsetB ? -1 : 1;
 507+ } else {
 508+
 509+ // Case 4: containers are siblings or descendants of siblings
 510+ root = getCommonAncestor(nodeA, nodeB);
 511+ childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
 512+ childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
 513+
 514+ if (childA === childB) {
 515+ // This shouldn't be possible
 516+
 517+ throw new Error("comparePoints got to case 4 and childA and childB are the same!");
 518+ } else {
 519+ n = root.firstChild;
 520+ while (n) {
 521+ if (n === childA) {
 522+ return -1;
 523+ } else if (n === childB) {
 524+ return 1;
 525+ }
 526+ n = n.nextSibling;
 527+ }
 528+ throw new Error("Should not be here!");
 529+ }
 530+ }
 531+ }
 532+
 533+ function fragmentFromNodeChildren(node) {
 534+ var fragment = getDocument(node).createDocumentFragment(), child;
 535+ while ( (child = node.firstChild) ) {
 536+ fragment.appendChild(child);
 537+ }
 538+ return fragment;
 539+ }
 540+
 541+ function inspectNode(node) {
 542+ if (!node) {
 543+ return "[No node]";
 544+ }
 545+ if (isCharacterDataNode(node)) {
 546+ return '"' + node.data + '"';
 547+ } else if (node.nodeType == 1) {
 548+ var idAttr = node.id ? ' id="' + node.id + '"' : "";
 549+ return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]";
 550+ } else {
 551+ return node.nodeName;
 552+ }
 553+ }
 554+
 555+ /**
 556+ * @constructor
 557+ */
 558+ function NodeIterator(root) {
 559+ this.root = root;
 560+ this._next = root;
 561+ }
 562+
 563+ NodeIterator.prototype = {
 564+ _current: null,
 565+
 566+ hasNext: function() {
 567+ return !!this._next;
 568+ },
 569+
 570+ next: function() {
 571+ var n = this._current = this._next;
 572+ var child, next;
 573+ if (this._current) {
 574+ child = n.firstChild;
 575+ if (child) {
 576+ this._next = child;
 577+ } else {
 578+ next = null;
 579+ while ((n !== this.root) && !(next = n.nextSibling)) {
 580+ n = n.parentNode;
 581+ }
 582+ this._next = next;
 583+ }
 584+ }
 585+ return this._current;
 586+ },
 587+
 588+ detach: function() {
 589+ this._current = this._next = this.root = null;
 590+ }
 591+ };
 592+
 593+ function createIterator(root) {
 594+ return new NodeIterator(root);
 595+ }
 596+
 597+ /**
 598+ * @constructor
 599+ */
 600+ function DomPosition(node, offset) {
 601+ this.node = node;
 602+ this.offset = offset;
 603+ }
 604+
 605+ DomPosition.prototype = {
 606+ equals: function(pos) {
 607+ return this.node === pos.node & this.offset == pos.offset;
 608+ },
 609+
 610+ inspect: function() {
 611+ return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
 612+ }
 613+ };
 614+
 615+ /**
 616+ * @constructor
 617+ */
 618+ function DOMException(codeName) {
 619+ this.code = this[codeName];
 620+ this.codeName = codeName;
 621+ this.message = "DOMException: " + this.codeName;
 622+ }
 623+
 624+ DOMException.prototype = {
 625+ INDEX_SIZE_ERR: 1,
 626+ HIERARCHY_REQUEST_ERR: 3,
 627+ WRONG_DOCUMENT_ERR: 4,
 628+ NO_MODIFICATION_ALLOWED_ERR: 7,
 629+ NOT_FOUND_ERR: 8,
 630+ NOT_SUPPORTED_ERR: 9,
 631+ INVALID_STATE_ERR: 11
 632+ };
 633+
 634+ DOMException.prototype.toString = function() {
 635+ return this.message;
 636+ };
 637+
 638+ api.dom = {
 639+ arrayContains: arrayContains,
 640+ isHtmlNamespace: isHtmlNamespace,
 641+ parentElement: parentElement,
 642+ getNodeIndex: getNodeIndex,
 643+ getNodeLength: getNodeLength,
 644+ getCommonAncestor: getCommonAncestor,
 645+ isAncestorOf: isAncestorOf,
 646+ getClosestAncestorIn: getClosestAncestorIn,
 647+ isCharacterDataNode: isCharacterDataNode,
 648+ insertAfter: insertAfter,
 649+ splitDataNode: splitDataNode,
 650+ getDocument: getDocument,
 651+ getWindow: getWindow,
 652+ getIframeWindow: getIframeWindow,
 653+ getIframeDocument: getIframeDocument,
 654+ getBody: getBody,
 655+ getRootContainer: getRootContainer,
 656+ comparePoints: comparePoints,
 657+ inspectNode: inspectNode,
 658+ fragmentFromNodeChildren: fragmentFromNodeChildren,
 659+ createIterator: createIterator,
 660+ DomPosition: DomPosition
 661+ };
 662+
 663+ api.DOMException = DOMException;
 664+});rangy.createModule("DomRange", function(api, module) {
 665+ api.requireModules( ["DomUtil"] );
 666+
 667+
 668+ var dom = api.dom;
 669+ var DomPosition = dom.DomPosition;
 670+ var DOMException = api.DOMException;
 671+
 672+ /*----------------------------------------------------------------------------------------------------------------*/
 673+
 674+ // Utility functions
 675+
 676+ function isNonTextPartiallySelected(node, range) {
 677+ return (node.nodeType != 3) &&
 678+ (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true));
 679+ }
 680+
 681+ function getRangeDocument(range) {
 682+ return dom.getDocument(range.startContainer);
 683+ }
 684+
 685+ function dispatchEvent(range, type, args) {
 686+ var listeners = range._listeners[type];
 687+ if (listeners) {
 688+ for (var i = 0, len = listeners.length; i < len; ++i) {
 689+ listeners[i].call(range, {target: range, args: args});
 690+ }
 691+ }
 692+ }
 693+
 694+ function getBoundaryBeforeNode(node) {
 695+ return new DomPosition(node.parentNode, dom.getNodeIndex(node));
 696+ }
 697+
 698+ function getBoundaryAfterNode(node) {
 699+ return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1);
 700+ }
 701+
 702+ function insertNodeAtPosition(node, n, o) {
 703+ var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
 704+ if (dom.isCharacterDataNode(n)) {
 705+ if (o == n.length) {
 706+ dom.insertAfter(node, n);
 707+ } else {
 708+ n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o));
 709+ }
 710+ } else if (o >= n.childNodes.length) {
 711+ n.appendChild(node);
 712+ } else {
 713+ n.insertBefore(node, n.childNodes[o]);
 714+ }
 715+ return firstNodeInserted;
 716+ }
 717+
 718+ function cloneSubtree(iterator) {
 719+ var partiallySelected;
 720+ for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
 721+ partiallySelected = iterator.isPartiallySelectedSubtree();
 722+
 723+ node = node.cloneNode(!partiallySelected);
 724+ if (partiallySelected) {
 725+ subIterator = iterator.getSubtreeIterator();
 726+ node.appendChild(cloneSubtree(subIterator));
 727+ subIterator.detach(true);
 728+ }
 729+
 730+ if (node.nodeType == 10) { // DocumentType
 731+ throw new DOMException("HIERARCHY_REQUEST_ERR");
 732+ }
 733+ frag.appendChild(node);
 734+ }
 735+ return frag;
 736+ }
 737+
 738+ function iterateSubtree(rangeIterator, func, iteratorState) {
 739+ var it, n;
 740+ iteratorState = iteratorState || { stop: false };
 741+ for (var node, subRangeIterator; node = rangeIterator.next(); ) {
 742+ //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
 743+ if (rangeIterator.isPartiallySelectedSubtree()) {
 744+ // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the
 745+ // node selected by the Range.
 746+ if (func(node) === false) {
 747+ iteratorState.stop = true;
 748+ return;
 749+ } else {
 750+ subRangeIterator = rangeIterator.getSubtreeIterator();
 751+ iterateSubtree(subRangeIterator, func, iteratorState);
 752+ subRangeIterator.detach(true);
 753+ if (iteratorState.stop) {
 754+ return;
 755+ }
 756+ }
 757+ } else {
 758+ // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
 759+ // descendant
 760+ it = dom.createIterator(node);
 761+ while ( (n = it.next()) ) {
 762+ if (func(n) === false) {
 763+ iteratorState.stop = true;
 764+ return;
 765+ }
 766+ }
 767+ }
 768+ }
 769+ }
 770+
 771+ function deleteSubtree(iterator) {
 772+ var subIterator;
 773+ while (iterator.next()) {
 774+ if (iterator.isPartiallySelectedSubtree()) {
 775+ subIterator = iterator.getSubtreeIterator();
 776+ deleteSubtree(subIterator);
 777+ subIterator.detach(true);
 778+ } else {
 779+ iterator.remove();
 780+ }
 781+ }
 782+ }
 783+
 784+ function extractSubtree(iterator) {
 785+
 786+ for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
 787+
 788+
 789+ if (iterator.isPartiallySelectedSubtree()) {
 790+ node = node.cloneNode(false);
 791+ subIterator = iterator.getSubtreeIterator();
 792+ node.appendChild(extractSubtree(subIterator));
 793+ subIterator.detach(true);
 794+ } else {
 795+ iterator.remove();
 796+ }
 797+ if (node.nodeType == 10) { // DocumentType
 798+ throw new DOMException("HIERARCHY_REQUEST_ERR");
 799+ }
 800+ frag.appendChild(node);
 801+ }
 802+ return frag;
 803+ }
 804+
 805+ function getNodesInRange(range, nodeTypes, filter) {
 806+ //log.info("getNodesInRange, " + nodeTypes.join(","));
 807+ var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
 808+ var filterExists = !!filter;
 809+ if (filterNodeTypes) {
 810+ regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
 811+ }
 812+
 813+ var nodes = [];
 814+ iterateSubtree(new RangeIterator(range, false), function(node) {
 815+ if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {
 816+ nodes.push(node);
 817+ }
 818+ });
 819+ return nodes;
 820+ }
 821+
 822+ function inspect(range) {
 823+ var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
 824+ return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
 825+ dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
 826+ }
 827+
 828+ /*----------------------------------------------------------------------------------------------------------------*/
 829+
 830+ // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
 831+
 832+ /**
 833+ * @constructor
 834+ */
 835+ function RangeIterator(range, clonePartiallySelectedTextNodes) {
 836+ this.range = range;
 837+ this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
 838+
 839+
 840+
 841+ if (!range.collapsed) {
 842+ this.sc = range.startContainer;
 843+ this.so = range.startOffset;
 844+ this.ec = range.endContainer;
 845+ this.eo = range.endOffset;
 846+ var root = range.commonAncestorContainer;
 847+
 848+ if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {
 849+ this.isSingleCharacterDataNode = true;
 850+ this._first = this._last = this._next = this.sc;
 851+ } else {
 852+ this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?
 853+ this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true);
 854+ this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?
 855+ this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true);
 856+ }
 857+
 858+ }
 859+ }
 860+
 861+ RangeIterator.prototype = {
 862+ _current: null,
 863+ _next: null,
 864+ _first: null,
 865+ _last: null,
 866+ isSingleCharacterDataNode: false,
 867+
 868+ reset: function() {
 869+ this._current = null;
 870+ this._next = this._first;
 871+ },
 872+
 873+ hasNext: function() {
 874+ return !!this._next;
 875+ },
 876+
 877+ next: function() {
 878+ // Move to next node
 879+ var current = this._current = this._next;
 880+ if (current) {
 881+ this._next = (current !== this._last) ? current.nextSibling : null;
 882+
 883+ // Check for partially selected text nodes
 884+ if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
 885+ if (current === this.ec) {
 886+
 887+ (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
 888+ }
 889+ if (this._current === this.sc) {
 890+
 891+ (current = current.cloneNode(true)).deleteData(0, this.so);
 892+ }
 893+ }
 894+ }
 895+
 896+ return current;
 897+ },
 898+
 899+ remove: function() {
 900+ var current = this._current, start, end;
 901+
 902+ if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
 903+ start = (current === this.sc) ? this.so : 0;
 904+ end = (current === this.ec) ? this.eo : current.length;
 905+ if (start != end) {
 906+ current.deleteData(start, end - start);
 907+ }
 908+ } else {
 909+ if (current.parentNode) {
 910+ current.parentNode.removeChild(current);
 911+ } else {
 912+
 913+ }
 914+ }
 915+ },
 916+
 917+ // Checks if the current node is partially selected
 918+ isPartiallySelectedSubtree: function() {
 919+ var current = this._current;
 920+ return isNonTextPartiallySelected(current, this.range);
 921+ },
 922+
 923+ getSubtreeIterator: function() {
 924+ var subRange;
 925+ if (this.isSingleCharacterDataNode) {
 926+ subRange = this.range.cloneRange();
 927+ subRange.collapse();
 928+ } else {
 929+ subRange = new Range(getRangeDocument(this.range));
 930+ var current = this._current;
 931+ var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current);
 932+
 933+ if (dom.isAncestorOf(current, this.sc, true)) {
 934+ startContainer = this.sc;
 935+ startOffset = this.so;
 936+ }
 937+ if (dom.isAncestorOf(current, this.ec, true)) {
 938+ endContainer = this.ec;
 939+ endOffset = this.eo;
 940+ }
 941+
 942+ updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
 943+ }
 944+ return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
 945+ },
 946+
 947+ detach: function(detachRange) {
 948+ if (detachRange) {
 949+ this.range.detach();
 950+ }
 951+ this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
 952+ }
 953+ };
 954+
 955+ /*----------------------------------------------------------------------------------------------------------------*/
 956+
 957+ // Exceptions
 958+
 959+ /**
 960+ * @constructor
 961+ */
 962+ function RangeException(codeName) {
 963+ this.code = this[codeName];
 964+ this.codeName = codeName;
 965+ this.message = "RangeException: " + this.codeName;
 966+ }
 967+
 968+ RangeException.prototype = {
 969+ BAD_BOUNDARYPOINTS_ERR: 1,
 970+ INVALID_NODE_TYPE_ERR: 2
 971+ };
 972+
 973+ RangeException.prototype.toString = function() {
 974+ return this.message;
 975+ };
 976+
 977+ /*----------------------------------------------------------------------------------------------------------------*/
 978+
 979+ /**
 980+ * Currently iterates through all nodes in the range on creation until I think of a decent way to do it
 981+ * TODO: Look into making this a proper iterator, not requiring preloading everything first
 982+ * @constructor
 983+ */
 984+ function RangeNodeIterator(range, nodeTypes, filter) {
 985+ this.nodes = getNodesInRange(range, nodeTypes, filter);
 986+ this._next = this.nodes[0];
 987+ this._position = 0;
 988+ }
 989+
 990+ RangeNodeIterator.prototype = {
 991+ _current: null,
 992+
 993+ hasNext: function() {
 994+ return !!this._next;
 995+ },
 996+
 997+ next: function() {
 998+ this._current = this._next;
 999+ this._next = this.nodes[ ++this._position ];
 1000+ return this._current;
 1001+ },
 1002+
 1003+ detach: function() {
 1004+ this._current = this._next = this.nodes = null;
 1005+ }
 1006+ };
 1007+
 1008+ var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
 1009+ var rootContainerNodeTypes = [2, 9, 11];
 1010+ var readonlyNodeTypes = [5, 6, 10, 12];
 1011+ var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
 1012+ var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
 1013+
 1014+ function createAncestorFinder(nodeTypes) {
 1015+ return function(node, selfIsAncestor) {
 1016+ var t, n = selfIsAncestor ? node : node.parentNode;
 1017+ while (n) {
 1018+ t = n.nodeType;
 1019+ if (dom.arrayContains(nodeTypes, t)) {
 1020+ return n;
 1021+ }
 1022+ n = n.parentNode;
 1023+ }
 1024+ return null;
 1025+ };
 1026+ }
 1027+
 1028+ var getRootContainer = dom.getRootContainer;
 1029+ var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
 1030+ var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
 1031+ var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
 1032+
 1033+ function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
 1034+ if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
 1035+ throw new RangeException("INVALID_NODE_TYPE_ERR");
 1036+ }
 1037+ }
 1038+
 1039+ function assertNotDetached(range) {
 1040+ if (!range.startContainer) {
 1041+ throw new DOMException("INVALID_STATE_ERR");
 1042+ }
 1043+ }
 1044+
 1045+ function assertValidNodeType(node, invalidTypes) {
 1046+ if (!dom.arrayContains(invalidTypes, node.nodeType)) {
 1047+ throw new RangeException("INVALID_NODE_TYPE_ERR");
 1048+ }
 1049+ }
 1050+
 1051+ function assertValidOffset(node, offset) {
 1052+ if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
 1053+ throw new DOMException("INDEX_SIZE_ERR");
 1054+ }
 1055+ }
 1056+
 1057+ function assertSameDocumentOrFragment(node1, node2) {
 1058+ if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
 1059+ throw new DOMException("WRONG_DOCUMENT_ERR");
 1060+ }
 1061+ }
 1062+
 1063+ function assertNodeNotReadOnly(node) {
 1064+ if (getReadonlyAncestor(node, true)) {
 1065+ throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
 1066+ }
 1067+ }
 1068+
 1069+ function assertNode(node, codeName) {
 1070+ if (!node) {
 1071+ throw new DOMException(codeName);
 1072+ }
 1073+ }
 1074+
 1075+ function isOrphan(node) {
 1076+ return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
 1077+ }
 1078+
 1079+ function isValidOffset(node, offset) {
 1080+ return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length);
 1081+ }
 1082+
 1083+ function assertRangeValid(range) {
 1084+ assertNotDetached(range);
 1085+ if (isOrphan(range.startContainer) || isOrphan(range.endContainer) ||
 1086+ !isValidOffset(range.startContainer, range.startOffset) ||
 1087+ !isValidOffset(range.endContainer, range.endOffset)) {
 1088+ throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
 1089+ }
 1090+ }
 1091+
 1092+ /*----------------------------------------------------------------------------------------------------------------*/
 1093+
 1094+ // Test the browser's innerHTML support to decide how to implement createContextualFragment
 1095+ var styleEl = document.createElement("style");
 1096+ var htmlParsingConforms = false;
 1097+ try {
 1098+ styleEl.innerHTML = "<b>x</b>";
 1099+ htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
 1100+ } catch (e) {
 1101+ // IE 6 and 7 throw
 1102+ }
 1103+
 1104+ api.features.htmlParsingConforms = htmlParsingConforms;
 1105+
 1106+ var createContextualFragment = htmlParsingConforms ?
 1107+
 1108+ // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
 1109+ // discussion and base code for this implementation at issue 67.
 1110+ // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
 1111+ // Thanks to Aleks Williams.
 1112+ function(fragmentStr) {
 1113+ // "Let node the context object's start's node."
 1114+ var node = this.startContainer;
 1115+ var doc = dom.getDocument(node);
 1116+
 1117+ // "If the context object's start's node is null, raise an INVALID_STATE_ERR
 1118+ // exception and abort these steps."
 1119+ if (!node) {
 1120+ throw new DOMException("INVALID_STATE_ERR");
 1121+ }
 1122+
 1123+ // "Let element be as follows, depending on node's interface:"
 1124+ // Document, Document Fragment: null
 1125+ var el = null;
 1126+
 1127+ // "Element: node"
 1128+ if (node.nodeType == 1) {
 1129+ el = node;
 1130+
 1131+ // "Text, Comment: node's parentElement"
 1132+ } else if (dom.isCharacterDataNode(node)) {
 1133+ el = dom.parentElement(node);
 1134+ }
 1135+
 1136+ // "If either element is null or element's ownerDocument is an HTML document
 1137+ // and element's local name is "html" and element's namespace is the HTML
 1138+ // namespace"
 1139+ if (el === null || (
 1140+ el.nodeName == "HTML"
 1141+ && dom.isHtmlNamespace(dom.getDocument(el).documentElement)
 1142+ && dom.isHtmlNamespace(el)
 1143+ )) {
 1144+
 1145+ // "let element be a new Element with "body" as its local name and the HTML
 1146+ // namespace as its namespace.""
 1147+ el = doc.createElement("body");
 1148+ } else {
 1149+ el = el.cloneNode(false);
 1150+ }
 1151+
 1152+ // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
 1153+ // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
 1154+ // "In either case, the algorithm must be invoked with fragment as the input
 1155+ // and element as the context element."
 1156+ el.innerHTML = fragmentStr;
 1157+
 1158+ // "If this raises an exception, then abort these steps. Otherwise, let new
 1159+ // children be the nodes returned."
 1160+
 1161+ // "Let fragment be a new DocumentFragment."
 1162+ // "Append all new children to fragment."
 1163+ // "Return fragment."
 1164+ return dom.fragmentFromNodeChildren(el);
 1165+ } :
 1166+
 1167+ // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
 1168+ // previous versions of Rangy used (with the exception of using a body element rather than a div)
 1169+ function(fragmentStr) {
 1170+ assertNotDetached(this);
 1171+ var doc = getRangeDocument(this);
 1172+ var el = doc.createElement("body");
 1173+ el.innerHTML = fragmentStr;
 1174+
 1175+ return dom.fragmentFromNodeChildren(el);
 1176+ };
 1177+
 1178+ /*----------------------------------------------------------------------------------------------------------------*/
 1179+
 1180+ var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
 1181+ "commonAncestorContainer"];
 1182+
 1183+ var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
 1184+ var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
 1185+
 1186+ function RangePrototype() {}
 1187+
 1188+ RangePrototype.prototype = {
 1189+ attachListener: function(type, listener) {
 1190+ this._listeners[type].push(listener);
 1191+ },
 1192+
 1193+ compareBoundaryPoints: function(how, range) {
 1194+ assertRangeValid(this);
 1195+ assertSameDocumentOrFragment(this.startContainer, range.startContainer);
 1196+
 1197+ var nodeA, offsetA, nodeB, offsetB;
 1198+ var prefixA = (how == e2s || how == s2s) ? "start" : "end";
 1199+ var prefixB = (how == s2e || how == s2s) ? "start" : "end";
 1200+ nodeA = this[prefixA + "Container"];
 1201+ offsetA = this[prefixA + "Offset"];
 1202+ nodeB = range[prefixB + "Container"];
 1203+ offsetB = range[prefixB + "Offset"];
 1204+ return dom.comparePoints(nodeA, offsetA, nodeB, offsetB);
 1205+ },
 1206+
 1207+ insertNode: function(node) {
 1208+ assertRangeValid(this);
 1209+ assertValidNodeType(node, insertableNodeTypes);
 1210+ assertNodeNotReadOnly(this.startContainer);
 1211+
 1212+ if (dom.isAncestorOf(node, this.startContainer, true)) {
 1213+ throw new DOMException("HIERARCHY_REQUEST_ERR");
 1214+ }
 1215+
 1216+ // No check for whether the container of the start of the Range is of a type that does not allow
 1217+ // children of the type of node: the browser's DOM implementation should do this for us when we attempt
 1218+ // to add the node
 1219+
 1220+ var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
 1221+ this.setStartBefore(firstNodeInserted);
 1222+ },
 1223+
 1224+ cloneContents: function() {
 1225+ assertRangeValid(this);
 1226+
 1227+ var clone, frag;
 1228+ if (this.collapsed) {
 1229+ return getRangeDocument(this).createDocumentFragment();
 1230+ } else {
 1231+ if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {
 1232+ clone = this.startContainer.cloneNode(true);
 1233+ clone.data = clone.data.slice(this.startOffset, this.endOffset);
 1234+ frag = getRangeDocument(this).createDocumentFragment();
 1235+ frag.appendChild(clone);
 1236+ return frag;
 1237+ } else {
 1238+ var iterator = new RangeIterator(this, true);
 1239+ clone = cloneSubtree(iterator);
 1240+ iterator.detach();
 1241+ }
 1242+ return clone;
 1243+ }
 1244+ },
 1245+
 1246+ canSurroundContents: function() {
 1247+ assertRangeValid(this);
 1248+ assertNodeNotReadOnly(this.startContainer);
 1249+ assertNodeNotReadOnly(this.endContainer);
 1250+
 1251+ // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
 1252+ // no non-text nodes.
 1253+ var iterator = new RangeIterator(this, true);
 1254+ var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
 1255+ (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
 1256+ iterator.detach();
 1257+ return !boundariesInvalid;
 1258+ },
 1259+
 1260+ surroundContents: function(node) {
 1261+ assertValidNodeType(node, surroundNodeTypes);
 1262+
 1263+ if (!this.canSurroundContents()) {
 1264+ throw new RangeException("BAD_BOUNDARYPOINTS_ERR");
 1265+ }
 1266+
 1267+ // Extract the contents
 1268+ var content = this.extractContents();
 1269+
 1270+ // Clear the children of the node
 1271+ if (node.hasChildNodes()) {
 1272+ while (node.lastChild) {
 1273+ node.removeChild(node.lastChild);
 1274+ }
 1275+ }
 1276+
 1277+ // Insert the new node and add the extracted contents
 1278+ insertNodeAtPosition(node, this.startContainer, this.startOffset);
 1279+ node.appendChild(content);
 1280+
 1281+ this.selectNode(node);
 1282+ },
 1283+
 1284+ cloneRange: function() {
 1285+ assertRangeValid(this);
 1286+ var range = new Range(getRangeDocument(this));
 1287+ var i = rangeProperties.length, prop;
 1288+ while (i--) {
 1289+ prop = rangeProperties[i];
 1290+ range[prop] = this[prop];
 1291+ }
 1292+ return range;
 1293+ },
 1294+
 1295+ toString: function() {
 1296+ assertRangeValid(this);
 1297+ var sc = this.startContainer;
 1298+ if (sc === this.endContainer && dom.isCharacterDataNode(sc)) {
 1299+ return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
 1300+ } else {
 1301+ var textBits = [], iterator = new RangeIterator(this, true);
 1302+
 1303+ iterateSubtree(iterator, function(node) {
 1304+ // Accept only text or CDATA nodes, not comments
 1305+
 1306+ if (node.nodeType == 3 || node.nodeType == 4) {
 1307+ textBits.push(node.data);
 1308+ }
 1309+ });
 1310+ iterator.detach();
 1311+ return textBits.join("");
 1312+ }
 1313+ },
 1314+
 1315+ // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
 1316+ // been removed from Mozilla.
 1317+
 1318+ compareNode: function(node) {
 1319+ assertRangeValid(this);
 1320+
 1321+ var parent = node.parentNode;
 1322+ var nodeIndex = dom.getNodeIndex(node);
 1323+
 1324+ if (!parent) {
 1325+ throw new DOMException("NOT_FOUND_ERR");
 1326+ }
 1327+
 1328+ var startComparison = this.comparePoint(parent, nodeIndex),
 1329+ endComparison = this.comparePoint(parent, nodeIndex + 1);
 1330+
 1331+ if (startComparison < 0) { // Node starts before
 1332+ return (endComparison > 0) ? n_b_a : n_b;
 1333+ } else {
 1334+ return (endComparison > 0) ? n_a : n_i;
 1335+ }
 1336+ },
 1337+
 1338+ comparePoint: function(node, offset) {
 1339+ assertRangeValid(this);
 1340+ assertNode(node, "HIERARCHY_REQUEST_ERR");
 1341+ assertSameDocumentOrFragment(node, this.startContainer);
 1342+
 1343+ if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
 1344+ return -1;
 1345+ } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
 1346+ return 1;
 1347+ }
 1348+ return 0;
 1349+ },
 1350+
 1351+ createContextualFragment: createContextualFragment,
 1352+
 1353+ toHtml: function() {
 1354+ assertRangeValid(this);
 1355+ var container = getRangeDocument(this).createElement("div");
 1356+ container.appendChild(this.cloneContents());
 1357+ return container.innerHTML;
 1358+ },
 1359+
 1360+ // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
 1361+ // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
 1362+ intersectsNode: function(node, touchingIsIntersecting) {
 1363+ assertRangeValid(this);
 1364+ assertNode(node, "NOT_FOUND_ERR");
 1365+ if (dom.getDocument(node) !== getRangeDocument(this)) {
 1366+ return false;
 1367+ }
 1368+
 1369+ var parent = node.parentNode, offset = dom.getNodeIndex(node);
 1370+ assertNode(parent, "NOT_FOUND_ERR");
 1371+
 1372+ var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset),
 1373+ endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
 1374+
 1375+ return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
 1376+ },
 1377+
 1378+
 1379+ isPointInRange: function(node, offset) {
 1380+ assertRangeValid(this);
 1381+ assertNode(node, "HIERARCHY_REQUEST_ERR");
 1382+ assertSameDocumentOrFragment(node, this.startContainer);
 1383+
 1384+ return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
 1385+ (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
 1386+ },
 1387+
 1388+ // The methods below are non-standard and invented by me.
 1389+
 1390+ // Sharing a boundary start-to-end or end-to-start does not count as intersection.
 1391+ intersectsRange: function(range, touchingIsIntersecting) {
 1392+ assertRangeValid(this);
 1393+
 1394+ if (getRangeDocument(range) != getRangeDocument(this)) {
 1395+ throw new DOMException("WRONG_DOCUMENT_ERR");
 1396+ }
 1397+
 1398+ var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset),
 1399+ endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset);
 1400+
 1401+ return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
 1402+ },
 1403+
 1404+ intersection: function(range) {
 1405+ if (this.intersectsRange(range)) {
 1406+ var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
 1407+ endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
 1408+
 1409+ var intersectionRange = this.cloneRange();
 1410+
 1411+ if (startComparison == -1) {
 1412+ intersectionRange.setStart(range.startContainer, range.startOffset);
 1413+ }
 1414+ if (endComparison == 1) {
 1415+ intersectionRange.setEnd(range.endContainer, range.endOffset);
 1416+ }
 1417+ return intersectionRange;
 1418+ }
 1419+ return null;
 1420+ },
 1421+
 1422+ union: function(range) {
 1423+ if (this.intersectsRange(range, true)) {
 1424+ var unionRange = this.cloneRange();
 1425+ if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
 1426+ unionRange.setStart(range.startContainer, range.startOffset);
 1427+ }
 1428+ if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
 1429+ unionRange.setEnd(range.endContainer, range.endOffset);
 1430+ }
 1431+ return unionRange;
 1432+ } else {
 1433+ throw new RangeException("Ranges do not intersect");
 1434+ }
 1435+ },
 1436+
 1437+ containsNode: function(node, allowPartial) {
 1438+ if (allowPartial) {
 1439+ return this.intersectsNode(node, false);
 1440+ } else {
 1441+ return this.compareNode(node) == n_i;
 1442+ }
 1443+ },
 1444+
 1445+ containsNodeContents: function(node) {
 1446+ return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0;
 1447+ },
 1448+
 1449+ containsRange: function(range) {
 1450+ return this.intersection(range).equals(range);
 1451+ },
 1452+
 1453+ containsNodeText: function(node) {
 1454+ var nodeRange = this.cloneRange();
 1455+ nodeRange.selectNode(node);
 1456+ var textNodes = nodeRange.getNodes([3]);
 1457+ if (textNodes.length > 0) {
 1458+ nodeRange.setStart(textNodes[0], 0);
 1459+ var lastTextNode = textNodes.pop();
 1460+ nodeRange.setEnd(lastTextNode, lastTextNode.length);
 1461+ var contains = this.containsRange(nodeRange);
 1462+ nodeRange.detach();
 1463+ return contains;
 1464+ } else {
 1465+ return this.containsNodeContents(node);
 1466+ }
 1467+ },
 1468+
 1469+ createNodeIterator: function(nodeTypes, filter) {
 1470+ assertRangeValid(this);
 1471+ return new RangeNodeIterator(this, nodeTypes, filter);
 1472+ },
 1473+
 1474+ getNodes: function(nodeTypes, filter) {
 1475+ assertRangeValid(this);
 1476+ return getNodesInRange(this, nodeTypes, filter);
 1477+ },
 1478+
 1479+ getDocument: function() {
 1480+ return getRangeDocument(this);
 1481+ },
 1482+
 1483+ collapseBefore: function(node) {
 1484+ assertNotDetached(this);
 1485+
 1486+ this.setEndBefore(node);
 1487+ this.collapse(false);
 1488+ },
 1489+
 1490+ collapseAfter: function(node) {
 1491+ assertNotDetached(this);
 1492+
 1493+ this.setStartAfter(node);
 1494+ this.collapse(true);
 1495+ },
 1496+
 1497+ getName: function() {
 1498+ return "DomRange";
 1499+ },
 1500+
 1501+ equals: function(range) {
 1502+ return Range.rangesEqual(this, range);
 1503+ },
 1504+
 1505+ inspect: function() {
 1506+ return inspect(this);
 1507+ }
 1508+ };
 1509+
 1510+ function copyComparisonConstantsToObject(obj) {
 1511+ obj.START_TO_START = s2s;
 1512+ obj.START_TO_END = s2e;
 1513+ obj.END_TO_END = e2e;
 1514+ obj.END_TO_START = e2s;
 1515+
 1516+ obj.NODE_BEFORE = n_b;
 1517+ obj.NODE_AFTER = n_a;
 1518+ obj.NODE_BEFORE_AND_AFTER = n_b_a;
 1519+ obj.NODE_INSIDE = n_i;
 1520+ }
 1521+
 1522+ function copyComparisonConstants(constructor) {
 1523+ copyComparisonConstantsToObject(constructor);
 1524+ copyComparisonConstantsToObject(constructor.prototype);
 1525+ }
 1526+
 1527+ function createRangeContentRemover(remover, boundaryUpdater) {
 1528+ return function() {
 1529+ assertRangeValid(this);
 1530+
 1531+ var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
 1532+
 1533+ var iterator = new RangeIterator(this, true);
 1534+
 1535+ // Work out where to position the range after content removal
 1536+ var node, boundary;
 1537+ if (sc !== root) {
 1538+ node = dom.getClosestAncestorIn(sc, root, true);
 1539+ boundary = getBoundaryAfterNode(node);
 1540+ sc = boundary.node;
 1541+ so = boundary.offset;
 1542+ }
 1543+
 1544+ // Check none of the range is read-only
 1545+ iterateSubtree(iterator, assertNodeNotReadOnly);
 1546+
 1547+ iterator.reset();
 1548+
 1549+ // Remove the content
 1550+ var returnValue = remover(iterator);
 1551+ iterator.detach();
 1552+
 1553+ // Move to the new position
 1554+ boundaryUpdater(this, sc, so, sc, so);
 1555+
 1556+ return returnValue;
 1557+ };
 1558+ }
 1559+
 1560+ function createPrototypeRange(constructor, boundaryUpdater, detacher) {
 1561+ function createBeforeAfterNodeSetter(isBefore, isStart) {
 1562+ return function(node) {
 1563+ assertNotDetached(this);
 1564+ assertValidNodeType(node, beforeAfterNodeTypes);
 1565+ assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
 1566+
 1567+ var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
 1568+ (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
 1569+ };
 1570+ }
 1571+
 1572+ function setRangeStart(range, node, offset) {
 1573+ var ec = range.endContainer, eo = range.endOffset;
 1574+ if (node !== range.startContainer || offset !== range.startOffset) {
 1575+ // Check the root containers of the range and the new boundary, and also check whether the new boundary
 1576+ // is after the current end. In either case, collapse the range to the new position
 1577+ if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) {
 1578+ ec = node;
 1579+ eo = offset;
 1580+ }
 1581+ boundaryUpdater(range, node, offset, ec, eo);
 1582+ }
 1583+ }
 1584+
 1585+ function setRangeEnd(range, node, offset) {
 1586+ var sc = range.startContainer, so = range.startOffset;
 1587+ if (node !== range.endContainer || offset !== range.endOffset) {
 1588+ // Check the root containers of the range and the new boundary, and also check whether the new boundary
 1589+ // is after the current end. In either case, collapse the range to the new position
 1590+ if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) {
 1591+ sc = node;
 1592+ so = offset;
 1593+ }
 1594+ boundaryUpdater(range, sc, so, node, offset);
 1595+ }
 1596+ }
 1597+
 1598+ function setRangeStartAndEnd(range, node, offset) {
 1599+ if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) {
 1600+ boundaryUpdater(range, node, offset, node, offset);
 1601+ }
 1602+ }
 1603+
 1604+ constructor.prototype = new RangePrototype();
 1605+
 1606+ api.util.extend(constructor.prototype, {
 1607+ setStart: function(node, offset) {
 1608+ assertNotDetached(this);
 1609+ assertNoDocTypeNotationEntityAncestor(node, true);
 1610+ assertValidOffset(node, offset);
 1611+
 1612+ setRangeStart(this, node, offset);
 1613+ },
 1614+
 1615+ setEnd: function(node, offset) {
 1616+ assertNotDetached(this);
 1617+ assertNoDocTypeNotationEntityAncestor(node, true);
 1618+ assertValidOffset(node, offset);
 1619+
 1620+ setRangeEnd(this, node, offset);
 1621+ },
 1622+
 1623+ setStartBefore: createBeforeAfterNodeSetter(true, true),
 1624+ setStartAfter: createBeforeAfterNodeSetter(false, true),
 1625+ setEndBefore: createBeforeAfterNodeSetter(true, false),
 1626+ setEndAfter: createBeforeAfterNodeSetter(false, false),
 1627+
 1628+ collapse: function(isStart) {
 1629+ assertRangeValid(this);
 1630+ if (isStart) {
 1631+ boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
 1632+ } else {
 1633+ boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
 1634+ }
 1635+ },
 1636+
 1637+ selectNodeContents: function(node) {
 1638+ // This doesn't seem well specified: the spec talks only about selecting the node's contents, which
 1639+ // could be taken to mean only its children. However, browsers implement this the same as selectNode for
 1640+ // text nodes, so I shall do likewise
 1641+ assertNotDetached(this);
 1642+ assertNoDocTypeNotationEntityAncestor(node, true);
 1643+
 1644+ boundaryUpdater(this, node, 0, node, dom.getNodeLength(node));
 1645+ },
 1646+
 1647+ selectNode: function(node) {
 1648+ assertNotDetached(this);
 1649+ assertNoDocTypeNotationEntityAncestor(node, false);
 1650+ assertValidNodeType(node, beforeAfterNodeTypes);
 1651+
 1652+ var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
 1653+ boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
 1654+ },
 1655+
 1656+ extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
 1657+
 1658+ deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
 1659+
 1660+ canSurroundContents: function() {
 1661+ assertRangeValid(this);
 1662+ assertNodeNotReadOnly(this.startContainer);
 1663+ assertNodeNotReadOnly(this.endContainer);
 1664+
 1665+ // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
 1666+ // no non-text nodes.
 1667+ var iterator = new RangeIterator(this, true);
 1668+ var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
 1669+ (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
 1670+ iterator.detach();
 1671+ return !boundariesInvalid;
 1672+ },
 1673+
 1674+ detach: function() {
 1675+ detacher(this);
 1676+ },
 1677+
 1678+ splitBoundaries: function() {
 1679+ assertRangeValid(this);
 1680+
 1681+
 1682+ var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
 1683+ var startEndSame = (sc === ec);
 1684+
 1685+ if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
 1686+ dom.splitDataNode(ec, eo);
 1687+
 1688+ }
 1689+
 1690+ if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) {
 1691+
 1692+ sc = dom.splitDataNode(sc, so);
 1693+ if (startEndSame) {
 1694+ eo -= so;
 1695+ ec = sc;
 1696+ } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) {
 1697+ eo++;
 1698+ }
 1699+ so = 0;
 1700+
 1701+ }
 1702+ boundaryUpdater(this, sc, so, ec, eo);
 1703+ },
 1704+
 1705+ normalizeBoundaries: function() {
 1706+ assertRangeValid(this);
 1707+
 1708+ var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
 1709+
 1710+ var mergeForward = function(node) {
 1711+ var sibling = node.nextSibling;
 1712+ if (sibling && sibling.nodeType == node.nodeType) {
 1713+ ec = node;
 1714+ eo = node.length;
 1715+ node.appendData(sibling.data);
 1716+ sibling.parentNode.removeChild(sibling);
 1717+ }
 1718+ };
 1719+
 1720+ var mergeBackward = function(node) {
 1721+ var sibling = node.previousSibling;
 1722+ if (sibling && sibling.nodeType == node.nodeType) {
 1723+ sc = node;
 1724+ var nodeLength = node.length;
 1725+ so = sibling.length;
 1726+ node.insertData(0, sibling.data);
 1727+ sibling.parentNode.removeChild(sibling);
 1728+ if (sc == ec) {
 1729+ eo += so;
 1730+ ec = sc;
 1731+ } else if (ec == node.parentNode) {
 1732+ var nodeIndex = dom.getNodeIndex(node);
 1733+ if (eo == nodeIndex) {
 1734+ ec = node;
 1735+ eo = nodeLength;
 1736+ } else if (eo > nodeIndex) {
 1737+ eo--;
 1738+ }
 1739+ }
 1740+ }
 1741+ };
 1742+
 1743+ var normalizeStart = true;
 1744+
 1745+ if (dom.isCharacterDataNode(ec)) {
 1746+ if (ec.length == eo) {
 1747+ mergeForward(ec);
 1748+ }
 1749+ } else {
 1750+ if (eo > 0) {
 1751+ var endNode = ec.childNodes[eo - 1];
 1752+ if (endNode && dom.isCharacterDataNode(endNode)) {
 1753+ mergeForward(endNode);
 1754+ }
 1755+ }
 1756+ normalizeStart = !this.collapsed;
 1757+ }
 1758+
 1759+ if (normalizeStart) {
 1760+ if (dom.isCharacterDataNode(sc)) {
 1761+ if (so == 0) {
 1762+ mergeBackward(sc);
 1763+ }
 1764+ } else {
 1765+ if (so < sc.childNodes.length) {
 1766+ var startNode = sc.childNodes[so];
 1767+ if (startNode && dom.isCharacterDataNode(startNode)) {
 1768+ mergeBackward(startNode);
 1769+ }
 1770+ }
 1771+ }
 1772+ } else {
 1773+ sc = ec;
 1774+ so = eo;
 1775+ }
 1776+
 1777+ boundaryUpdater(this, sc, so, ec, eo);
 1778+ },
 1779+
 1780+ collapseToPoint: function(node, offset) {
 1781+ assertNotDetached(this);
 1782+
 1783+ assertNoDocTypeNotationEntityAncestor(node, true);
 1784+ assertValidOffset(node, offset);
 1785+
 1786+ setRangeStartAndEnd(this, node, offset);
 1787+ }
 1788+ });
 1789+
 1790+ copyComparisonConstants(constructor);
 1791+ }
 1792+
 1793+ /*----------------------------------------------------------------------------------------------------------------*/
 1794+
 1795+ // Updates commonAncestorContainer and collapsed after boundary change
 1796+ function updateCollapsedAndCommonAncestor(range) {
 1797+ range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
 1798+ range.commonAncestorContainer = range.collapsed ?
 1799+ range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
 1800+ }
 1801+
 1802+ function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
 1803+ var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);
 1804+ var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);
 1805+
 1806+ range.startContainer = startContainer;
 1807+ range.startOffset = startOffset;
 1808+ range.endContainer = endContainer;
 1809+ range.endOffset = endOffset;
 1810+
 1811+ updateCollapsedAndCommonAncestor(range);
 1812+ dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved});
 1813+ }
 1814+
 1815+ function detach(range) {
 1816+ assertNotDetached(range);
 1817+ range.startContainer = range.startOffset = range.endContainer = range.endOffset = null;
 1818+ range.collapsed = range.commonAncestorContainer = null;
 1819+ dispatchEvent(range, "detach", null);
 1820+ range._listeners = null;
 1821+ }
 1822+
 1823+ /**
 1824+ * @constructor
 1825+ */
 1826+ function Range(doc) {
 1827+ this.startContainer = doc;
 1828+ this.startOffset = 0;
 1829+ this.endContainer = doc;
 1830+ this.endOffset = 0;
 1831+ this._listeners = {
 1832+ boundarychange: [],
 1833+ detach: []
 1834+ };
 1835+ updateCollapsedAndCommonAncestor(this);
 1836+ }
 1837+
 1838+ createPrototypeRange(Range, updateBoundaries, detach);
 1839+
 1840+ api.rangePrototype = RangePrototype.prototype;
 1841+
 1842+ Range.rangeProperties = rangeProperties;
 1843+ Range.RangeIterator = RangeIterator;
 1844+ Range.copyComparisonConstants = copyComparisonConstants;
 1845+ Range.createPrototypeRange = createPrototypeRange;
 1846+ Range.inspect = inspect;
 1847+ Range.getRangeDocument = getRangeDocument;
 1848+ Range.rangesEqual = function(r1, r2) {
 1849+ return r1.startContainer === r2.startContainer &&
 1850+ r1.startOffset === r2.startOffset &&
 1851+ r1.endContainer === r2.endContainer &&
 1852+ r1.endOffset === r2.endOffset;
 1853+ };
 1854+
 1855+ api.DomRange = Range;
 1856+ api.RangeException = RangeException;
 1857+});rangy.createModule("WrappedRange", function(api, module) {
 1858+ api.requireModules( ["DomUtil", "DomRange"] );
 1859+
 1860+ /**
 1861+ * @constructor
 1862+ */
 1863+ var WrappedRange;
 1864+ var dom = api.dom;
 1865+ var DomPosition = dom.DomPosition;
 1866+ var DomRange = api.DomRange;
 1867+
 1868+
 1869+
 1870+ /*----------------------------------------------------------------------------------------------------------------*/
 1871+
 1872+ /*
 1873+ This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
 1874+ method. For example, in the following (where pipes denote the selection boundaries):
 1875+
 1876+ <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
 1877+
 1878+ var range = document.selection.createRange();
 1879+ alert(range.parentElement().id); // Should alert "ul" but alerts "b"
 1880+
 1881+ This method returns the common ancestor node of the following:
 1882+ - the parentElement() of the textRange
 1883+ - the parentElement() of the textRange after calling collapse(true)
 1884+ - the parentElement() of the textRange after calling collapse(false)
 1885+ */
 1886+ function getTextRangeContainerElement(textRange) {
 1887+ var parentEl = textRange.parentElement();
 1888+
 1889+ var range = textRange.duplicate();
 1890+ range.collapse(true);
 1891+ var startEl = range.parentElement();
 1892+ range = textRange.duplicate();
 1893+ range.collapse(false);
 1894+ var endEl = range.parentElement();
 1895+ var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
 1896+
 1897+ return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
 1898+ }
 1899+
 1900+ function textRangeIsCollapsed(textRange) {
 1901+ return textRange.compareEndPoints("StartToEnd", textRange) == 0;
 1902+ }
 1903+
 1904+ // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as
 1905+ // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has
 1906+ // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling
 1907+ // for inputs and images, plus optimizations.
 1908+ function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) {
 1909+ var workingRange = textRange.duplicate();
 1910+
 1911+ workingRange.collapse(isStart);
 1912+ var containerElement = workingRange.parentElement();
 1913+
 1914+ // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
 1915+ // check for that
 1916+ // TODO: Find out when. Workaround for wholeRangeContainerElement may break this
 1917+ if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) {
 1918+ containerElement = wholeRangeContainerElement;
 1919+
 1920+ }
 1921+
 1922+
 1923+
 1924+ // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
 1925+ // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
 1926+ if (!containerElement.canHaveHTML) {
 1927+ return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
 1928+ }
 1929+
 1930+ var workingNode = dom.getDocument(containerElement).createElement("span");
 1931+ var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
 1932+ var previousNode, nextNode, boundaryPosition, boundaryNode;
 1933+
 1934+ // Move the working range through the container's children, starting at the end and working backwards, until the
 1935+ // working range reaches or goes past the boundary we're interested in
 1936+ do {
 1937+ containerElement.insertBefore(workingNode, workingNode.previousSibling);
 1938+ workingRange.moveToElementText(workingNode);
 1939+ } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&
 1940+ workingNode.previousSibling);
 1941+
 1942+ // We've now reached or gone past the boundary of the text range we're interested in
 1943+ // so have identified the node we want
 1944+ boundaryNode = workingNode.nextSibling;
 1945+
 1946+ if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) {
 1947+ // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the
 1948+ // node containing the text range's boundary, so we move the end of the working range to the boundary point
 1949+ // and measure the length of its text to get the boundary's offset within the node.
 1950+ workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
 1951+
 1952+
 1953+ var offset;
 1954+
 1955+ if (/[\r\n]/.test(boundaryNode.data)) {
 1956+ /*
 1957+ For the particular case of a boundary within a text node containing line breaks (within a <pre> element,
 1958+ for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:
 1959+
 1960+ - Each line break is represented as \r in the text node's data/nodeValue properties
 1961+ - Each line break is represented as \r\n in the TextRange's 'text' property
 1962+ - The 'text' property of the TextRange does not contain trailing line breaks
 1963+
 1964+ To get round the problem presented by the final fact above, we can use the fact that TextRange's
 1965+ moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily
 1966+ the same as the number of characters it was instructed to move. The simplest approach is to use this to
 1967+ store the characters moved when moving both the start and end of the range to the start of the document
 1968+ body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).
 1969+ However, this is extremely slow when the document is large and the range is near the end of it. Clearly
 1970+ doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same
 1971+ problem.
 1972+
 1973+ Another approach that works is to use moveStart() to move the start boundary of the range up to the end
 1974+ boundary one character at a time and incrementing a counter with the value returned by the moveStart()
 1975+ call. However, the check for whether the start boundary has reached the end boundary is expensive, so
 1976+ this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of
 1977+ the range within the document).
 1978+
 1979+ The method below is a hybrid of the two methods above. It uses the fact that a string containing the
 1980+ TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the
 1981+ text of the TextRange, so the start of the range is moved that length initially and then a character at
 1982+ a time to make up for any trailing line breaks not contained in the 'text' property. This has good
 1983+ performance in most situations compared to the previous two methods.
 1984+ */
 1985+ var tempRange = workingRange.duplicate();
 1986+ var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
 1987+
 1988+ offset = tempRange.moveStart("character", rangeLength);
 1989+ while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
 1990+ offset++;
 1991+ tempRange.moveStart("character", 1);
 1992+ }
 1993+ } else {
 1994+ offset = workingRange.text.length;
 1995+ }
 1996+ boundaryPosition = new DomPosition(boundaryNode, offset);
 1997+ } else {
 1998+
 1999+
 2000+ // If the boundary immediately follows a character data node and this is the end boundary, we should favour
 2001+ // a position within that, and likewise for a start boundary preceding a character data node
 2002+ previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
 2003+ nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
 2004+
 2005+
 2006+
 2007+ if (nextNode && dom.isCharacterDataNode(nextNode)) {
 2008+ boundaryPosition = new DomPosition(nextNode, 0);
 2009+ } else if (previousNode && dom.isCharacterDataNode(previousNode)) {
 2010+ boundaryPosition = new DomPosition(previousNode, previousNode.length);
 2011+ } else {
 2012+ boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
 2013+ }
 2014+ }
 2015+
 2016+ // Clean up
 2017+ workingNode.parentNode.removeChild(workingNode);
 2018+
 2019+ return boundaryPosition;
 2020+ }
 2021+
 2022+ // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
 2023+ // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
 2024+ // (http://code.google.com/p/ierange/)
 2025+ function createBoundaryTextRange(boundaryPosition, isStart) {
 2026+ var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
 2027+ var doc = dom.getDocument(boundaryPosition.node);
 2028+ var workingNode, childNodes, workingRange = doc.body.createTextRange();
 2029+ var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);
 2030+
 2031+ if (nodeIsDataNode) {
 2032+ boundaryNode = boundaryPosition.node;
 2033+ boundaryParent = boundaryNode.parentNode;
 2034+ } else {
 2035+ childNodes = boundaryPosition.node.childNodes;
 2036+ boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
 2037+ boundaryParent = boundaryPosition.node;
 2038+ }
 2039+
 2040+ // Position the range immediately before the node containing the boundary
 2041+ workingNode = doc.createElement("span");
 2042+
 2043+ // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the
 2044+ // element rather than immediately before or after it, which is what we want
 2045+ workingNode.innerHTML = "&#feff;";
 2046+
 2047+ // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
 2048+ // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
 2049+ if (boundaryNode) {
 2050+ boundaryParent.insertBefore(workingNode, boundaryNode);
 2051+ } else {
 2052+ boundaryParent.appendChild(workingNode);
 2053+ }
 2054+
 2055+ workingRange.moveToElementText(workingNode);
 2056+ workingRange.collapse(!isStart);
 2057+
 2058+ // Clean up
 2059+ boundaryParent.removeChild(workingNode);
 2060+
 2061+ // Move the working range to the text offset, if required
 2062+ if (nodeIsDataNode) {
 2063+ workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
 2064+ }
 2065+
 2066+ return workingRange;
 2067+ }
 2068+
 2069+ /*----------------------------------------------------------------------------------------------------------------*/
 2070+
 2071+ if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {
 2072+ // This is a wrapper around the browser's native DOM Range. It has two aims:
 2073+ // - Provide workarounds for specific browser bugs
 2074+ // - provide convenient extensions, which are inherited from Rangy's DomRange
 2075+
 2076+ (function() {
 2077+ var rangeProto;
 2078+ var rangeProperties = DomRange.rangeProperties;
 2079+ var canSetRangeStartAfterEnd;
 2080+
 2081+ function updateRangeProperties(range) {
 2082+ var i = rangeProperties.length, prop;
 2083+ while (i--) {
 2084+ prop = rangeProperties[i];
 2085+ range[prop] = range.nativeRange[prop];
 2086+ }
 2087+ }
 2088+
 2089+ function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {
 2090+ var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
 2091+ var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
 2092+
 2093+ // Always set both boundaries for the benefit of IE9 (see issue 35)
 2094+ if (startMoved || endMoved) {
 2095+ range.setEnd(endContainer, endOffset);
 2096+ range.setStart(startContainer, startOffset);
 2097+ }
 2098+ }
 2099+
 2100+ function detach(range) {
 2101+ range.nativeRange.detach();
 2102+ range.detached = true;
 2103+ var i = rangeProperties.length, prop;
 2104+ while (i--) {
 2105+ prop = rangeProperties[i];
 2106+ range[prop] = null;
 2107+ }
 2108+ }
 2109+
 2110+ var createBeforeAfterNodeSetter;
 2111+
 2112+ WrappedRange = function(range) {
 2113+ if (!range) {
 2114+ throw new Error("Range must be specified");
 2115+ }
 2116+ this.nativeRange = range;
 2117+ updateRangeProperties(this);
 2118+ };
 2119+
 2120+ DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);
 2121+
 2122+ rangeProto = WrappedRange.prototype;
 2123+
 2124+ rangeProto.selectNode = function(node) {
 2125+ this.nativeRange.selectNode(node);
 2126+ updateRangeProperties(this);
 2127+ };
 2128+
 2129+ rangeProto.deleteContents = function() {
 2130+ this.nativeRange.deleteContents();
 2131+ updateRangeProperties(this);
 2132+ };
 2133+
 2134+ rangeProto.extractContents = function() {
 2135+ var frag = this.nativeRange.extractContents();
 2136+ updateRangeProperties(this);
 2137+ return frag;
 2138+ };
 2139+
 2140+ rangeProto.cloneContents = function() {
 2141+ return this.nativeRange.cloneContents();
 2142+ };
 2143+
 2144+ // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
 2145+ // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
 2146+ // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
 2147+ // insertNode, which works but is almost certainly slower than the native implementation.
 2148+/*
 2149+ rangeProto.insertNode = function(node) {
 2150+ this.nativeRange.insertNode(node);
 2151+ updateRangeProperties(this);
 2152+ };
 2153+*/
 2154+
 2155+ rangeProto.surroundContents = function(node) {
 2156+ this.nativeRange.surroundContents(node);
 2157+ updateRangeProperties(this);
 2158+ };
 2159+
 2160+ rangeProto.collapse = function(isStart) {
 2161+ this.nativeRange.collapse(isStart);
 2162+ updateRangeProperties(this);
 2163+ };
 2164+
 2165+ rangeProto.cloneRange = function() {
 2166+ return new WrappedRange(this.nativeRange.cloneRange());
 2167+ };
 2168+
 2169+ rangeProto.refresh = function() {
 2170+ updateRangeProperties(this);
 2171+ };
 2172+
 2173+ rangeProto.toString = function() {
 2174+ return this.nativeRange.toString();
 2175+ };
 2176+
 2177+ // Create test range and node for feature detection
 2178+
 2179+ var testTextNode = document.createTextNode("test");
 2180+ dom.getBody(document).appendChild(testTextNode);
 2181+ var range = document.createRange();
 2182+
 2183+ /*--------------------------------------------------------------------------------------------------------*/
 2184+
 2185+ // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
 2186+ // correct for it
 2187+
 2188+ range.setStart(testTextNode, 0);
 2189+ range.setEnd(testTextNode, 0);
 2190+
 2191+ try {
 2192+ range.setStart(testTextNode, 1);
 2193+ canSetRangeStartAfterEnd = true;
 2194+
 2195+ rangeProto.setStart = function(node, offset) {
 2196+ this.nativeRange.setStart(node, offset);
 2197+ updateRangeProperties(this);
 2198+ };
 2199+
 2200+ rangeProto.setEnd = function(node, offset) {
 2201+ this.nativeRange.setEnd(node, offset);
 2202+ updateRangeProperties(this);
 2203+ };
 2204+
 2205+ createBeforeAfterNodeSetter = function(name) {
 2206+ return function(node) {
 2207+ this.nativeRange[name](node);
 2208+ updateRangeProperties(this);
 2209+ };
 2210+ };
 2211+
 2212+ } catch(ex) {
 2213+
 2214+
 2215+ canSetRangeStartAfterEnd = false;
 2216+
 2217+ rangeProto.setStart = function(node, offset) {
 2218+ try {
 2219+ this.nativeRange.setStart(node, offset);
 2220+ } catch (ex) {
 2221+ this.nativeRange.setEnd(node, offset);
 2222+ this.nativeRange.setStart(node, offset);
 2223+ }
 2224+ updateRangeProperties(this);
 2225+ };
 2226+
 2227+ rangeProto.setEnd = function(node, offset) {
 2228+ try {
 2229+ this.nativeRange.setEnd(node, offset);
 2230+ } catch (ex) {
 2231+ this.nativeRange.setStart(node, offset);
 2232+ this.nativeRange.setEnd(node, offset);
 2233+ }
 2234+ updateRangeProperties(this);
 2235+ };
 2236+
 2237+ createBeforeAfterNodeSetter = function(name, oppositeName) {
 2238+ return function(node) {
 2239+ try {
 2240+ this.nativeRange[name](node);
 2241+ } catch (ex) {
 2242+ this.nativeRange[oppositeName](node);
 2243+ this.nativeRange[name](node);
 2244+ }
 2245+ updateRangeProperties(this);
 2246+ };
 2247+ };
 2248+ }
 2249+
 2250+ rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
 2251+ rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
 2252+ rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
 2253+ rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
 2254+
 2255+ /*--------------------------------------------------------------------------------------------------------*/
 2256+
 2257+ // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
 2258+ // the 0th character of the text node
 2259+ range.selectNodeContents(testTextNode);
 2260+ if (range.startContainer == testTextNode && range.endContainer == testTextNode &&
 2261+ range.startOffset == 0 && range.endOffset == testTextNode.length) {
 2262+ rangeProto.selectNodeContents = function(node) {
 2263+ this.nativeRange.selectNodeContents(node);
 2264+ updateRangeProperties(this);
 2265+ };
 2266+ } else {
 2267+ rangeProto.selectNodeContents = function(node) {
 2268+ this.setStart(node, 0);
 2269+ this.setEnd(node, DomRange.getEndOffset(node));
 2270+ };
 2271+ }
 2272+
 2273+ /*--------------------------------------------------------------------------------------------------------*/
 2274+
 2275+ // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants
 2276+ // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
 2277+
 2278+ range.selectNodeContents(testTextNode);
 2279+ range.setEnd(testTextNode, 3);
 2280+
 2281+ var range2 = document.createRange();
 2282+ range2.selectNodeContents(testTextNode);
 2283+ range2.setEnd(testTextNode, 4);
 2284+ range2.setStart(testTextNode, 2);
 2285+
 2286+ if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &
 2287+ range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
 2288+ // This is the wrong way round, so correct for it
 2289+
 2290+
 2291+ rangeProto.compareBoundaryPoints = function(type, range) {
 2292+ range = range.nativeRange || range;
 2293+ if (type == range.START_TO_END) {
 2294+ type = range.END_TO_START;
 2295+ } else if (type == range.END_TO_START) {
 2296+ type = range.START_TO_END;
 2297+ }
 2298+ return this.nativeRange.compareBoundaryPoints(type, range);
 2299+ };
 2300+ } else {
 2301+ rangeProto.compareBoundaryPoints = function(type, range) {
 2302+ return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
 2303+ };
 2304+ }
 2305+
 2306+ /*--------------------------------------------------------------------------------------------------------*/
 2307+
 2308+ // Test for existence of createContextualFragment and delegate to it if it exists
 2309+ if (api.util.isHostMethod(range, "createContextualFragment")) {
 2310+ rangeProto.createContextualFragment = function(fragmentStr) {
 2311+ return this.nativeRange.createContextualFragment(fragmentStr);
 2312+ };
 2313+ }
 2314+
 2315+ /*--------------------------------------------------------------------------------------------------------*/
 2316+
 2317+ // Clean up
 2318+ dom.getBody(document).removeChild(testTextNode);
 2319+ range.detach();
 2320+ range2.detach();
 2321+ })();
 2322+
 2323+ api.createNativeRange = function(doc) {
 2324+ doc = doc || document;
 2325+ return doc.createRange();
 2326+ };
 2327+ } else if (api.features.implementsTextRange) {
 2328+ // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
 2329+ // prototype
 2330+
 2331+ WrappedRange = function(textRange) {
 2332+ this.textRange = textRange;
 2333+ this.refresh();
 2334+ };
 2335+
 2336+ WrappedRange.prototype = new DomRange(document);
 2337+
 2338+ WrappedRange.prototype.refresh = function() {
 2339+ var start, end;
 2340+
 2341+ // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
 2342+ var rangeContainerElement = getTextRangeContainerElement(this.textRange);
 2343+
 2344+ if (textRangeIsCollapsed(this.textRange)) {
 2345+ end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);
 2346+ } else {
 2347+
 2348+ start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
 2349+ end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
 2350+ }
 2351+
 2352+ this.setStart(start.node, start.offset);
 2353+ this.setEnd(end.node, end.offset);
 2354+ };
 2355+
 2356+ DomRange.copyComparisonConstants(WrappedRange);
 2357+
 2358+ // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work
 2359+ var globalObj = (function() { return this; })();
 2360+ if (typeof globalObj.Range == "undefined") {
 2361+ globalObj.Range = WrappedRange;
 2362+ }
 2363+
 2364+ api.createNativeRange = function(doc) {
 2365+ doc = doc || document;
 2366+ return doc.body.createTextRange();
 2367+ };
 2368+ }
 2369+
 2370+ if (api.features.implementsTextRange) {
 2371+ WrappedRange.rangeToTextRange = function(range) {
 2372+ if (range.collapsed) {
 2373+ var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
 2374+
 2375+
 2376+
 2377+ return tr;
 2378+
 2379+ //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
 2380+ } else {
 2381+ var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
 2382+ var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
 2383+ var textRange = dom.getDocument(range.startContainer).body.createTextRange();
 2384+ textRange.setEndPoint("StartToStart", startRange);
 2385+ textRange.setEndPoint("EndToEnd", endRange);
 2386+ return textRange;
 2387+ }
 2388+ };
 2389+ }
 2390+
 2391+ WrappedRange.prototype.getName = function() {
 2392+ return "WrappedRange";
 2393+ };
 2394+
 2395+ api.WrappedRange = WrappedRange;
 2396+
 2397+ api.createRange = function(doc) {
 2398+ doc = doc || document;
 2399+ return new WrappedRange(api.createNativeRange(doc));
 2400+ };
 2401+
 2402+ api.createRangyRange = function(doc) {
 2403+ doc = doc || document;
 2404+ return new DomRange(doc);
 2405+ };
 2406+
 2407+ api.createIframeRange = function(iframeEl) {
 2408+ return api.createRange(dom.getIframeDocument(iframeEl));
 2409+ };
 2410+
 2411+ api.createIframeRangyRange = function(iframeEl) {
 2412+ return api.createRangyRange(dom.getIframeDocument(iframeEl));
 2413+ };
 2414+
 2415+ api.addCreateMissingNativeApiListener(function(win) {
 2416+ var doc = win.document;
 2417+ if (typeof doc.createRange == "undefined") {
 2418+ doc.createRange = function() {
 2419+ return api.createRange(this);
 2420+ };
 2421+ }
 2422+ doc = win = null;
 2423+ });
 2424+});rangy.createModule("WrappedSelection", function(api, module) {
 2425+ // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
 2426+ // spec (http://html5.org/specs/dom-range.html)
 2427+
 2428+ api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
 2429+
 2430+ api.config.checkSelectionRanges = true;
 2431+
 2432+ var BOOLEAN = "boolean",
 2433+ windowPropertyName = "_rangySelection",
 2434+ dom = api.dom,
 2435+ util = api.util,
 2436+ DomRange = api.DomRange,
 2437+ WrappedRange = api.WrappedRange,
 2438+ DOMException = api.DOMException,
 2439+ DomPosition = dom.DomPosition,
 2440+ getSelection,
 2441+ selectionIsCollapsed,
 2442+ CONTROL = "Control";
 2443+
 2444+
 2445+
 2446+ function getWinSelection(winParam) {
 2447+ return (winParam || window).getSelection();
 2448+ }
 2449+
 2450+ function getDocSelection(winParam) {
 2451+ return (winParam || window).document.selection;
 2452+ }
 2453+
 2454+ // Test for the Range/TextRange and Selection features required
 2455+ // Test for ability to retrieve selection
 2456+ var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),
 2457+ implementsDocSelection = api.util.isHostObject(document, "selection");
 2458+
 2459+ var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
 2460+
 2461+ if (useDocumentSelection) {
 2462+ getSelection = getDocSelection;
 2463+ api.isSelectionValid = function(winParam) {
 2464+ var doc = (winParam || window).document, nativeSel = doc.selection;
 2465+
 2466+ // Check whether the selection TextRange is actually contained within the correct document
 2467+ return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
 2468+ };
 2469+ } else if (implementsWinGetSelection) {
 2470+ getSelection = getWinSelection;
 2471+ api.isSelectionValid = function() {
 2472+ return true;
 2473+ };
 2474+ } else {
 2475+ module.fail("Neither document.selection or window.getSelection() detected.");
 2476+ }
 2477+
 2478+ api.getNativeSelection = getSelection;
 2479+
 2480+ var testSelection = getSelection();
 2481+ var testRange = api.createNativeRange(document);
 2482+ var body = dom.getBody(document);
 2483+
 2484+ // Obtaining a range from a selection
 2485+ var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&
 2486+ util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));
 2487+ api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
 2488+
 2489+ // Test for existence of native selection extend() method
 2490+ var selectionHasExtend = util.isHostMethod(testSelection, "extend");
 2491+ api.features.selectionHasExtend = selectionHasExtend;
 2492+
 2493+ // Test if rangeCount exists
 2494+ var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
 2495+ api.features.selectionHasRangeCount = selectionHasRangeCount;
 2496+
 2497+ var selectionSupportsMultipleRanges = false;
 2498+ var collapsedNonEditableSelectionsSupported = true;
 2499+
 2500+ if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
 2501+ typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {
 2502+
 2503+ (function() {
 2504+ var iframe = document.createElement("iframe");
 2505+ body.appendChild(iframe);
 2506+
 2507+ var iframeDoc = dom.getIframeDocument(iframe);
 2508+ iframeDoc.open();
 2509+ iframeDoc.write("<html><head></head><body>12</body></html>");
 2510+ iframeDoc.close();
 2511+
 2512+ var sel = dom.getIframeWindow(iframe).getSelection();
 2513+ var docEl = iframeDoc.documentElement;
 2514+ var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;
 2515+
 2516+ // Test whether the native selection will allow a collapsed selection within a non-editable element
 2517+ var r1 = iframeDoc.createRange();
 2518+ r1.setStart(textNode, 1);
 2519+ r1.collapse(true);
 2520+ sel.addRange(r1);
 2521+ collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
 2522+ sel.removeAllRanges();
 2523+
 2524+ // Test whether the native selection is capable of supporting multiple ranges
 2525+ var r2 = r1.cloneRange();
 2526+ r1.setStart(textNode, 0);
 2527+ r2.setEnd(textNode, 2);
 2528+ sel.addRange(r1);
 2529+ sel.addRange(r2);
 2530+
 2531+ selectionSupportsMultipleRanges = (sel.rangeCount == 2);
 2532+
 2533+ // Clean up
 2534+ r1.detach();
 2535+ r2.detach();
 2536+
 2537+ body.removeChild(iframe);
 2538+ })();
 2539+ }
 2540+
 2541+ api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
 2542+ api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
 2543+
 2544+ // ControlRanges
 2545+ var implementsControlRange = false, testControlRange;
 2546+
 2547+ if (body && util.isHostMethod(body, "createControlRange")) {
 2548+ testControlRange = body.createControlRange();
 2549+ if (util.areHostProperties(testControlRange, ["item", "add"])) {
 2550+ implementsControlRange = true;
 2551+ }
 2552+ }
 2553+ api.features.implementsControlRange = implementsControlRange;
 2554+
 2555+ // Selection collapsedness
 2556+ if (selectionHasAnchorAndFocus) {
 2557+ selectionIsCollapsed = function(sel) {
 2558+ return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
 2559+ };
 2560+ } else {
 2561+ selectionIsCollapsed = function(sel) {
 2562+ return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
 2563+ };
 2564+ }
 2565+
 2566+ function updateAnchorAndFocusFromRange(sel, range, backwards) {
 2567+ var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end";
 2568+ sel.anchorNode = range[anchorPrefix + "Container"];
 2569+ sel.anchorOffset = range[anchorPrefix + "Offset"];
 2570+ sel.focusNode = range[focusPrefix + "Container"];
 2571+ sel.focusOffset = range[focusPrefix + "Offset"];
 2572+ }
 2573+
 2574+ function updateAnchorAndFocusFromNativeSelection(sel) {
 2575+ var nativeSel = sel.nativeSelection;
 2576+ sel.anchorNode = nativeSel.anchorNode;
 2577+ sel.anchorOffset = nativeSel.anchorOffset;
 2578+ sel.focusNode = nativeSel.focusNode;
 2579+ sel.focusOffset = nativeSel.focusOffset;
 2580+ }
 2581+
 2582+ function updateEmptySelection(sel) {
 2583+ sel.anchorNode = sel.focusNode = null;
 2584+ sel.anchorOffset = sel.focusOffset = 0;
 2585+ sel.rangeCount = 0;
 2586+ sel.isCollapsed = true;
 2587+ sel._ranges.length = 0;
 2588+ }
 2589+
 2590+ function getNativeRange(range) {
 2591+ var nativeRange;
 2592+ if (range instanceof DomRange) {
 2593+ nativeRange = range._selectionNativeRange;
 2594+ if (!nativeRange) {
 2595+ nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));
 2596+ nativeRange.setEnd(range.endContainer, range.endOffset);
 2597+ nativeRange.setStart(range.startContainer, range.startOffset);
 2598+ range._selectionNativeRange = nativeRange;
 2599+ range.attachListener("detach", function() {
 2600+
 2601+ this._selectionNativeRange = null;
 2602+ });
 2603+ }
 2604+ } else if (range instanceof WrappedRange) {
 2605+ nativeRange = range.nativeRange;
 2606+ } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
 2607+ nativeRange = range;
 2608+ }
 2609+ return nativeRange;
 2610+ }
 2611+
 2612+ function rangeContainsSingleElement(rangeNodes) {
 2613+ if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
 2614+ return false;
 2615+ }
 2616+ for (var i = 1, len = rangeNodes.length; i < len; ++i) {
 2617+ if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
 2618+ return false;
 2619+ }
 2620+ }
 2621+ return true;
 2622+ }
 2623+
 2624+ function getSingleElementFromRange(range) {
 2625+ var nodes = range.getNodes();
 2626+ if (!rangeContainsSingleElement(nodes)) {
 2627+ throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
 2628+ }
 2629+ return nodes[0];
 2630+ }
 2631+
 2632+ function isTextRange(range) {
 2633+ return !!range && typeof range.text != "undefined";
 2634+ }
 2635+
 2636+ function updateFromTextRange(sel, range) {
 2637+ // Create a Range from the selected TextRange
 2638+ var wrappedRange = new WrappedRange(range);
 2639+ sel._ranges = [wrappedRange];
 2640+
 2641+ updateAnchorAndFocusFromRange(sel, wrappedRange, false);
 2642+ sel.rangeCount = 1;
 2643+ sel.isCollapsed = wrappedRange.collapsed;
 2644+ }
 2645+
 2646+ function updateControlSelection(sel) {
 2647+ // Update the wrapped selection based on what's now in the native selection
 2648+ sel._ranges.length = 0;
 2649+ if (sel.docSelection.type == "None") {
 2650+ updateEmptySelection(sel);
 2651+ } else {
 2652+ var controlRange = sel.docSelection.createRange();
 2653+ if (isTextRange(controlRange)) {
 2654+ // This case (where the selection type is "Control" and calling createRange() on the selection returns
 2655+ // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
 2656+ // ControlRange have been removed from the ControlRange and removed from the document.
 2657+ updateFromTextRange(sel, controlRange);
 2658+ } else {
 2659+ sel.rangeCount = controlRange.length;
 2660+ var range, doc = dom.getDocument(controlRange.item(0));
 2661+ for (var i = 0; i < sel.rangeCount; ++i) {
 2662+ range = api.createRange(doc);
 2663+ range.selectNode(controlRange.item(i));
 2664+ sel._ranges.push(range);
 2665+ }
 2666+ sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
 2667+ updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
 2668+ }
 2669+ }
 2670+ }
 2671+
 2672+ function addRangeToControlSelection(sel, range) {
 2673+ var controlRange = sel.docSelection.createRange();
 2674+ var rangeElement = getSingleElementFromRange(range);
 2675+
 2676+ // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
 2677+ // contained by the supplied range
 2678+ var doc = dom.getDocument(controlRange.item(0));
 2679+ var newControlRange = dom.getBody(doc).createControlRange();
 2680+ for (var i = 0, len = controlRange.length; i < len; ++i) {
 2681+ newControlRange.add(controlRange.item(i));
 2682+ }
 2683+ try {
 2684+ newControlRange.add(rangeElement);
 2685+ } catch (ex) {
 2686+ throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
 2687+ }
 2688+ newControlRange.select();
 2689+
 2690+ // Update the wrapped selection based on what's now in the native selection
 2691+ updateControlSelection(sel);
 2692+ }
 2693+
 2694+ var getSelectionRangeAt;
 2695+
 2696+ if (util.isHostMethod(testSelection, "getRangeAt")) {
 2697+ getSelectionRangeAt = function(sel, index) {
 2698+ try {
 2699+ return sel.getRangeAt(index);
 2700+ } catch(ex) {
 2701+ return null;
 2702+ }
 2703+ };
 2704+ } else if (selectionHasAnchorAndFocus) {
 2705+ getSelectionRangeAt = function(sel) {
 2706+ var doc = dom.getDocument(sel.anchorNode);
 2707+ var range = api.createRange(doc);
 2708+ range.setStart(sel.anchorNode, sel.anchorOffset);
 2709+ range.setEnd(sel.focusNode, sel.focusOffset);
 2710+
 2711+ // Handle the case when the selection was selected backwards (from the end to the start in the
 2712+ // document)
 2713+ if (range.collapsed !== this.isCollapsed) {
 2714+ range.setStart(sel.focusNode, sel.focusOffset);
 2715+ range.setEnd(sel.anchorNode, sel.anchorOffset);
 2716+ }
 2717+
 2718+ return range;
 2719+ };
 2720+ }
 2721+
 2722+ /**
 2723+ * @constructor
 2724+ */
 2725+ function WrappedSelection(selection, docSelection, win) {
 2726+ this.nativeSelection = selection;
 2727+ this.docSelection = docSelection;
 2728+ this._ranges = [];
 2729+ this.win = win;
 2730+ this.refresh();
 2731+ }
 2732+
 2733+ api.getSelection = function(win) {
 2734+ win = win || window;
 2735+ var sel = win[windowPropertyName];
 2736+ var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
 2737+ if (sel) {
 2738+ sel.nativeSelection = nativeSel;
 2739+ sel.docSelection = docSel;
 2740+ sel.refresh(win);
 2741+ } else {
 2742+ sel = new WrappedSelection(nativeSel, docSel, win);
 2743+ win[windowPropertyName] = sel;
 2744+ }
 2745+ return sel;
 2746+ };
 2747+
 2748+ api.getIframeSelection = function(iframeEl) {
 2749+ return api.getSelection(dom.getIframeWindow(iframeEl));
 2750+ };
 2751+
 2752+ var selProto = WrappedSelection.prototype;
 2753+
 2754+ function createControlSelection(sel, ranges) {
 2755+ // Ensure that the selection becomes of type "Control"
 2756+ var doc = dom.getDocument(ranges[0].startContainer);
 2757+ var controlRange = dom.getBody(doc).createControlRange();
 2758+ for (var i = 0, el; i < rangeCount; ++i) {
 2759+ el = getSingleElementFromRange(ranges[i]);
 2760+ try {
 2761+ controlRange.add(el);
 2762+ } catch (ex) {
 2763+ throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");
 2764+ }
 2765+ }
 2766+ controlRange.select();
 2767+
 2768+ // Update the wrapped selection based on what's now in the native selection
 2769+ updateControlSelection(sel);
 2770+ }
 2771+
 2772+ // Selecting a range
 2773+ if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
 2774+ selProto.removeAllRanges = function() {
 2775+ this.nativeSelection.removeAllRanges();
 2776+ updateEmptySelection(this);
 2777+ };
 2778+
 2779+ var addRangeBackwards = function(sel, range) {
 2780+ var doc = DomRange.getRangeDocument(range);
 2781+ var endRange = api.createRange(doc);
 2782+ endRange.collapseToPoint(range.endContainer, range.endOffset);
 2783+ sel.nativeSelection.addRange(getNativeRange(endRange));
 2784+ sel.nativeSelection.extend(range.startContainer, range.startOffset);
 2785+ sel.refresh();
 2786+ };
 2787+
 2788+ if (selectionHasRangeCount) {
 2789+ selProto.addRange = function(range, backwards) {
 2790+ if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
 2791+ addRangeToControlSelection(this, range);
 2792+ } else {
 2793+ if (backwards && selectionHasExtend) {
 2794+ addRangeBackwards(this, range);
 2795+ } else {
 2796+ var previousRangeCount;
 2797+ if (selectionSupportsMultipleRanges) {
 2798+ previousRangeCount = this.rangeCount;
 2799+ } else {
 2800+ this.removeAllRanges();
 2801+ previousRangeCount = 0;
 2802+ }
 2803+ this.nativeSelection.addRange(getNativeRange(range));
 2804+
 2805+ // Check whether adding the range was successful
 2806+ this.rangeCount = this.nativeSelection.rangeCount;
 2807+
 2808+ if (this.rangeCount == previousRangeCount + 1) {
 2809+ // The range was added successfully
 2810+
 2811+ // Check whether the range that we added to the selection is reflected in the last range extracted from
 2812+ // the selection
 2813+ if (api.config.checkSelectionRanges) {
 2814+ var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
 2815+ if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) {
 2816+ // Happens in WebKit with, for example, a selection placed at the start of a text node
 2817+ range = new WrappedRange(nativeRange);
 2818+ }
 2819+ }
 2820+ this._ranges[this.rangeCount - 1] = range;
 2821+ updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));
 2822+ this.isCollapsed = selectionIsCollapsed(this);
 2823+ } else {
 2824+ // The range was not added successfully. The simplest thing is to refresh
 2825+ this.refresh();
 2826+ }
 2827+ }
 2828+ }
 2829+ };
 2830+ } else {
 2831+ selProto.addRange = function(range, backwards) {
 2832+ if (backwards && selectionHasExtend) {
 2833+ addRangeBackwards(this, range);
 2834+ } else {
 2835+ this.nativeSelection.addRange(getNativeRange(range));
 2836+ this.refresh();
 2837+ }
 2838+ };
 2839+ }
 2840+
 2841+ selProto.setRanges = function(ranges) {
 2842+ if (implementsControlRange && ranges.length > 1) {
 2843+ createControlSelection(this, ranges);
 2844+ } else {
 2845+ this.removeAllRanges();
 2846+ for (var i = 0, len = ranges.length; i < len; ++i) {
 2847+ this.addRange(ranges[i]);
 2848+ }
 2849+ }
 2850+ };
 2851+ } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&
 2852+ implementsControlRange && useDocumentSelection) {
 2853+
 2854+ selProto.removeAllRanges = function() {
 2855+ // Added try/catch as fix for issue #21
 2856+ try {
 2857+ this.docSelection.empty();
 2858+
 2859+ // Check for empty() not working (issue #24)
 2860+ if (this.docSelection.type != "None") {
 2861+ // Work around failure to empty a control selection by instead selecting a TextRange and then
 2862+ // calling empty()
 2863+ var doc;
 2864+ if (this.anchorNode) {
 2865+ doc = dom.getDocument(this.anchorNode);
 2866+ } else if (this.docSelection.type == CONTROL) {
 2867+ var controlRange = this.docSelection.createRange();
 2868+ if (controlRange.length) {
 2869+ doc = dom.getDocument(controlRange.item(0)).body.createTextRange();
 2870+ }
 2871+ }
 2872+ if (doc) {
 2873+ var textRange = doc.body.createTextRange();
 2874+ textRange.select();
 2875+ this.docSelection.empty();
 2876+ }
 2877+ }
 2878+ } catch(ex) {}
 2879+ updateEmptySelection(this);
 2880+ };
 2881+
 2882+ selProto.addRange = function(range) {
 2883+ if (this.docSelection.type == CONTROL) {
 2884+ addRangeToControlSelection(this, range);
 2885+ } else {
 2886+ WrappedRange.rangeToTextRange(range).select();
 2887+ this._ranges[0] = range;
 2888+ this.rangeCount = 1;
 2889+ this.isCollapsed = this._ranges[0].collapsed;
 2890+ updateAnchorAndFocusFromRange(this, range, false);
 2891+ }
 2892+ };
 2893+
 2894+ selProto.setRanges = function(ranges) {
 2895+ this.removeAllRanges();
 2896+ var rangeCount = ranges.length;
 2897+ if (rangeCount > 1) {
 2898+ createControlSelection(this, ranges);
 2899+ } else if (rangeCount) {
 2900+ this.addRange(ranges[0]);
 2901+ }
 2902+ };
 2903+ } else {
 2904+ module.fail("No means of selecting a Range or TextRange was found");
 2905+ return false;
 2906+ }
 2907+
 2908+ selProto.getRangeAt = function(index) {
 2909+ if (index < 0 || index >= this.rangeCount) {
 2910+ throw new DOMException("INDEX_SIZE_ERR");
 2911+ } else {
 2912+ return this._ranges[index];
 2913+ }
 2914+ };
 2915+
 2916+ var refreshSelection;
 2917+
 2918+ if (useDocumentSelection) {
 2919+ refreshSelection = function(sel) {
 2920+ var range;
 2921+ if (api.isSelectionValid(sel.win)) {
 2922+ range = sel.docSelection.createRange();
 2923+ } else {
 2924+ range = dom.getBody(sel.win.document).createTextRange();
 2925+ range.collapse(true);
 2926+ }
 2927+
 2928+
 2929+ if (sel.docSelection.type == CONTROL) {
 2930+ updateControlSelection(sel);
 2931+ } else if (isTextRange(range)) {
 2932+ updateFromTextRange(sel, range);
 2933+ } else {
 2934+ updateEmptySelection(sel);
 2935+ }
 2936+ };
 2937+ } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") {
 2938+ refreshSelection = function(sel) {
 2939+ if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
 2940+ updateControlSelection(sel);
 2941+ } else {
 2942+ sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
 2943+ if (sel.rangeCount) {
 2944+ for (var i = 0, len = sel.rangeCount; i < len; ++i) {
 2945+ sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
 2946+ }
 2947+ updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection));
 2948+ sel.isCollapsed = selectionIsCollapsed(sel);
 2949+ } else {
 2950+ updateEmptySelection(sel);
 2951+ }
 2952+ }
 2953+ };
 2954+ } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {
 2955+ refreshSelection = function(sel) {
 2956+ var range, nativeSel = sel.nativeSelection;
 2957+ if (nativeSel.anchorNode) {
 2958+ range = getSelectionRangeAt(nativeSel, 0);
 2959+ sel._ranges = [range];
 2960+ sel.rangeCount = 1;
 2961+ updateAnchorAndFocusFromNativeSelection(sel);
 2962+ sel.isCollapsed = selectionIsCollapsed(sel);
 2963+ } else {
 2964+ updateEmptySelection(sel);
 2965+ }
 2966+ };
 2967+ } else {
 2968+ module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
 2969+ return false;
 2970+ }
 2971+
 2972+ selProto.refresh = function(checkForChanges) {
 2973+ var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
 2974+ refreshSelection(this);
 2975+ if (checkForChanges) {
 2976+ var i = oldRanges.length;
 2977+ if (i != this._ranges.length) {
 2978+ return false;
 2979+ }
 2980+ while (i--) {
 2981+ if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
 2982+ return false;
 2983+ }
 2984+ }
 2985+ return true;
 2986+ }
 2987+ };
 2988+
 2989+ // Removal of a single range
 2990+ var removeRangeManually = function(sel, range) {
 2991+ var ranges = sel.getAllRanges(), removed = false;
 2992+ sel.removeAllRanges();
 2993+ for (var i = 0, len = ranges.length; i < len; ++i) {
 2994+ if (removed || range !== ranges[i]) {
 2995+ sel.addRange(ranges[i]);
 2996+ } else {
 2997+ // According to the draft WHATWG Range spec, the same range may be added to the selection multiple
 2998+ // times. removeRange should only remove the first instance, so the following ensures only the first
 2999+ // instance is removed
 3000+ removed = true;
 3001+ }
 3002+ }
 3003+ if (!sel.rangeCount) {
 3004+ updateEmptySelection(sel);
 3005+ }
 3006+ };
 3007+
 3008+ if (implementsControlRange) {
 3009+ selProto.removeRange = function(range) {
 3010+ if (this.docSelection.type == CONTROL) {
 3011+ var controlRange = this.docSelection.createRange();
 3012+ var rangeElement = getSingleElementFromRange(range);
 3013+
 3014+ // Create a new ControlRange containing all the elements in the selected ControlRange minus the
 3015+ // element contained by the supplied range
 3016+ var doc = dom.getDocument(controlRange.item(0));
 3017+ var newControlRange = dom.getBody(doc).createControlRange();
 3018+ var el, removed = false;
 3019+ for (var i = 0, len = controlRange.length; i < len; ++i) {
 3020+ el = controlRange.item(i);
 3021+ if (el !== rangeElement || removed) {
 3022+ newControlRange.add(controlRange.item(i));
 3023+ } else {
 3024+ removed = true;
 3025+ }
 3026+ }
 3027+ newControlRange.select();
 3028+
 3029+ // Update the wrapped selection based on what's now in the native selection
 3030+ updateControlSelection(this);
 3031+ } else {
 3032+ removeRangeManually(this, range);
 3033+ }
 3034+ };
 3035+ } else {
 3036+ selProto.removeRange = function(range) {
 3037+ removeRangeManually(this, range);
 3038+ };
 3039+ }
 3040+
 3041+ // Detecting if a selection is backwards
 3042+ var selectionIsBackwards;
 3043+ if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {
 3044+ selectionIsBackwards = function(sel) {
 3045+ var backwards = false;
 3046+ if (sel.anchorNode) {
 3047+ backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
 3048+ }
 3049+ return backwards;
 3050+ };
 3051+
 3052+ selProto.isBackwards = function() {
 3053+ return selectionIsBackwards(this);
 3054+ };
 3055+ } else {
 3056+ selectionIsBackwards = selProto.isBackwards = function() {
 3057+ return false;
 3058+ };
 3059+ }
 3060+
 3061+ // Selection text
 3062+ // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation
 3063+ selProto.toString = function() {
 3064+
 3065+ var rangeTexts = [];
 3066+ for (var i = 0, len = this.rangeCount; i < len; ++i) {
 3067+ rangeTexts[i] = "" + this._ranges[i];
 3068+ }
 3069+ return rangeTexts.join("");
 3070+ };
 3071+
 3072+ function assertNodeInSameDocument(sel, node) {
 3073+ if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
 3074+ throw new DOMException("WRONG_DOCUMENT_ERR");
 3075+ }
 3076+ }
 3077+
 3078+ // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
 3079+ selProto.collapse = function(node, offset) {
 3080+ assertNodeInSameDocument(this, node);
 3081+ var range = api.createRange(dom.getDocument(node));
 3082+ range.collapseToPoint(node, offset);
 3083+ this.removeAllRanges();
 3084+ this.addRange(range);
 3085+ this.isCollapsed = true;
 3086+ };
 3087+
 3088+ selProto.collapseToStart = function() {
 3089+ if (this.rangeCount) {
 3090+ var range = this._ranges[0];
 3091+ this.collapse(range.startContainer, range.startOffset);
 3092+ } else {
 3093+ throw new DOMException("INVALID_STATE_ERR");
 3094+ }
 3095+ };
 3096+
 3097+ selProto.collapseToEnd = function() {
 3098+ if (this.rangeCount) {
 3099+ var range = this._ranges[this.rangeCount - 1];
 3100+ this.collapse(range.endContainer, range.endOffset);
 3101+ } else {
 3102+ throw new DOMException("INVALID_STATE_ERR");
 3103+ }
 3104+ };
 3105+
 3106+ // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is
 3107+ // never used by Rangy.
 3108+ selProto.selectAllChildren = function(node) {
 3109+ assertNodeInSameDocument(this, node);
 3110+ var range = api.createRange(dom.getDocument(node));
 3111+ range.selectNodeContents(node);
 3112+ this.removeAllRanges();
 3113+ this.addRange(range);
 3114+ };
 3115+
 3116+ selProto.deleteFromDocument = function() {
 3117+ // Sepcial behaviour required for Control selections
 3118+ if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
 3119+ var controlRange = this.docSelection.createRange();
 3120+ var element;
 3121+ while (controlRange.length) {
 3122+ element = controlRange.item(0);
 3123+ controlRange.remove(element);
 3124+ element.parentNode.removeChild(element);
 3125+ }
 3126+ this.refresh();
 3127+ } else if (this.rangeCount) {
 3128+ var ranges = this.getAllRanges();
 3129+ this.removeAllRanges();
 3130+ for (var i = 0, len = ranges.length; i < len; ++i) {
 3131+ ranges[i].deleteContents();
 3132+ }
 3133+ // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each
 3134+ // range. Firefox moves the selection to where the final selected range was, so we emulate that
 3135+ this.addRange(ranges[len - 1]);
 3136+ }
 3137+ };
 3138+
 3139+ // The following are non-standard extensions
 3140+ selProto.getAllRanges = function() {
 3141+ return this._ranges.slice(0);
 3142+ };
 3143+
 3144+ selProto.setSingleRange = function(range) {
 3145+ this.setRanges( [range] );
 3146+ };
 3147+
 3148+ selProto.containsNode = function(node, allowPartial) {
 3149+ for (var i = 0, len = this._ranges.length; i < len; ++i) {
 3150+ if (this._ranges[i].containsNode(node, allowPartial)) {
 3151+ return true;
 3152+ }
 3153+ }
 3154+ return false;
 3155+ };
 3156+
 3157+ selProto.toHtml = function() {
 3158+ var html = "";
 3159+ if (this.rangeCount) {
 3160+ var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");
 3161+ for (var i = 0, len = this._ranges.length; i < len; ++i) {
 3162+ container.appendChild(this._ranges[i].cloneContents());
 3163+ }
 3164+ html = container.innerHTML;
 3165+ }
 3166+ return html;
 3167+ };
 3168+
 3169+ function inspect(sel) {
 3170+ var rangeInspects = [];
 3171+ var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
 3172+ var focus = new DomPosition(sel.focusNode, sel.focusOffset);
 3173+ var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
 3174+
 3175+ if (typeof sel.rangeCount != "undefined") {
 3176+ for (var i = 0, len = sel.rangeCount; i < len; ++i) {
 3177+ rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
 3178+ }
 3179+ }
 3180+ return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
 3181+ ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
 3182+
 3183+ }
 3184+
 3185+ selProto.getName = function() {
 3186+ return "WrappedSelection";
 3187+ };
 3188+
 3189+ selProto.inspect = function() {
 3190+ return inspect(this);
 3191+ };
 3192+
 3193+ selProto.detach = function() {
 3194+ this.win[windowPropertyName] = null;
 3195+ this.win = this.anchorNode = this.focusNode = null;
 3196+ };
 3197+
 3198+ WrappedSelection.inspect = inspect;
 3199+
 3200+ api.Selection = WrappedSelection;
 3201+
 3202+ api.selectionPrototype = selProto;
 3203+
 3204+ api.addCreateMissingNativeApiListener(function(win) {
 3205+ if (typeof win.getSelection == "undefined") {
 3206+ win.getSelection = function() {
 3207+ return api.getSelection(this);
 3208+ };
 3209+ }
 3210+ win = null;
 3211+ });
 3212+});
Index: trunk/extensions/VisualEditor/modules/rangy/rangy-selectionsaverestore.js
@@ -0,0 +1,195 @@
 2+/**
 3+ * @license Selection save and restore module for Rangy.
 4+ * Saves and restores user selections using marker invisible elements in the DOM.
 5+ *
 6+ * Part of Rangy, a cross-browser JavaScript range and selection library
 7+ * http://code.google.com/p/rangy/
 8+ *
 9+ * Depends on Rangy core.
 10+ *
 11+ * Copyright 2011, Tim Down
 12+ * Licensed under the MIT license.
 13+ * Version: 1.2.2
 14+ * Build date: 13 November 2011
 15+ */
 16+rangy.createModule("SaveRestore", function(api, module) {
 17+ api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
 18+
 19+ var dom = api.dom;
 20+
 21+ var markerTextChar = "\ufeff";
 22+
 23+ function gEBI(id, doc) {
 24+ return (doc || document).getElementById(id);
 25+ }
 26+
 27+ function insertRangeBoundaryMarker(range, atStart) {
 28+ var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
 29+ var markerEl;
 30+ var doc = dom.getDocument(range.startContainer);
 31+
 32+ // Clone the Range and collapse to the appropriate boundary point
 33+ var boundaryRange = range.cloneRange();
 34+ boundaryRange.collapse(atStart);
 35+
 36+ // Create the marker element containing a single invisible character using DOM methods and insert it
 37+ markerEl = doc.createElement("span");
 38+ markerEl.id = markerId;
 39+ markerEl.style.lineHeight = "0";
 40+ markerEl.style.display = "none";
 41+ markerEl.className = "rangySelectionBoundary";
 42+ markerEl.appendChild(doc.createTextNode(markerTextChar));
 43+
 44+ boundaryRange.insertNode(markerEl);
 45+ boundaryRange.detach();
 46+ return markerEl;
 47+ }
 48+
 49+ function setRangeBoundary(doc, range, markerId, atStart) {
 50+ var markerEl = gEBI(markerId, doc);
 51+ if (markerEl) {
 52+ range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
 53+ markerEl.parentNode.removeChild(markerEl);
 54+ } else {
 55+ module.warn("Marker element has been removed. Cannot restore selection.");
 56+ }
 57+ }
 58+
 59+ function compareRanges(r1, r2) {
 60+ return r2.compareBoundaryPoints(r1.START_TO_START, r1);
 61+ }
 62+
 63+ function saveSelection(win) {
 64+ win = win || window;
 65+ var doc = win.document;
 66+ if (!api.isSelectionValid(win)) {
 67+ module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
 68+ return;
 69+ }
 70+ var sel = api.getSelection(win);
 71+ var ranges = sel.getAllRanges();
 72+ var rangeInfos = [], startEl, endEl, range;
 73+
 74+ // Order the ranges by position within the DOM, latest first
 75+ ranges.sort(compareRanges);
 76+
 77+ for (var i = 0, len = ranges.length; i < len; ++i) {
 78+ range = ranges[i];
 79+ if (range.collapsed) {
 80+ endEl = insertRangeBoundaryMarker(range, false);
 81+ rangeInfos.push({
 82+ markerId: endEl.id,
 83+ collapsed: true
 84+ });
 85+ } else {
 86+ endEl = insertRangeBoundaryMarker(range, false);
 87+ startEl = insertRangeBoundaryMarker(range, true);
 88+
 89+ rangeInfos[i] = {
 90+ startMarkerId: startEl.id,
 91+ endMarkerId: endEl.id,
 92+ collapsed: false,
 93+ backwards: ranges.length == 1 && sel.isBackwards()
 94+ };
 95+ }
 96+ }
 97+
 98+ // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
 99+ // between its markers
 100+ for (i = len - 1; i >= 0; --i) {
 101+ range = ranges[i];
 102+ if (range.collapsed) {
 103+ range.collapseBefore(gEBI(rangeInfos[i].markerId, doc));
 104+ } else {
 105+ range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
 106+ range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
 107+ }
 108+ }
 109+
 110+ // Ensure current selection is unaffected
 111+ sel.setRanges(ranges);
 112+ return {
 113+ win: win,
 114+ doc: doc,
 115+ rangeInfos: rangeInfos,
 116+ restored: false
 117+ };
 118+ }
 119+
 120+ function restoreSelection(savedSelection, preserveDirection) {
 121+ if (!savedSelection.restored) {
 122+ var rangeInfos = savedSelection.rangeInfos;
 123+ var sel = api.getSelection(savedSelection.win);
 124+ var ranges = [];
 125+
 126+ // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
 127+ // normalization affecting previously restored ranges.
 128+ for (var len = rangeInfos.length, i = len - 1, rangeInfo, range; i >= 0; --i) {
 129+ rangeInfo = rangeInfos[i];
 130+ range = api.createRange(savedSelection.doc);
 131+ if (rangeInfo.collapsed) {
 132+ var markerEl = gEBI(rangeInfo.markerId, savedSelection.doc);
 133+ if (markerEl) {
 134+ markerEl.style.display = "inline";
 135+ var previousNode = markerEl.previousSibling;
 136+
 137+ // Workaround for issue 17
 138+ if (previousNode && previousNode.nodeType == 3) {
 139+ markerEl.parentNode.removeChild(markerEl);
 140+ range.collapseToPoint(previousNode, previousNode.length);
 141+ } else {
 142+ range.collapseBefore(markerEl);
 143+ markerEl.parentNode.removeChild(markerEl);
 144+ }
 145+ } else {
 146+ module.warn("Marker element has been removed. Cannot restore selection.");
 147+ }
 148+ } else {
 149+ setRangeBoundary(savedSelection.doc, range, rangeInfo.startMarkerId, true);
 150+ setRangeBoundary(savedSelection.doc, range, rangeInfo.endMarkerId, false);
 151+ }
 152+
 153+ // Normalizing range boundaries is only viable if the selection contains only one range. For example,
 154+ // if the selection contained two ranges that were both contained within the same single text node,
 155+ // both would alter the same text node when restoring and break the other range.
 156+ if (len == 1) {
 157+ range.normalizeBoundaries();
 158+ }
 159+ ranges[i] = range;
 160+ }
 161+ if (len == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backwards) {
 162+ sel.removeAllRanges();
 163+ sel.addRange(ranges[0], true);
 164+ } else {
 165+ sel.setRanges(ranges);
 166+ }
 167+
 168+ savedSelection.restored = true;
 169+ }
 170+ }
 171+
 172+ function removeMarkerElement(doc, markerId) {
 173+ var markerEl = gEBI(markerId, doc);
 174+ if (markerEl) {
 175+ markerEl.parentNode.removeChild(markerEl);
 176+ }
 177+ }
 178+
 179+ function removeMarkers(savedSelection) {
 180+ var rangeInfos = savedSelection.rangeInfos;
 181+ for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
 182+ rangeInfo = rangeInfos[i];
 183+ if (rangeInfo.collapsed) {
 184+ removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
 185+ } else {
 186+ removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
 187+ removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
 188+ }
 189+ }
 190+ }
 191+
 192+ api.saveSelection = saveSelection;
 193+ api.restoreSelection = restoreSelection;
 194+ api.removeMarkerElement = removeMarkerElement;
 195+ api.removeMarkers = removeMarkers;
 196+});
Index: trunk/extensions/VisualEditor/modules/rangy/rangy-serializer.js
@@ -0,0 +1,300 @@
 2+/**
 3+ * @license Serializer module for Rangy.
 4+ * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
 5+ * cookie or local storage and restore it on the user's next visit to the same page.
 6+ *
 7+ * Part of Rangy, a cross-browser JavaScript range and selection library
 8+ * http://code.google.com/p/rangy/
 9+ *
 10+ * Depends on Rangy core.
 11+ *
 12+ * Copyright 2011, Tim Down
 13+ * Licensed under the MIT license.
 14+ * Version: 1.2.2
 15+ * Build date: 13 November 2011
 16+ */
 17+rangy.createModule("Serializer", function(api, module) {
 18+ api.requireModules( ["WrappedSelection", "WrappedRange"] );
 19+ var UNDEF = "undefined";
 20+
 21+ // encodeURIComponent and decodeURIComponent are required for cookie handling
 22+ if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
 23+ module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");
 24+ }
 25+
 26+ // Checksum for checking whether range can be serialized
 27+ var crc32 = (function() {
 28+ function utf8encode(str) {
 29+ var utf8CharCodes = [];
 30+
 31+ for (var i = 0, len = str.length, c; i < len; ++i) {
 32+ c = str.charCodeAt(i);
 33+ if (c < 128) {
 34+ utf8CharCodes.push(c);
 35+ } else if (c < 2048) {
 36+ utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
 37+ } else {
 38+ utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
 39+ }
 40+ }
 41+ return utf8CharCodes;
 42+ }
 43+
 44+ var cachedCrcTable = null;
 45+
 46+ function buildCRCTable() {
 47+ var table = [];
 48+ for (var i = 0, j, crc; i < 256; ++i) {
 49+ crc = i;
 50+ j = 8;
 51+ while (j--) {
 52+ if ((crc & 1) == 1) {
 53+ crc = (crc >>> 1) ^ 0xEDB88320;
 54+ } else {
 55+ crc >>>= 1;
 56+ }
 57+ }
 58+ table[i] = crc >>> 0;
 59+ }
 60+ return table;
 61+ }
 62+
 63+ function getCrcTable() {
 64+ if (!cachedCrcTable) {
 65+ cachedCrcTable = buildCRCTable();
 66+ }
 67+ return cachedCrcTable;
 68+ }
 69+
 70+ return function(str) {
 71+ var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
 72+ for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
 73+ y = (crc ^ utf8CharCodes[i]) & 0xFF;
 74+ crc = (crc >>> 8) ^ crcTable[y];
 75+ }
 76+ return (crc ^ -1) >>> 0;
 77+ };
 78+ })();
 79+
 80+ var dom = api.dom;
 81+
 82+ function escapeTextForHtml(str) {
 83+ return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
 84+ }
 85+
 86+ function nodeToInfoString(node, infoParts) {
 87+ infoParts = infoParts || [];
 88+ var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
 89+ var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
 90+ var start = "", end = "";
 91+ switch (nodeType) {
 92+ case 3: // Text node
 93+ start = escapeTextForHtml(node.nodeValue);
 94+ break;
 95+ case 8: // Comment
 96+ start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
 97+ break;
 98+ default:
 99+ start = "<" + nodeInfo + ">";
 100+ end = "</>";
 101+ break;
 102+ }
 103+ if (start) {
 104+ infoParts.push(start);
 105+ }
 106+ for (var i = 0; i < childCount; ++i) {
 107+ nodeToInfoString(children[i], infoParts);
 108+ }
 109+ if (end) {
 110+ infoParts.push(end);
 111+ }
 112+ return infoParts;
 113+ }
 114+
 115+ // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
 116+ // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
 117+ // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
 118+ // innerHTML whenever the user changes an input within the element.
 119+ function getElementChecksum(el) {
 120+ var info = nodeToInfoString(el).join("");
 121+ return crc32(info).toString(16);
 122+ }
 123+
 124+ function serializePosition(node, offset, rootNode) {
 125+ var pathBits = [], n = node;
 126+ rootNode = rootNode || dom.getDocument(node).documentElement;
 127+ while (n && n != rootNode) {
 128+ pathBits.push(dom.getNodeIndex(n, true));
 129+ n = n.parentNode;
 130+ }
 131+ return pathBits.join("/") + ":" + offset;
 132+ }
 133+
 134+ function deserializePosition(serialized, rootNode, doc) {
 135+ if (rootNode) {
 136+ doc = doc || dom.getDocument(rootNode);
 137+ } else {
 138+ doc = doc || document;
 139+ rootNode = doc.documentElement;
 140+ }
 141+ var bits = serialized.split(":");
 142+ var node = rootNode;
 143+ var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;
 144+
 145+ while (i--) {
 146+ nodeIndex = parseInt(nodeIndices[i], 10);
 147+ if (nodeIndex < node.childNodes.length) {
 148+ node = node.childNodes[parseInt(nodeIndices[i], 10)];
 149+ } else {
 150+ throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +
 151+ " has no child with index " + nodeIndex + ", " + i);
 152+ }
 153+ }
 154+
 155+ return new dom.DomPosition(node, parseInt(bits[1], 10));
 156+ }
 157+
 158+ function serializeRange(range, omitChecksum, rootNode) {
 159+ rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
 160+ if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {
 161+ throw new Error("serializeRange: range is not wholly contained within specified root node");
 162+ }
 163+ var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
 164+ serializePosition(range.endContainer, range.endOffset, rootNode);
 165+ if (!omitChecksum) {
 166+ serialized += "{" + getElementChecksum(rootNode) + "}";
 167+ }
 168+ return serialized;
 169+ }
 170+
 171+ function deserializeRange(serialized, rootNode, doc) {
 172+ if (rootNode) {
 173+ doc = doc || dom.getDocument(rootNode);
 174+ } else {
 175+ doc = doc || document;
 176+ rootNode = doc.documentElement;
 177+ }
 178+ var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);
 179+ var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
 180+ if (checksum && checksum !== getElementChecksum(rootNode)) {
 181+ throw new Error("deserializeRange: checksums of serialized range root node (" + checksum +
 182+ ") and target root node (" + rootNodeChecksum + ") do not match");
 183+ }
 184+ var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
 185+ var range = api.createRange(doc);
 186+ range.setStart(start.node, start.offset);
 187+ range.setEnd(end.node, end.offset);
 188+ return range;
 189+ }
 190+
 191+ function canDeserializeRange(serialized, rootNode, doc) {
 192+ if (rootNode) {
 193+ doc = doc || dom.getDocument(rootNode);
 194+ } else {
 195+ doc = doc || document;
 196+ rootNode = doc.documentElement;
 197+ }
 198+ var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);
 199+ var checksum = result[3];
 200+ return !checksum || checksum === getElementChecksum(rootNode);
 201+ }
 202+
 203+ function serializeSelection(selection, omitChecksum, rootNode) {
 204+ selection = selection || api.getSelection();
 205+ var ranges = selection.getAllRanges(), serializedRanges = [];
 206+ for (var i = 0, len = ranges.length; i < len; ++i) {
 207+ serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
 208+ }
 209+ return serializedRanges.join("|");
 210+ }
 211+
 212+ function deserializeSelection(serialized, rootNode, win) {
 213+ if (rootNode) {
 214+ win = win || dom.getWindow(rootNode);
 215+ } else {
 216+ win = win || window;
 217+ rootNode = win.document.documentElement;
 218+ }
 219+ var serializedRanges = serialized.split("|");
 220+ var sel = api.getSelection(win);
 221+ var ranges = [];
 222+
 223+ for (var i = 0, len = serializedRanges.length; i < len; ++i) {
 224+ ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
 225+ }
 226+ sel.setRanges(ranges);
 227+
 228+ return sel;
 229+ }
 230+
 231+ function canDeserializeSelection(serialized, rootNode, win) {
 232+ var doc;
 233+ if (rootNode) {
 234+ doc = win ? win.document : dom.getDocument(rootNode);
 235+ } else {
 236+ win = win || window;
 237+ rootNode = win.document.documentElement;
 238+ }
 239+ var serializedRanges = serialized.split("|");
 240+
 241+ for (var i = 0, len = serializedRanges.length; i < len; ++i) {
 242+ if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
 243+ return false;
 244+ }
 245+ }
 246+
 247+ return true;
 248+ }
 249+
 250+
 251+ var cookieName = "rangySerializedSelection";
 252+
 253+ function getSerializedSelectionFromCookie(cookie) {
 254+ var parts = cookie.split(/[;,]/);
 255+ for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
 256+ nameVal = parts[i].split("=");
 257+ if (nameVal[0].replace(/^\s+/, "") == cookieName) {
 258+ val = nameVal[1];
 259+ if (val) {
 260+ return decodeURIComponent(val.replace(/\s+$/, ""));
 261+ }
 262+ }
 263+ }
 264+ return null;
 265+ }
 266+
 267+ function restoreSelectionFromCookie(win) {
 268+ win = win || window;
 269+ var serialized = getSerializedSelectionFromCookie(win.document.cookie);
 270+ if (serialized) {
 271+ deserializeSelection(serialized, win.doc)
 272+ }
 273+ }
 274+
 275+ function saveSelectionCookie(win, props) {
 276+ win = win || window;
 277+ props = (typeof props == "object") ? props : {};
 278+ var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
 279+ var path = props.path ? ";path=" + props.path : "";
 280+ var domain = props.domain ? ";domain=" + props.domain : "";
 281+ var secure = props.secure ? ";secure" : "";
 282+ var serialized = serializeSelection(api.getSelection(win));
 283+ win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
 284+ }
 285+
 286+ api.serializePosition = serializePosition;
 287+ api.deserializePosition = deserializePosition;
 288+
 289+ api.serializeRange = serializeRange;
 290+ api.deserializeRange = deserializeRange;
 291+ api.canDeserializeRange = canDeserializeRange;
 292+
 293+ api.serializeSelection = serializeSelection;
 294+ api.deserializeSelection = deserializeSelection;
 295+ api.canDeserializeSelection = canDeserializeSelection;
 296+
 297+ api.restoreSelectionFromCookie = restoreSelectionFromCookie;
 298+ api.saveSelectionCookie = saveSelectionCookie;
 299+
 300+ api.getElementChecksum = getElementChecksum;
 301+});
Index: trunk/extensions/VisualEditor/modules/rangy/rangy-cssclassapplier.js
@@ -0,0 +1,713 @@
 2+/**
 3+ * @license CSS Class Applier module for Rangy.
 4+ * Adds, removes and toggles CSS classes on Ranges and Selections
 5+ *
 6+ * Part of Rangy, a cross-browser JavaScript range and selection library
 7+ * http://code.google.com/p/rangy/
 8+ *
 9+ * Depends on Rangy core.
 10+ *
 11+ * Copyright 2011, Tim Down
 12+ * Licensed under the MIT license.
 13+ * Version: 1.2.2
 14+ * Build date: 13 November 2011
 15+ */
 16+rangy.createModule("CssClassApplier", function(api, module) {
 17+ api.requireModules( ["WrappedSelection", "WrappedRange"] );
 18+
 19+ var dom = api.dom;
 20+
 21+
 22+
 23+ var defaultTagName = "span";
 24+
 25+ function trim(str) {
 26+ return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
 27+ }
 28+
 29+ function hasClass(el, cssClass) {
 30+ return el.className && new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)").test(el.className);
 31+ }
 32+
 33+ function addClass(el, cssClass) {
 34+ if (el.className) {
 35+ if (!hasClass(el, cssClass)) {
 36+ el.className += " " + cssClass;
 37+ }
 38+ } else {
 39+ el.className = cssClass;
 40+ }
 41+ }
 42+
 43+ var removeClass = (function() {
 44+ function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
 45+ return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
 46+ }
 47+
 48+ return function(el, cssClass) {
 49+ if (el.className) {
 50+ el.className = el.className.replace(new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)"), replacer);
 51+ }
 52+ };
 53+ })();
 54+
 55+ function sortClassName(className) {
 56+ return className.split(/\s+/).sort().join(" ");
 57+ }
 58+
 59+ function getSortedClassName(el) {
 60+ return sortClassName(el.className);
 61+ }
 62+
 63+ function haveSameClasses(el1, el2) {
 64+ return getSortedClassName(el1) == getSortedClassName(el2);
 65+ }
 66+
 67+ function replaceWithOwnChildren(el) {
 68+
 69+ var parent = el.parentNode;
 70+ while (el.hasChildNodes()) {
 71+ parent.insertBefore(el.firstChild, el);
 72+ }
 73+ parent.removeChild(el);
 74+ }
 75+
 76+ function rangeSelectsAnyText(range, textNode) {
 77+ var textRange = range.cloneRange();
 78+ textRange.selectNodeContents(textNode);
 79+
 80+ var intersectionRange = textRange.intersection(range);
 81+ var text = intersectionRange ? intersectionRange.toString() : "";
 82+ textRange.detach();
 83+
 84+ return text != "";
 85+ }
 86+
 87+ function getEffectiveTextNodes(range) {
 88+ return range.getNodes([3], function(textNode) {
 89+ return rangeSelectsAnyText(range, textNode);
 90+ });
 91+ }
 92+
 93+ function elementsHaveSameNonClassAttributes(el1, el2) {
 94+ if (el1.attributes.length != el2.attributes.length) return false;
 95+ for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
 96+ attr1 = el1.attributes[i];
 97+ name = attr1.name;
 98+ if (name != "class") {
 99+ attr2 = el2.attributes.getNamedItem(name);
 100+ if (attr1.specified != attr2.specified) return false;
 101+ if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
 102+ }
 103+ }
 104+ return true;
 105+ }
 106+
 107+ function elementHasNonClassAttributes(el, exceptions) {
 108+ for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
 109+ attrName = el.attributes[i].name;
 110+ if ( !(exceptions && dom.arrayContains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
 111+ return true;
 112+ }
 113+ }
 114+ return false;
 115+ }
 116+
 117+ function elementHasProps(el, props) {
 118+ for (var p in props) {
 119+ if (props.hasOwnProperty(p) && el[p] !== props[p]) {
 120+ return false;
 121+ }
 122+ }
 123+ return true;
 124+ }
 125+
 126+ var getComputedStyleProperty;
 127+
 128+ if (typeof window.getComputedStyle != "undefined") {
 129+ getComputedStyleProperty = function(el, propName) {
 130+ return dom.getWindow(el).getComputedStyle(el, null)[propName];
 131+ };
 132+ } else if (typeof document.documentElement.currentStyle != "undefined") {
 133+ getComputedStyleProperty = function(el, propName) {
 134+ return el.currentStyle[propName];
 135+ };
 136+ } else {
 137+ module.fail("No means of obtaining computed style properties found");
 138+ }
 139+
 140+ var isEditableElement;
 141+
 142+ (function() {
 143+ var testEl = document.createElement("div");
 144+ if (typeof testEl.isContentEditable == "boolean") {
 145+ isEditableElement = function(node) {
 146+ return node && node.nodeType == 1 && node.isContentEditable;
 147+ };
 148+ } else {
 149+ isEditableElement = function(node) {
 150+ if (!node || node.nodeType != 1 || node.contentEditable == "false") {
 151+ return false;
 152+ }
 153+ return node.contentEditable == "true" || isEditableElement(node.parentNode);
 154+ };
 155+ }
 156+ })();
 157+
 158+ function isEditingHost(node) {
 159+ var parent;
 160+ return node && node.nodeType == 1
 161+ && (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on")
 162+ || (isEditableElement(node) && !isEditableElement(node.parentNode)));
 163+ }
 164+
 165+ function isEditable(node) {
 166+ return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
 167+ }
 168+
 169+ var inlineDisplayRegex = /^inline(-block|-table)?$/i;
 170+
 171+ function isNonInlineElement(node) {
 172+ return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
 173+ }
 174+
 175+ // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
 176+ var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
 177+
 178+ function isUnrenderedWhiteSpaceNode(node) {
 179+ if (node.data.length == 0) {
 180+ return true;
 181+ }
 182+ if (htmlNonWhiteSpaceRegex.test(node.data)) {
 183+ return false;
 184+ }
 185+ var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
 186+ switch (cssWhiteSpace) {
 187+ case "pre":
 188+ case "pre-wrap":
 189+ case "-moz-pre-wrap":
 190+ return false;
 191+ case "pre-line":
 192+ if (/[\r\n]/.test(node.data)) {
 193+ return false;
 194+ }
 195+ }
 196+
 197+ // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
 198+ // non-inline element, it will not be rendered. This seems to be a good enough definition.
 199+ return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
 200+ }
 201+
 202+ function isSplitPoint(node, offset) {
 203+ if (dom.isCharacterDataNode(node)) {
 204+ if (offset == 0) {
 205+ return !!node.previousSibling;
 206+ } else if (offset == node.length) {
 207+ return !!node.nextSibling;
 208+ } else {
 209+ return true;
 210+ }
 211+ }
 212+
 213+ return offset > 0 && offset < node.childNodes.length;
 214+ }
 215+
 216+ function splitNodeAt(node, descendantNode, descendantOffset, rangesToPreserve) {
 217+ var newNode;
 218+ var splitAtStart = (descendantOffset == 0);
 219+
 220+ if (dom.isAncestorOf(descendantNode, node)) {
 221+
 222+ return node;
 223+ }
 224+
 225+ if (dom.isCharacterDataNode(descendantNode)) {
 226+ if (descendantOffset == 0) {
 227+ descendantOffset = dom.getNodeIndex(descendantNode);
 228+ descendantNode = descendantNode.parentNode;
 229+ } else if (descendantOffset == descendantNode.length) {
 230+ descendantOffset = dom.getNodeIndex(descendantNode) + 1;
 231+ descendantNode = descendantNode.parentNode;
 232+ } else {
 233+ throw module.createError("splitNodeAt should not be called with offset in the middle of a data node ("
 234+ + descendantOffset + " in " + descendantNode.data);
 235+ }
 236+ }
 237+
 238+ if (isSplitPoint(descendantNode, descendantOffset)) {
 239+ if (!newNode) {
 240+ newNode = descendantNode.cloneNode(false);
 241+ if (newNode.id) {
 242+ newNode.removeAttribute("id");
 243+ }
 244+ var child;
 245+ while ((child = descendantNode.childNodes[descendantOffset])) {
 246+ newNode.appendChild(child);
 247+ }
 248+ dom.insertAfter(newNode, descendantNode);
 249+ }
 250+ return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, dom.getNodeIndex(newNode), rangesToPreserve);
 251+ } else if (node != descendantNode) {
 252+ newNode = descendantNode.parentNode;
 253+
 254+ // Work out a new split point in the parent node
 255+ var newNodeIndex = dom.getNodeIndex(descendantNode);
 256+
 257+ if (!splitAtStart) {
 258+ newNodeIndex++;
 259+ }
 260+ return splitNodeAt(node, newNode, newNodeIndex, rangesToPreserve);
 261+ }
 262+ return node;
 263+ }
 264+
 265+ function areElementsMergeable(el1, el2) {
 266+ return el1.tagName == el2.tagName && haveSameClasses(el1, el2) && elementsHaveSameNonClassAttributes(el1, el2);
 267+ }
 268+
 269+ function createAdjacentMergeableTextNodeGetter(forward) {
 270+ var propName = forward ? "nextSibling" : "previousSibling";
 271+
 272+ return function(textNode, checkParentElement) {
 273+ var el = textNode.parentNode;
 274+ var adjacentNode = textNode[propName];
 275+ if (adjacentNode) {
 276+ // Can merge if the node's previous/next sibling is a text node
 277+ if (adjacentNode && adjacentNode.nodeType == 3) {
 278+ return adjacentNode;
 279+ }
 280+ } else if (checkParentElement) {
 281+ // Compare text node parent element with its sibling
 282+ adjacentNode = el[propName];
 283+
 284+ if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
 285+ return adjacentNode[forward ? "firstChild" : "lastChild"];
 286+ }
 287+ }
 288+ return null;
 289+ }
 290+ }
 291+
 292+ var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
 293+ getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
 294+
 295+
 296+ function Merge(firstNode) {
 297+ this.isElementMerge = (firstNode.nodeType == 1);
 298+ this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
 299+ this.textNodes = [this.firstTextNode];
 300+ }
 301+
 302+ Merge.prototype = {
 303+ doMerge: function() {
 304+ var textBits = [], textNode, parent, text;
 305+ for (var i = 0, len = this.textNodes.length; i < len; ++i) {
 306+ textNode = this.textNodes[i];
 307+ parent = textNode.parentNode;
 308+ textBits[i] = textNode.data;
 309+ if (i) {
 310+ parent.removeChild(textNode);
 311+ if (!parent.hasChildNodes()) {
 312+ parent.parentNode.removeChild(parent);
 313+ }
 314+ }
 315+ }
 316+ this.firstTextNode.data = text = textBits.join("");
 317+ return text;
 318+ },
 319+
 320+ getLength: function() {
 321+ var i = this.textNodes.length, len = 0;
 322+ while (i--) {
 323+ len += this.textNodes[i].length;
 324+ }
 325+ return len;
 326+ },
 327+
 328+ toString: function() {
 329+ var textBits = [];
 330+ for (var i = 0, len = this.textNodes.length; i < len; ++i) {
 331+ textBits[i] = "'" + this.textNodes[i].data + "'";
 332+ }
 333+ return "[Merge(" + textBits.join(",") + ")]";
 334+ }
 335+ };
 336+
 337+ var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly"];
 338+
 339+ // Allow "class" as a property name in object properties
 340+ var mappedPropertyNames = {"class" : "className"};
 341+
 342+ function CssClassApplier(cssClass, options, tagNames) {
 343+ this.cssClass = cssClass;
 344+ var normalize, i, len, propName;
 345+
 346+ var elementPropertiesFromOptions = null;
 347+
 348+ // Initialize from options object
 349+ if (typeof options == "object" && options !== null) {
 350+ tagNames = options.tagNames;
 351+ elementPropertiesFromOptions = options.elementProperties;
 352+
 353+ for (i = 0; propName = optionProperties[i++]; ) {
 354+ if (options.hasOwnProperty(propName)) {
 355+ this[propName] = options[propName];
 356+ }
 357+ }
 358+ normalize = options.normalize;
 359+ } else {
 360+ normalize = options;
 361+ }
 362+
 363+ // Backwards compatibility: the second parameter can also be a Boolean indicating whether normalization
 364+ this.normalize = (typeof normalize == "undefined") ? true : normalize;
 365+
 366+ // Initialize element properties and attribute exceptions
 367+ this.attrExceptions = [];
 368+ var el = document.createElement(this.elementTagName);
 369+ this.elementProperties = {};
 370+ for (var p in elementPropertiesFromOptions) {
 371+ if (elementPropertiesFromOptions.hasOwnProperty(p)) {
 372+ // Map "class" to "className"
 373+ if (mappedPropertyNames.hasOwnProperty(p)) {
 374+ p = mappedPropertyNames[p];
 375+ }
 376+ el[p] = elementPropertiesFromOptions[p];
 377+
 378+ // Copy the property back from the dummy element so that later comparisons to check whether elements
 379+ // may be removed are checking against the right value. For example, the href property of an element
 380+ // returns a fully qualified URL even if it was previously assigned a relative URL.
 381+ this.elementProperties[p] = el[p];
 382+ this.attrExceptions.push(p);
 383+ }
 384+ }
 385+
 386+ this.elementSortedClassName = this.elementProperties.hasOwnProperty("className") ?
 387+ sortClassName(this.elementProperties.className + " " + cssClass) : cssClass;
 388+
 389+ // Initialize tag names
 390+ this.applyToAnyTagName = false;
 391+ var type = typeof tagNames;
 392+ if (type == "string") {
 393+ if (tagNames == "*") {
 394+ this.applyToAnyTagName = true;
 395+ } else {
 396+ this.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
 397+ }
 398+ } else if (type == "object" && typeof tagNames.length == "number") {
 399+ this.tagNames = [];
 400+ for (i = 0, len = tagNames.length; i < len; ++i) {
 401+ if (tagNames[i] == "*") {
 402+ this.applyToAnyTagName = true;
 403+ } else {
 404+ this.tagNames.push(tagNames[i].toLowerCase());
 405+ }
 406+ }
 407+ } else {
 408+ this.tagNames = [this.elementTagName];
 409+ }
 410+ }
 411+
 412+ CssClassApplier.prototype = {
 413+ elementTagName: defaultTagName,
 414+ elementProperties: {},
 415+ ignoreWhiteSpace: true,
 416+ applyToEditableOnly: false,
 417+
 418+ hasClass: function(node) {
 419+ return node.nodeType == 1 && dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && hasClass(node, this.cssClass);
 420+ },
 421+
 422+ getSelfOrAncestorWithClass: function(node) {
 423+ while (node) {
 424+ if (this.hasClass(node, this.cssClass)) {
 425+ return node;
 426+ }
 427+ node = node.parentNode;
 428+ }
 429+ return null;
 430+ },
 431+
 432+ isModifiable: function(node) {
 433+ return !this.applyToEditableOnly || isEditable(node);
 434+ },
 435+
 436+ // White space adjacent to an unwrappable node can be ignored for wrapping
 437+ isIgnorableWhiteSpaceNode: function(node) {
 438+ return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
 439+ },
 440+
 441+ // Normalizes nodes after applying a CSS class to a Range.
 442+ postApply: function(textNodes, range, isUndo) {
 443+
 444+ var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
 445+
 446+ var merges = [], currentMerge;
 447+
 448+ var rangeStartNode = firstNode, rangeEndNode = lastNode;
 449+ var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
 450+
 451+ var textNode, precedingTextNode;
 452+
 453+ for (var i = 0, len = textNodes.length; i < len; ++i) {
 454+ textNode = textNodes[i];
 455+ precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
 456+
 457+ if (precedingTextNode) {
 458+ if (!currentMerge) {
 459+ currentMerge = new Merge(precedingTextNode);
 460+ merges.push(currentMerge);
 461+ }
 462+ currentMerge.textNodes.push(textNode);
 463+ if (textNode === firstNode) {
 464+ rangeStartNode = currentMerge.firstTextNode;
 465+ rangeStartOffset = rangeStartNode.length;
 466+ }
 467+ if (textNode === lastNode) {
 468+ rangeEndNode = currentMerge.firstTextNode;
 469+ rangeEndOffset = currentMerge.getLength();
 470+ }
 471+ } else {
 472+ currentMerge = null;
 473+ }
 474+ }
 475+
 476+ // Test whether the first node after the range needs merging
 477+ var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
 478+
 479+ if (nextTextNode) {
 480+ if (!currentMerge) {
 481+ currentMerge = new Merge(lastNode);
 482+ merges.push(currentMerge);
 483+ }
 484+ currentMerge.textNodes.push(nextTextNode);
 485+ }
 486+
 487+ // Do the merges
 488+ if (merges.length) {
 489+
 490+ for (i = 0, len = merges.length; i < len; ++i) {
 491+ merges[i].doMerge();
 492+ }
 493+
 494+
 495+ // Set the range boundaries
 496+ range.setStart(rangeStartNode, rangeStartOffset);
 497+ range.setEnd(rangeEndNode, rangeEndOffset);
 498+ }
 499+
 500+ },
 501+
 502+ createContainer: function(doc) {
 503+ var el = doc.createElement(this.elementTagName);
 504+ api.util.extend(el, this.elementProperties);
 505+ addClass(el, this.cssClass);
 506+ return el;
 507+ },
 508+
 509+ applyToTextNode: function(textNode) {
 510+
 511+
 512+ var parent = textNode.parentNode;
 513+ if (parent.childNodes.length == 1 && dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
 514+ addClass(parent, this.cssClass);
 515+ } else {
 516+ var el = this.createContainer(dom.getDocument(textNode));
 517+ textNode.parentNode.insertBefore(el, textNode);
 518+ el.appendChild(textNode);
 519+ }
 520+
 521+ },
 522+
 523+ isRemovable: function(el) {
 524+ return el.tagName.toLowerCase() == this.elementTagName
 525+ && getSortedClassName(el) == this.elementSortedClassName
 526+ && elementHasProps(el, this.elementProperties)
 527+ && !elementHasNonClassAttributes(el, this.attrExceptions)
 528+ && this.isModifiable(el);
 529+ },
 530+
 531+ undoToTextNode: function(textNode, range, ancestorWithClass) {
 532+
 533+ if (!range.containsNode(ancestorWithClass)) {
 534+ // Split out the portion of the ancestor from which we can remove the CSS class
 535+ //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
 536+ var ancestorRange = range.cloneRange();
 537+ ancestorRange.selectNode(ancestorWithClass);
 538+
 539+ if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)/* && isSplitPoint(range.endContainer, range.endOffset)*/) {
 540+ splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, [range]);
 541+ range.setEndAfter(ancestorWithClass);
 542+ }
 543+ if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)/* && isSplitPoint(range.startContainer, range.startOffset)*/) {
 544+ ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, [range]);
 545+ }
 546+ }
 547+
 548+ if (this.isRemovable(ancestorWithClass)) {
 549+ replaceWithOwnChildren(ancestorWithClass);
 550+ } else {
 551+ removeClass(ancestorWithClass, this.cssClass);
 552+ }
 553+ },
 554+
 555+ applyToRange: function(range) {
 556+ range.splitBoundaries();
 557+ var textNodes = getEffectiveTextNodes(range);
 558+
 559+ if (textNodes.length) {
 560+ var textNode;
 561+
 562+ for (var i = 0, len = textNodes.length; i < len; ++i) {
 563+ textNode = textNodes[i];
 564+
 565+ if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode)
 566+ && this.isModifiable(textNode)) {
 567+ this.applyToTextNode(textNode);
 568+ }
 569+ }
 570+ range.setStart(textNodes[0], 0);
 571+ textNode = textNodes[textNodes.length - 1];
 572+ range.setEnd(textNode, textNode.length);
 573+ if (this.normalize) {
 574+ this.postApply(textNodes, range, false);
 575+ }
 576+ }
 577+ },
 578+
 579+ applyToSelection: function(win) {
 580+
 581+ win = win || window;
 582+ var sel = api.getSelection(win);
 583+
 584+ var range, ranges = sel.getAllRanges();
 585+ sel.removeAllRanges();
 586+ var i = ranges.length;
 587+ while (i--) {
 588+ range = ranges[i];
 589+ this.applyToRange(range);
 590+ sel.addRange(range);
 591+ }
 592+
 593+ },
 594+
 595+ undoToRange: function(range) {
 596+
 597+ range.splitBoundaries();
 598+ var textNodes = getEffectiveTextNodes(range);
 599+ var textNode, ancestorWithClass;
 600+ var lastTextNode = textNodes[textNodes.length - 1];
 601+
 602+ if (textNodes.length) {
 603+ for (var i = 0, len = textNodes.length; i < len; ++i) {
 604+ textNode = textNodes[i];
 605+ ancestorWithClass = this.getSelfOrAncestorWithClass(textNode);
 606+ if (ancestorWithClass && this.isModifiable(textNode)) {
 607+ this.undoToTextNode(textNode, range, ancestorWithClass);
 608+ }
 609+
 610+ // Ensure the range is still valid
 611+ range.setStart(textNodes[0], 0);
 612+ range.setEnd(lastTextNode, lastTextNode.length);
 613+ }
 614+
 615+
 616+
 617+ if (this.normalize) {
 618+ this.postApply(textNodes, range, true);
 619+ }
 620+ }
 621+ },
 622+
 623+ undoToSelection: function(win) {
 624+ win = win || window;
 625+ var sel = api.getSelection(win);
 626+ var ranges = sel.getAllRanges(), range;
 627+ sel.removeAllRanges();
 628+ for (var i = 0, len = ranges.length; i < len; ++i) {
 629+ range = ranges[i];
 630+ this.undoToRange(range);
 631+ sel.addRange(range);
 632+ }
 633+ },
 634+
 635+ getTextSelectedByRange: function(textNode, range) {
 636+ var textRange = range.cloneRange();
 637+ textRange.selectNodeContents(textNode);
 638+
 639+ var intersectionRange = textRange.intersection(range);
 640+ var text = intersectionRange ? intersectionRange.toString() : "";
 641+ textRange.detach();
 642+
 643+ return text;
 644+ },
 645+
 646+ isAppliedToRange: function(range) {
 647+ if (range.collapsed) {
 648+ return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
 649+ } else {
 650+ var textNodes = range.getNodes( [3] );
 651+ for (var i = 0, textNode; textNode = textNodes[i++]; ) {
 652+ if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode)
 653+ && this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
 654+ return false;
 655+ }
 656+ }
 657+ return true;
 658+ }
 659+ },
 660+
 661+ isAppliedToSelection: function(win) {
 662+ win = win || window;
 663+ var sel = api.getSelection(win);
 664+ var ranges = sel.getAllRanges();
 665+ var i = ranges.length;
 666+ while (i--) {
 667+ if (!this.isAppliedToRange(ranges[i])) {
 668+ return false;
 669+ }
 670+ }
 671+
 672+ return true;
 673+ },
 674+
 675+ toggleRange: function(range) {
 676+ if (this.isAppliedToRange(range)) {
 677+ this.undoToRange(range);
 678+ } else {
 679+ this.applyToRange(range);
 680+ }
 681+ },
 682+
 683+ toggleSelection: function(win) {
 684+ if (this.isAppliedToSelection(win)) {
 685+ this.undoToSelection(win);
 686+ } else {
 687+ this.applyToSelection(win);
 688+ }
 689+ },
 690+
 691+ detach: function() {}
 692+ };
 693+
 694+ function createCssClassApplier(cssClass, options, tagNames) {
 695+ return new CssClassApplier(cssClass, options, tagNames);
 696+ }
 697+
 698+ CssClassApplier.util = {
 699+ hasClass: hasClass,
 700+ addClass: addClass,
 701+ removeClass: removeClass,
 702+ hasSameClasses: haveSameClasses,
 703+ replaceWithOwnChildren: replaceWithOwnChildren,
 704+ elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
 705+ elementHasNonClassAttributes: elementHasNonClassAttributes,
 706+ splitNodeAt: splitNodeAt,
 707+ isEditableElement: isEditableElement,
 708+ isEditingHost: isEditingHost,
 709+ isEditable: isEditable
 710+ };
 711+
 712+ api.CssClassApplier = CssClassApplier;
 713+ api.createCssClassApplier = createCssClassApplier;
 714+});
Index: trunk/extensions/VisualEditor/modules/rangy/rangy-position.js
@@ -0,0 +1,364 @@
 2+/**
 3+ * @license Position module for Rangy.
 4+ * Extensions to Range and Selection objects to provide access to pixel positions relative to the viewport or document.
 5+ *
 6+ * Part of Rangy, a cross-browser JavaScript range and selection library
 7+ * http://code.google.com/p/rangy/
 8+ *
 9+ * Depends on Rangy core.
 10+ *
 11+ * Copyright %%build:year%%, Tim Down
 12+ * Licensed under the MIT license.
 13+ * Version: %%build:version%%
 14+ * Build date: %%build:date%%
 15+ */
 16+rangy.createModule("Coordinates", function(api, module) {
 17+ api.requireModules( ["WrappedSelection", "WrappedRange"] );
 18+
 19+ var NUMBER = "number";
 20+ var WrappedRange = api.WrappedRange;
 21+ var dom = api.dom, util = api.util;
 22+
 23+ // Since Rangy can deal with multiple documents, we have to do the checks every time, unless we cache a
 24+ // getScrollPosition function in each document. This would necessarily pollute the document's global
 25+ // namespace, which I'm choosing to view as a greater evil than a slight performance hit.
 26+ function getScrollPosition(win) {
 27+ var x = 0, y = 0;
 28+ if (typeof win.pageXOffset == NUMBER && typeof win.pageYOffset == NUMBER) {
 29+ x = win.pageXOffset;
 30+ y = win.pageYOffset;
 31+ } else {
 32+ var doc = win.document;
 33+ var docEl = doc.documentElement;
 34+ var compatMode = doc.compatMode;
 35+ var scrollEl = (typeof compatMode == "string" && compatMode.indexOf("CSS") >= 0 && docEl)
 36+ ? docEl : dom.getBody(doc);
 37+
 38+ if (scrollEl && typeof scrollEl.scrollLeft == NUMBER && typeof scrollEl.scrollTop == NUMBER) {
 39+ try {
 40+ x = scrollEl.scrollLeft;
 41+ y = scrollEl.scrollTop;
 42+ } catch (ex) {}
 43+ }
 44+ }
 45+ return { x: x, y: y };
 46+ }
 47+
 48+ function getAncestorElement(node, tagName) {
 49+ tagName = tagName.toLowerCase();
 50+ while (node) {
 51+ if (node.nodeType == 1 && node.tagName.toLowerCase() == tagName) {
 52+ return node;
 53+ }
 54+ node = node.parentNode;
 55+ }
 56+ return null;
 57+ }
 58+
 59+ function Rect(top, right, bottom, left) {
 60+ this.top = top;
 61+ this.right = right;
 62+ this.bottom = bottom;
 63+ this.left = left;
 64+ this.width = right - left;
 65+ this.height = bottom - top;
 66+ }
 67+
 68+ function createRelativeRect(rect, dx, dy) {
 69+ return new Rect(rect.top + dy, rect.right + dx, rect.bottom + dy, rect.left + dx);
 70+ }
 71+
 72+ function adjustClientRect(rect, doc) {
 73+ // Older IEs have an issue with a two pixel margin on the body element
 74+ var dx = 0, dy = 0;
 75+ var docEl = doc.documentElement, body = dom.getBody(doc);
 76+ var container = (docEl.clientWidth === 0 && typeof body.clientTop == NUMBER) ? body : docEl;
 77+ var clientLeft = container.clientLeft, clientTop = container.clientTop;
 78+ if (clientLeft) {
 79+ dx = -clientLeft;
 80+ }
 81+ if (clientTop) {
 82+ dy = -clientTop;
 83+ }
 84+ return createRelativeRect(rect, dx, dy);
 85+ }
 86+
 87+ function mergeRects(rects) {
 88+ var tops = [], bottoms = [], lefts = [], rights = [];
 89+ for (var i = 0, len = rects.length, rect; i < len; ++i) {
 90+ rect = rects[i];
 91+ if (rect) {
 92+ tops.push(rect.top);
 93+ bottoms.push(rect.bottom);
 94+ lefts.push(rect.left);
 95+ rights.push(rect.right);
 96+ }
 97+ }
 98+ return new Rect(
 99+ Math.min.apply(Math, tops),
 100+ Math.max.apply(Math, rights),
 101+ Math.max.apply(Math, bottoms),
 102+ Math.min.apply(Math, lefts)
 103+ );
 104+ }
 105+
 106+ (function() {
 107+
 108+ // Test that <span> elements support getBoundingClientRect
 109+ var span = document.createElement("span");
 110+ var elementSupportsGetBoundingClientRect = util.isHostMethod(span, "getBoundingClientRect");
 111+ span = null;
 112+
 113+ // Test for getBoundingClientRect support in Range
 114+ var rangeSupportsGetClientRects = false, rangeSupportsGetBoundingClientRect = false;
 115+ if (api.features.implementsDomRange) {
 116+ var testRange = api.createNativeRange();
 117+ rangeSupportsGetClientRects = util.isHostMethod(testRange, "getClientRects");
 118+ rangeSupportsGetBoundingClientRect = util.isHostMethod(testRange, "getBoundingClientRect");
 119+ testRange.detach();
 120+ }
 121+
 122+ util.extend(api.features, {
 123+ rangeSupportsGetBoundingClientRect: rangeSupportsGetBoundingClientRect,
 124+ rangeSupportsGetClientRects: rangeSupportsGetClientRects,
 125+ elementSupportsGetBoundingClientRect: elementSupportsGetBoundingClientRect
 126+ });
 127+
 128+ var createClientBoundaryPosGetter = function(isStart) {
 129+ return function() {
 130+ var boundaryRange = this.cloneRange();
 131+ boundaryRange.collapse(isStart);
 132+ var rect = boundaryRange.getBoundingClientRect();
 133+ return { x: rect[isStart ? "left" : "right"], y: rect[isStart ? "top" : "bottom"] };
 134+ };
 135+ };
 136+
 137+ var rangeProto = api.rangePrototype;
 138+
 139+ if (api.features.implementsTextRange && elementSupportsGetBoundingClientRect) {
 140+ rangeProto.getBoundingClientRect = function() {
 141+ // We need a TextRange
 142+ var textRange = WrappedRange.rangeToTextRange(this);
 143+
 144+ // Work around table problems (table cell bounding rects seem not to count if TextRange spans cells)
 145+ var cells = this.getNodes([1], function(el) {
 146+ return /^t[dh]$/i.test(el.tagName);
 147+ });
 148+
 149+ // Merge rects for each cell selected by the range into overall rect
 150+ var rect, rects = [];
 151+ if (cells.length > 0) {
 152+ var lastTable = getAncestorElement(this.startContainer, "table");
 153+
 154+ for (var i = 0, cell, tempTextRange, table, subRange, subRect; cell = cells[i]; ++i) {
 155+ // Handle non-table sections of the range
 156+ table = getAncestorElement(cell, "table");
 157+ if (!lastTable || table != lastTable) {
 158+ // There is a section of the range prior to the current table, or lying between tables.
 159+ // Merge in its rect
 160+ subRange = this.cloneRange();
 161+ if (lastTable) {
 162+ subRange.setStartAfter(lastTable);
 163+ }
 164+ subRange.setEndBefore(table);
 165+ rects.push(WrappedRange.rangeToTextRange(subRange).getBoundingClientRect());
 166+ }
 167+
 168+ if (this.containsNode(cell)) {
 169+ rects.push(cell.getBoundingClientRect());
 170+ } else {
 171+ tempTextRange = textRange.duplicate();
 172+ tempTextRange.moveToElementText(cell);
 173+ if (tempTextRange.compareEndPoints("StartToStart", textRange) == -1) {
 174+ tempTextRange.setEndPoint("StartToStart", textRange);
 175+ } else if (tempTextRange.compareEndPoints("EndToEnd", textRange) == 1) {
 176+ tempTextRange.setEndPoint("EndToEnd", textRange);
 177+ }
 178+ rects.push(tempTextRange.getBoundingClientRect());
 179+ }
 180+ lastTable = table;
 181+ }
 182+
 183+ // Merge in the rect for any content lying after the final table
 184+ var endTable = getAncestorElement(this.endContainer, "table");
 185+ if (!endTable && lastTable) {
 186+ subRange = this.cloneRange();
 187+ subRange.setStartAfter(lastTable);
 188+ rects.push(WrappedRange.rangeToTextRange(subRange).getBoundingClientRect());
 189+ }
 190+ rect = mergeRects(rects);
 191+ } else {
 192+ rect = textRange.getBoundingClientRect();
 193+ }
 194+
 195+ return adjustClientRect(rect, dom.getDocument(this.startContainer));
 196+ };
 197+ } else if (api.features.implementsDomRange) {
 198+ var createWrappedRange = function(range) {
 199+ return (range instanceof WrappedRange) ? range : new WrappedRange(range);
 200+ };
 201+
 202+ if (rangeSupportsGetBoundingClientRect) {
 203+ rangeProto.getBoundingClientRect = function() {
 204+ var nativeRange = createWrappedRange(this).nativeRange;
 205+ // Test for WebKit getBoundingClientRect bug (https://bugs.webkit.org/show_bug.cgi?id=65324)
 206+ var rect = nativeRange.getBoundingClientRect() || nativeRange.getClientRects()[0];
 207+ return adjustClientRect(rect, dom.getDocument(this.startContainer));
 208+ };
 209+
 210+ if (rangeSupportsGetClientRects) {
 211+ createClientBoundaryPosGetter = function(isStart) {
 212+ return function() {
 213+ var rect, nativeRange = createWrappedRange(this).nativeRange;
 214+ if (isStart) {
 215+ rect = nativeRange.getClientRects()[0];
 216+ return { x: rect.left, y: rect.top };
 217+ } else {
 218+ var rects = nativeRange.getClientRects();
 219+ rect = rects[rects.length - 1];
 220+ return { x: rect.right, y: rect.bottom };
 221+ }
 222+ };
 223+ }
 224+ }
 225+ } else {
 226+ var getElementBoundingClientRect = elementSupportsGetBoundingClientRect ?
 227+ function(el) {
 228+ return adjustClientRect(el.getBoundingClientRect(), dom.getDocument(el));
 229+ } :
 230+
 231+ // This implementation is very naive. There are many browser quirks that make it extremely
 232+ // difficult to get accurate element coordinates in all situations
 233+ function(el) {
 234+ var x = 0, y = 0, offsetEl = el, width = el.offsetWidth, height = el.offsetHeight;
 235+ while (offsetEl) {
 236+ x += offsetEl.offsetLeft;
 237+ y += offsetEl.offsetTop;
 238+ offsetEl = offsetEl.offsetParent;
 239+ }
 240+
 241+ return adjustClientRect(new Rect(y, x + width, y + height, x), dom.getDocument(el));
 242+ };
 243+
 244+ var getRectFromBoundaries = function(range) {
 245+ var rect;
 246+ range.splitBoundaries();
 247+ var span = document.createElement("span");
 248+
 249+ if (range.collapsed) {
 250+ range.insertNode(span);
 251+ rect = getElementBoundingClientRect(span);
 252+ span.parentNode.removeChild(span);
 253+ } else {
 254+ // TODO: This isn't right. I'm not sure it can be made right sensibly. Consider what to do.
 255+ // This doesn't consider all the line boxes it needs to consider.
 256+ var workingRange = range.cloneRange();
 257+
 258+ // Get the start rectangle
 259+ workingRange.collapse(true);
 260+ workingRange.insertNode(span);
 261+ var startRect = getElementBoundingClientRect(span);
 262+ span.parentNode.removeChild(span);
 263+
 264+ // Get the end rectangle
 265+ workingRange.collapseToPoint(range.endContainer, range.endOffset);
 266+ workingRange.insertNode(span);
 267+ var endRect = getElementBoundingClientRect(span);
 268+ span.parentNode.removeChild(span);
 269+
 270+ // Merge the start and end rects
 271+ var rects = [startRect, endRect];
 272+
 273+ // Merge in rectangles for all elements in the range
 274+ var elements = range.getNodes([1], function(el) {
 275+ return range.containsNode(el);
 276+ });
 277+
 278+ for (var i = 0, len = elements.length; i < len; ++i) {
 279+ rects.push(getElementBoundingClientRect(elements[i]));
 280+ }
 281+ rect = mergeRects(rects)
 282+ }
 283+
 284+ // Clean up
 285+ range.normalizeBoundaries();
 286+ return rect;
 287+ };
 288+
 289+ rangeProto.getBoundingClientRect = function(range) {
 290+ return getRectFromBoundaries(createWrappedRange(range));
 291+ };
 292+ }
 293+
 294+ function createDocumentBoundaryPosGetter(isStart) {
 295+ return function() {
 296+ var pos = this["get" + (isStart ? "Start" : "End") + "ClientPos"]();
 297+ var scrollPos = getScrollPosition( dom.getWindow(this.startContainer) );
 298+ return { x: pos.x + scrollPos.x, y: pos.y + scrollPos.y };
 299+ };
 300+ }
 301+ }
 302+
 303+ util.extend(rangeProto, {
 304+ getBoundingDocumentRect: function() {
 305+ var scrollPos = getScrollPosition( dom.getWindow(this.startContainer) );
 306+ return createRelativeRect(this.getBoundingClientRect(), scrollPos.x, scrollPos.y);
 307+ },
 308+
 309+ getStartClientPos: createClientBoundaryPosGetter(true),
 310+ getEndClientPos: createClientBoundaryPosGetter(false),
 311+
 312+ getStartDocumentPos: createDocumentBoundaryPosGetter(true),
 313+ getEndDocumentPos: createDocumentBoundaryPosGetter(false)
 314+ });
 315+ })();
 316+
 317+ // Add Selection methods
 318+ (function() {
 319+ function compareRanges(r1, r2) {
 320+ return r1.compareBoundaryPoints(r2.START_TO_START, r2);
 321+ }
 322+
 323+ function createSelectionRectGetter(isDocument) {
 324+ return function() {
 325+ var rangeMethodName = "getBounding" + (isDocument ? "Document" : "Client") + "Rect";
 326+ var rects = [];
 327+ for (var i = 0, rect = null, rangeRect; i < this.rangeCount; ++i) {
 328+ rects.push(this.getRangeAt(i)[rangeMethodName]());
 329+ }
 330+ return mergeRects(rects);
 331+ };
 332+ }
 333+
 334+ function createSelectionBoundaryPosGetter(isStart, isDocument) {
 335+ return function() {
 336+ if (this.rangeCount == 0) {
 337+ return null;
 338+ }
 339+
 340+ var posType = isDocument ? "Document" : "Client";
 341+
 342+ var ranges = this.getAllRanges();
 343+ if (ranges.length > 1) {
 344+ // Order the ranges by position within the DOM
 345+ ranges.sort(compareRanges);
 346+ }
 347+
 348+ return isStart ?
 349+ ranges[0]["getStart" + posType + "Pos"]() :
 350+ ranges[ranges.length - 1]["getEnd" + posType + "Pos"]();
 351+ };
 352+ }
 353+
 354+ util.extend(api.selectionPrototype, {
 355+ getBoundingClientRect: createSelectionRectGetter(false),
 356+ getBoundingDocumentRect: createSelectionRectGetter(true),
 357+
 358+ getStartClientPos: createSelectionBoundaryPosGetter(true, false),
 359+ getEndClientPos: createSelectionBoundaryPosGetter(false, false),
 360+
 361+ getStartDocumentPos: createSelectionBoundaryPosGetter(true, true),
 362+ getEndDocumentPos: createSelectionBoundaryPosGetter(false, true)
 363+ });
 364+ })();
 365+});
Index: trunk/extensions/VisualEditor/modules/ve/ce/ve.es.Surface.js
@@ -58,6 +58,64 @@
5959 }
6060 };
6161
 62+ve.es.Surface.prototype.getOffset = function( localNode, localOffset ) {
 63+ var $node = $( localNode );
 64+ while( !$node.hasClass( 'ce-leafNode' ) ) {
 65+ $node = $node.parent();
 66+ }
 67+
 68+ var current = [$node.contents(), 0];
 69+ var stack = [current];
 70+
 71+ var offset = 0;
 72+
 73+ while ( stack.length > 0 ) {
 74+ if ( current[1] >= current[0].length ) {
 75+ stack.pop();
 76+ current = stack[ stack.length - 1 ];
 77+ continue;
 78+ }
 79+ var item = current[0][current[1]];
 80+ var $item = current[0].eq( current[1] );
 81+
 82+ if ( item.nodeType === 3 ) {
 83+ if ( item === localNode ) {
 84+ offset += localOffset;
 85+ break;
 86+ } else {
 87+ offset += item.textContent.length;
 88+ }
 89+ } else if ( item.nodeType === 1 ) {
 90+ if ( $( item ).attr('contentEditable') === "false" ) {
 91+ offset += 1;
 92+ } else {
 93+ stack.push( [$item.contents(), 0] );
 94+ current[1]++;
 95+ current = stack[stack.length-1];
 96+ continue;
 97+ }
 98+ }
 99+ current[1]++;
 100+ }
 101+
 102+ return this.documentView.getOffsetFromNode( $node.data('view') ) + 1 + offset;
 103+}
 104+
 105+ve.es.Surface.prototype.getSelection = function() {
 106+ var selection = rangy.getSelection();
 107+
 108+ if ( selection.anchorNode === selection.focusNode && selection.anchorOffset === selection.focusOffset ) {
 109+ // only one offset
 110+ var offset = this.getOffset( selection.anchorNode, selection.anchorOffset );
 111+ return new ve.Range( offset, offset );
 112+ } else {
 113+ // two offsets
 114+ var offset1 = this.getOffset( selection.anchorNode, selection.anchorOffset );
 115+ var offset2 = this.getOffset( selection.focusNode, selection.focusOffset );
 116+ return new ve.Range( offset1, offset2 );
 117+ }
 118+};
 119+
62120 /* Inheritance */
63121
64122 ve.extendClass( ve.es.Surface, ve.EventEmitter );

Status & tagging log