Index: trunk/extensions/VisualEditor/demos/ce/index.php |
— | — | @@ -79,6 +79,10 @@ |
80 | 80 | include( '../../modules/sandbox/base.php' ); |
81 | 81 | |
82 | 82 | ?> |
| 83 | + <!-- Rangy --> |
| 84 | + <script src="../../modules/rangy/rangy-core.js"></script> |
| 85 | + <script src="../../modules/rangy/rangy-position.js"></script> |
| 86 | + |
83 | 87 | <!-- ve --> |
84 | 88 | <script src="../../modules/jquery/jquery.js"></script> |
85 | 89 | <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, "<").replace(/>/g, ">");
|
| 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 @@ |
59 | 59 | } |
60 | 60 | }; |
61 | 61 | |
| 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 | + |
62 | 120 | /* Inheritance */ |
63 | 121 | |
64 | 122 | ve.extendClass( ve.es.Surface, ve.EventEmitter ); |