1/** 2 * @license wysihtml5 v0.3.0 3 * https://github.com/xing/wysihtml5 4 * 5 * Author: Christopher Blum (https://github.com/tiff) 6 * 7 * Copyright (C) 2012 XING AG 8 * Licensed under the MIT license (MIT) 9 * 10 */ 11var wysihtml5 = { 12 version: "0.3.0", 13 14 // namespaces 15 commands: {}, 16 dom: {}, 17 quirks: {}, 18 toolbar: {}, 19 lang: {}, 20 selection: {}, 21 views: {}, 22 23 INVISIBLE_SPACE: "\uFEFF", 24 25 EMPTY_FUNCTION: function() {}, 26 27 ELEMENT_NODE: 1, 28 TEXT_NODE: 3, 29 30 BACKSPACE_KEY: 8, 31 ENTER_KEY: 13, 32 ESCAPE_KEY: 27, 33 SPACE_KEY: 32, 34 DELETE_KEY: 46 35};/** 36 * @license Rangy, a cross-browser JavaScript range and selection library 37 * http://code.google.com/p/rangy/ 38 * 39 * Copyright 2011, Tim Down 40 * Licensed under the MIT license. 41 * Version: 1.2.2 42 * Build date: 13 November 2011 43 */ 44window['rangy'] = (function() { 45 46 47 var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; 48 49 var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", 50 "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"]; 51 52 var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore", 53 "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
54 "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"]; 55 56 var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; 57 58 // Subset of TextRange's full set of methods that we're interested in 59 var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark", 60 "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"]; 61 62 /*----------------------------------------------------------------------------------------------------------------*/ 63 64 // Trio of functions taken from Peter Michaux's article: 65 // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting 66 function isHostMethod(o, p) { 67 var t = typeof o[p]; 68 return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown"; 69 } 70 71 function isHostObject(o, p) { 72 return !!(typeof o[p] == OBJECT && o[p]); 73 } 74 75 function isHostProperty(o, p) { 76 return typeof o[p] != UNDEFINED; 77 } 78 79 // Creates a convenience function to save verbose repeated calls to tests functions 80 function createMultiplePropertyTest(testFunc) { 81 return function(o, props) { 82 var i = props.length; 83 while (i--) { 84 if (!testFunc(o, props[i])) { 85 return false; 86 } 87 } 88 return true; 89 }; 90 } 91 92 // Next trio of functions are a convenience to save verbose repeated calls to previous two functions 93 var areHostMethods = createMultiplePropertyTest(isHostMethod); 94 var areHostObjects = createMultiplePropertyTest(isHostObject); 95 var areHostProperties = createMultiplePropertyTest(isHostProperty); 96 97 function isTextRange(range) { 98 return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); 99 } 100 101 var api = { 102 version: "1.2.2", 103 initialized: false, 104 supported: true, 105 106 util: { 107 isHostMethod: isHostMethod, 108 isHostObject: isHostObject, 109 isHostProperty: isHostProperty, 110 areHostMethods: areHostMethods, 111 areHostObjects: areHostObjects, 112 areHostProperties: areHostProperties, 113 isTextRange: isTextRange 114 }, 115 116 features: {}, 117 118 modules: {}, 119 config: { 120 alertOnWarn: false, 121 preferTextRange: false 122 } 123 }; 124 125 function fail(reason) { 126 window.alert("Rangy not supported in your browser. Reason: " + reason); 127 api.initialized = true; 128 api.supported = false; 129 } 130 131 api.fail = fail; 132 133 function warn(msg) { 134 var warningMessage = "Rangy warning: " + msg; 135 if (api.config.alertOnWarn) { 136 window.alert(warningMessage); 137 } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) { 138 window.console.log(warningMessage); 139 } 140 } 141 142 api.warn = warn; 143 144 if ({}.hasOwnProperty) { 145 api.util.extend = function(o, props) { 146 for (var i in props) { 147 if (props.hasOwnProperty(i)) { 148 o[i] = props[i]; 149 } 150 } 151 }; 152 } else { 153 fail("hasOwnProperty not supported"); 154 } 155 156 var initListeners = []; 157 var moduleInitializers = []; 158 159 // Initialization 160 function init() { 161 if (api.initialized) { 162 return; 163 } 164 var testRange; 165 var implementsDomRange = false, implementsTextRange = false; 166 167 // First, perform basic feature tests 168 169 if (isHostMethod(document, "createRange")) { 170 testRange = document.createRange(); 171 if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { 172 implementsDomRange = true; 173 } 174 testRange.detach(); 175 } 176 177 var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0]; 178 179 if (body && isHostMethod(body, "createTextRange")) { 180 testRange = body.createTextRange(); 181 if (isTextRange(testRange)) { 182 implementsTextRange = true; 183 } 184 } 185 186 if (!implementsDomRange && !implementsTextRange) { 187 fail("Neither Range nor TextRange are implemented"); 188 } 189 190 api.initialized = true; 191 api.features = { 192 implementsDomRange: implementsDomRange, 193 implementsTextRange: implementsTextRange 194 }; 195 196 // Initialize modules and call init listeners
197 var allListeners = moduleInitializers.concat(initListeners); 198 for (var i = 0, len = allListeners.length; i < len; ++i) { 199 try { 200 allListeners[i](api); 201 } catch (ex) { 202 if (isHostObject(window, "console") && isHostMethod(window.console, "log")) { 203 window.console.log("Init listener threw an exception. Continuing.", ex); 204 } 205 206 } 207 } 208 } 209 210 // Allow external scripts to initialize this library in case it's loaded after the document has loaded 211 api.init = init; 212 213 // Execute listener immediately if already initialized 214 api.addInitListener = function(listener) { 215 if (api.initialized) { 216 listener(api); 217 } else { 218 initListeners.push(listener); 219 } 220 }; 221 222 var createMissingNativeApiListeners = []; 223 224 api.addCreateMissingNativeApiListener = function(listener) { 225 createMissingNativeApiListeners.push(listener); 226 }; 227 228 function createMissingNativeApi(win) { 229 win = win || window; 230 init(); 231 232 // Notify listeners 233 for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) { 234 createMissingNativeApiListeners[i](win); 235 } 236 } 237 238 api.createMissingNativeApi = createMissingNativeApi; 239 240 /** 241 * @constructor 242 */ 243 function Module(name) { 244 this.name = name; 245 this.initialized = false; 246 this.supported = false; 247 } 248 249 Module.prototype.fail = function(reason) { 250 this.initialized = true; 251 this.supported = false; 252 253 throw new Error("Module '" + this.name + "' failed to load: " + reason); 254 }; 255 256 Module.prototype.warn = function(msg) { 257 api.warn("Module " + this.name + ": " + msg); 258 }; 259 260 Module.prototype.createError = function(msg) { 261 return new Error("Error in Rangy " + this.name + " module: " + msg); 262 }; 263 264 api.createModule = function(name, initFunc) { 265 var module = new Module(name); 266 api.modules[name] = module; 267 268 moduleInitializers.push(function(api) { 269 initFunc(api, module); 270 module.initialized = true; 271 module.supported = true; 272 }); 273 }; 274 275 api.requireModules = function(modules) { 276 for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) { 277 moduleName = modules[i]; 278 module = api.modules[moduleName]; 279 if (!module || !(module instanceof Module)) { 280 throw new Error("Module '" + moduleName + "' not found"); 281 } 282 if (!module.supported) { 283 throw new Error("Module '" + moduleName + "' not supported"); 284 } 285 } 286 }; 287 288 /*----------------------------------------------------------------------------------------------------------------*/ 289 290 // Wait for document to load before running tests 291 292 var docReady = false; 293 294 var loadHandler = function(e) { 295 296 if (!docReady) { 297 docReady = true; 298 if (!api.initialized) { 299 init(); 300 } 301 } 302 }; 303 304 // Test whether we have window and document objects that we will need 305 if (typeof window == UNDEFINED) { 306 fail("No window found"); 307 return; 308 } 309 if (typeof document == UNDEFINED) { 310 fail("No document found"); 311 return; 312 } 313 314 if (isHostMethod(document, "addEventListener")) { 315 document.addEventListener("DOMContentLoaded", loadHandler, false); 316 } 317 318 // Add a fallback in case the DOMContentLoaded event isn't supported 319 if (isHostMethod(window, "addEventListener")) { 320 window.addEventListener("load", loadHandler, false); 321 } else if (isHostMethod(window, "attachEvent")) { 322 window.attachEvent("onload", loadHandler); 323 } else { 324 fail("Window does not have required addEventListener or attachEvent method"); 325 } 326 327 return api; 328})(); 329rangy.createModule("DomUtil", function(api, module) { 330 331 var UNDEF = "undefined"; 332 var util = api.util; 333 334 // Perform feature tests 335 if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { 336 module.fail("document missing a Node creation method"); 337 } 338 339 if (!util.isHostMethod(document, "getElementsByTagName")) { 340 module.fail("document missing getElementsByTagName method"); 341 } 342 343 var el = document.createElement("div"); 344 if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || 345 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { 346 module.fail("Incomplete Element implementation"); 347 } 348 349 // innerHTML is required for Range's createContextualFragment method 350 if (!util.isHostProperty(el, "innerHTML")) { 351 module.fail("Element is missing innerHTML property"); 352 } 353 354 var textNode = document.createTextNode("test"); 355 if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] || 356 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) || 357 !util.areHostProperties(textNode, ["data"]))) { 358 module.fail("Incomplete Text Node implementation"); 359 } 360 361 /*----------------------------------------------------------------------------------------------------------------*/ 362 363 // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been 364 // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
365 // contains just the document as a single element and the value searched for is the document. 366 var arrayContains = /*Array.prototype.indexOf ? 367 function(arr, val) { 368 return arr.indexOf(val) > -1; 369 }:*/ 370 371 function(arr, val) { 372 var i = arr.length; 373 while (i--) { 374 if (arr[i] === val) { 375 return true; 376 } 377 } 378 return false; 379 }; 380 381 // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI 382 function isHtmlNamespace(node) { 383 var ns; 384 return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"); 385 } 386 387 function parentElement(node) { 388 var parent = node.parentNode; 389 return (parent.nodeType == 1) ? parent : null; 390 } 391 392 function getNodeIndex(node) { 393 var i = 0; 394 while( (node = node.previousSibling) ) { 395 i++; 396 } 397 return i; 398 } 399 400 function getNodeLength(node) { 401 var childNodes; 402 return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0); 403 } 404 405 function getCommonAncestor(node1, node2) { 406 var ancestors = [], n; 407 for (n = node1; n; n = n.parentNode) { 408 ancestors.push(n); 409 } 410 411 for (n = node2; n; n = n.parentNode) { 412 if (arrayContains(ancestors, n)) { 413 return n; 414 } 415 } 416 417 return null; 418 } 419 420 function isAncestorOf(ancestor, descendant, selfIsAncestor) { 421 var n = selfIsAncestor ? descendant : descendant.parentNode; 422 while (n) { 423 if (n === ancestor) { 424 return true; 425 } else { 426 n = n.parentNode; 427 } 428 } 429 return false; 430 } 431 432 function getClosestAncestorIn(node, ancestor, selfIsAncestor) { 433 var p, n = selfIsAncestor ? node : node.parentNode; 434 while (n) { 435 p = n.parentNode; 436 if (p === ancestor) { 437 return n; 438 } 439 n = p; 440 } 441 return null; 442 } 443 444 function isCharacterDataNode(node) { 445 var t = node.nodeType; 446 return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment 447 } 448 449 function insertAfter(node, precedingNode) { 450 var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; 451 if (nextNode) { 452 parent.insertBefore(node, nextNode); 453 } else { 454 parent.appendChild(node); 455 } 456 return node; 457 } 458 459 // Note that we cannot use splitText() because it is bugridden in IE 9. 460 function splitDataNode(node, index) { 461 var newNode = node.cloneNode(false); 462 newNode.deleteData(0, index); 463 node.deleteData(index, node.length - index); 464 insertAfter(newNode, node); 465 return newNode; 466 } 467 468 function getDocument(node) { 469 if (node.nodeType == 9) { 470 return node; 471 } else if (typeof node.ownerDocument != UNDEF) { 472 return node.ownerDocument; 473 } else if (typeof node.document != UNDEF) { 474 return node.document; 475 } else if (node.parentNode) { 476 return getDocument(node.parentNode); 477 } else { 478 throw new Error("getDocument: no document found for node"); 479 } 480 } 481 482 function getWindow(node) { 483 var doc = getDocument(node); 484 if (typeof doc.defaultView != UNDEF) { 485 return doc.defaultView; 486 } else if (typeof doc.parentWindow != UNDEF) { 487 return doc.parentWindow; 488 } else { 489 throw new Error("Cannot get a window object for node"); 490 } 491 } 492 493 function getIframeDocument(iframeEl) { 494 if (typeof iframeEl.contentDocument != UNDEF) { 495 return iframeEl.contentDocument; 496 } else if (typeof iframeEl.contentWindow != UNDEF) { 497 return iframeEl.contentWindow.document; 498 } else {
499 throw new Error("getIframeWindow: No Document object found for iframe element"); 500 } 501 } 502 503 function getIframeWindow(iframeEl) { 504 if (typeof iframeEl.contentWindow != UNDEF) { 505 return iframeEl.contentWindow; 506 } else if (typeof iframeEl.contentDocument != UNDEF) { 507 return iframeEl.contentDocument.defaultView; 508 } else { 509 throw new Error("getIframeWindow: No Window object found for iframe element"); 510 } 511 } 512 513 function getBody(doc) { 514 return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; 515 } 516 517 function getRootContainer(node) { 518 var parent; 519 while ( (parent = node.parentNode) ) { 520 node = parent; 521 } 522 return node; 523 } 524 525 function comparePoints(nodeA, offsetA, nodeB, offsetB) { 526 // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing 527 var nodeC, root, childA, childB, n; 528 if (nodeA == nodeB) { 529 530 // Case 1: nodes are the same 531 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; 532 } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { 533 534 // Case 2: node C (container B or an ancestor) is a child node of A 535 return offsetA <= getNodeIndex(nodeC) ? -1 : 1; 536 } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) { 537 538 // Case 3: node C (container A or an ancestor) is a child node of B 539 return getNodeIndex(nodeC) < offsetB ? -1 : 1; 540 } else { 541 542 // Case 4: containers are siblings or descendants of siblings 543 root = getCommonAncestor(nodeA, nodeB); 544 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); 545 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); 546 547 if (childA === childB) { 548 // This shouldn't be possible 549 550 throw new Error("comparePoints got to case 4 and childA and childB are the same!"); 551 } else { 552 n = root.firstChild; 553 while (n) { 554 if (n === childA) { 555 return -1; 556 } else if (n === childB) { 557 return 1; 558 } 559 n = n.nextSibling; 560 } 561 throw new Error("Should not be here!"); 562 } 563 } 564 } 565 566 function fragmentFromNodeChildren(node) { 567 var fragment = getDocument(node).createDocumentFragment(), child; 568 while ( (child = node.firstChild) ) { 569 fragment.appendChild(child); 570 } 571 return fragment; 572 } 573 574 function inspectNode(node) { 575 if (!node) { 576 return "[No node]"; 577 } 578 if (isCharacterDataNode(node)) { 579 return '"' + node.data + '"'; 580 } else if (node.nodeType == 1) { 581 var idAttr = node.id ? ' id="' + node.id + '"' : ""; 582 return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]"; 583 } else { 584 return node.nodeName; 585 } 586 } 587 588 /** 589 * @constructor 590 */ 591 function NodeIterator(root) { 592 this.root = root; 593 this._next = root; 594 } 595 596 NodeIterator.prototype = { 597 _current: null, 598 599 hasNext: function() { 600 return !!this._next; 601 }, 602 603 next: function() { 604 var n = this._current = this._next; 605 var child, next; 606 if (this._current) { 607 child = n.firstChild; 608 if (child) { 609 this._next = child; 610 } else { 611 next = null; 612 while ((n !== this.root) && !(next = n.nextSibling)) { 613 n = n.parentNode; 614 } 615 this._next = next; 616 } 617 } 618 return this._current; 619 }, 620 621 detach: function() { 622 this._current = this._next = this.root = null; 623 } 624 }; 625 626 function createIterator(root) { 627 return new NodeIterator(root); 628 } 629 630 /** 631 * @constructor 632 */ 633 function DomPosition(node, offset) { 634 this.node = node; 635 this.offset = offset; 636 } 637 638 DomPosition.prototype = { 639 equals: function(pos) { 640 return this.node === pos.node & this.offset == pos.offset; 641 }, 642 643 inspect: function() { 644 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; 645 } 646 }; 647 648 /** 649 * @constructor 650 */ 651 function DOMException(codeName) { 652 this.code = this[codeName]; 653 this.codeName = codeName;
654 this.message = "DOMException: " + this.codeName; 655 } 656 657 DOMException.prototype = { 658 INDEX_SIZE_ERR: 1, 659 HIERARCHY_REQUEST_ERR: 3, 660 WRONG_DOCUMENT_ERR: 4, 661 NO_MODIFICATION_ALLOWED_ERR: 7, 662 NOT_FOUND_ERR: 8, 663 NOT_SUPPORTED_ERR: 9, 664 INVALID_STATE_ERR: 11 665 }; 666 667 DOMException.prototype.toString = function() { 668 return this.message; 669 }; 670 671 api.dom = { 672 arrayContains: arrayContains, 673 isHtmlNamespace: isHtmlNamespace, 674 parentElement: parentElement, 675 getNodeIndex: getNodeIndex, 676 getNodeLength: getNodeLength, 677 getCommonAncestor: getCommonAncestor, 678 isAncestorOf: isAncestorOf, 679 getClosestAncestorIn: getClosestAncestorIn, 680 isCharacterDataNode: isCharacterDataNode, 681 insertAfter: insertAfter, 682 splitDataNode: splitDataNode, 683 getDocument: getDocument, 684 getWindow: getWindow, 685 getIframeWindow: getIframeWindow, 686 getIframeDocument: getIframeDocument, 687 getBody: getBody, 688 getRootContainer: getRootContainer, 689 comparePoints: comparePoints, 690 inspectNode: inspectNode, 691 fragmentFromNodeChildren: fragmentFromNodeChildren, 692 createIterator: createIterator, 693 DomPosition: DomPosition 694 }; 695 696 api.DOMException = DOMException; 697});rangy.createModule("DomRange", function(api, module) { 698 api.requireModules( ["DomUtil"] ); 699 700 701 var dom = api.dom; 702 var DomPosition = dom.DomPosition; 703 var DOMException = api.DOMException; 704 705 /*----------------------------------------------------------------------------------------------------------------*/ 706 707 // Utility functions 708 709 function isNonTextPartiallySelected(node, range) { 710 return (node.nodeType != 3) && 711 (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true)); 712 } 713 714 function getRangeDocument(range) { 715 return dom.getDocument(range.startContainer); 716 } 717 718 function dispatchEvent(range, type, args) { 719 var listeners = range._listeners[type]; 720 if (listeners) { 721 for (var i = 0, len = listeners.length; i < len; ++i) { 722 listeners[i].call(range, {target: range, args: args}); 723 } 724 } 725 } 726 727 function getBoundaryBeforeNode(node) { 728 return new DomPosition(node.parentNode, dom.getNodeIndex(node)); 729 } 730 731 function getBoundaryAfterNode(node) { 732 return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1); 733 } 734 735 function insertNodeAtPosition(node, n, o) { 736 var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; 737 if (dom.isCharacterDataNode(n)) { 738 if (o == n.length) { 739 dom.insertAfter(node, n); 740 } else { 741 n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o)); 742 } 743 } else if (o >= n.childNodes.length) { 744 n.appendChild(node); 745 } else { 746 n.insertBefore(node, n.childNodes[o]); 747 } 748 return firstNodeInserted; 749 } 750 751 function cloneSubtree(iterator) { 752 var partiallySelected; 753 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { 754 partiallySelected = iterator.isPartiallySelectedSubtree(); 755 756 node = node.cloneNode(!partiallySelected); 757 if (partiallySelected) { 758 subIterator = iterator.getSubtreeIterator(); 759 node.appendChild(cloneSubtree(subIterator)); 760 subIterator.detach(true); 761 } 762 763 if (node.nodeType == 10) { // DocumentType 764 throw new DOMException("HIERARCHY_REQUEST_ERR"); 765 } 766 frag.appendChild(node); 767 } 768 return frag; 769 } 770 771 function iterateSubtree(rangeIterator, func, iteratorState) { 772 var it, n; 773 iteratorState = iteratorState || { stop: false }; 774 for (var node, subRangeIterator; node = rangeIterator.next(); ) { 775 //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
776 if (rangeIterator.isPartiallySelectedSubtree()) { 777 // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the 778 // node selected by the Range. 779 if (func(node) === false) { 780 iteratorState.stop = true; 781 return; 782 } else { 783 subRangeIterator = rangeIterator.getSubtreeIterator(); 784 iterateSubtree(subRangeIterator, func, iteratorState); 785 subRangeIterator.detach(true); 786 if (iteratorState.stop) { 787 return; 788 } 789 } 790 } else { 791 // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its 792 // descendant 793 it = dom.createIterator(node); 794 while ( (n = it.next()) ) { 795 if (func(n) === false) { 796 iteratorState.stop = true; 797 return; 798 } 799 } 800 } 801 } 802 } 803 804 function deleteSubtree(iterator) { 805 var subIterator; 806 while (iterator.next()) { 807 if (iterator.isPartiallySelectedSubtree()) { 808 subIterator = iterator.getSubtreeIterator(); 809 deleteSubtree(subIterator); 810 subIterator.detach(true); 811 } else { 812 iterator.remove(); 813 } 814 } 815 } 816 817 function extractSubtree(iterator) { 818 819 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { 820 821 822 if (iterator.isPartiallySelectedSubtree()) { 823 node = node.cloneNode(false); 824 subIterator = iterator.getSubtreeIterator(); 825 node.appendChild(extractSubtree(subIterator)); 826 subIterator.detach(true); 827 } else { 828 iterator.remove(); 829 } 830 if (node.nodeType == 10) { // DocumentType 831 throw new DOMException("HIERARCHY_REQUEST_ERR"); 832 } 833 frag.appendChild(node); 834 } 835 return frag; 836 } 837 838 function getNodesInRange(range, nodeTypes, filter) { 839 //log.info("getNodesInRange, " + nodeTypes.join(",")); 840 var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; 841 var filterExists = !!filter; 842 if (filterNodeTypes) { 843 regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); 844 } 845 846 var nodes = []; 847 iterateSubtree(new RangeIterator(range, false), function(node) { 848 if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) { 849 nodes.push(node); 850 } 851 }); 852 return nodes; 853 } 854 855 function inspect(range) { 856 var name = (typeof range.getName == "undefined") ? "Range" : range.getName(); 857 return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + 858 dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; 859 } 860 861 /*----------------------------------------------------------------------------------------------------------------*/ 862 863 // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) 864 865 /** 866 * @constructor 867 */ 868 function RangeIterator(range, clonePartiallySelectedTextNodes) { 869 this.range = range; 870 this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; 871 872 873 874 if (!range.collapsed) { 875 this.sc = range.startContainer; 876 this.so = range.startOffset; 877 this.ec = range.endContainer; 878 this.eo = range.endOffset; 879 var root = range.commonAncestorContainer; 880 881 if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) { 882 this.isSingleCharacterDataNode = true; 883 this._first = this._last = this._next = this.sc; 884 } else { 885 this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ? 886 this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true); 887 this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ? 888 this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true); 889 } 890 891 } 892 } 893 894 RangeIterator.prototype = { 895 _current: null, 896 _next: null, 897 _first: null, 898 _last: null, 899 isSingleCharacterDataNode: false, 900 901 reset: function() { 902 this._current = null; 903 this._next = this._first; 904 }, 905 906 hasNext: function() { 907 return !!this._next; 908 }, 909 910 next: function() { 911 // Move to next node 912 var current = this._current = this._next; 913 if (current) { 914 this._next = (current !== this._last) ? current.nextSibling : null; 915 916 // Check for partially selected text nodes 917 if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { 918 if (current === this.ec) { 919 920 (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); 921 } 922 if (this._current === this.sc) { 923 924 (current = current.cloneNode(true)).deleteData(0, this.so); 925 } 926 } 927 } 928 929 return current; 930 }, 931 932 remove: function() { 933 var current = this._current, start, end; 934 935 if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { 936 start = (current === this.sc) ? this.so : 0; 937 end = (current === this.ec) ? this.eo : current.length; 938 if (start != end) {
939 current.deleteData(start, end - start); 940 } 941 } else { 942 if (current.parentNode) { 943 current.parentNode.removeChild(current); 944 } else { 945 946 } 947 } 948 }, 949 950 // Checks if the current node is partially selected 951 isPartiallySelectedSubtree: function() { 952 var current = this._current; 953 return isNonTextPartiallySelected(current, this.range); 954 }, 955 956 getSubtreeIterator: function() { 957 var subRange; 958 if (this.isSingleCharacterDataNode) { 959 subRange = this.range.cloneRange(); 960 subRange.collapse(); 961 } else { 962 subRange = new Range(getRangeDocument(this.range)); 963 var current = this._current; 964 var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current); 965 966 if (dom.isAncestorOf(current, this.sc, true)) { 967 startContainer = this.sc; 968 startOffset = this.so; 969 } 970 if (dom.isAncestorOf(current, this.ec, true)) { 971 endContainer = this.ec; 972 endOffset = this.eo; 973 } 974 975 updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); 976 } 977 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); 978 }, 979 980 detach: function(detachRange) { 981 if (detachRange) { 982 this.range.detach(); 983 } 984 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; 985 } 986 }; 987 988 /*----------------------------------------------------------------------------------------------------------------*/ 989 990 // Exceptions 991 992 /** 993 * @constructor 994 */ 995 function RangeException(codeName) { 996 this.code = this[codeName]; 997 this.codeName = codeName; 998 this.message = "RangeException: " + this.codeName; 999 } 1000 1001 RangeException.prototype = { 1002 BAD_BOUNDARYPOINTS_ERR: 1,
1003 INVALID_NODE_TYPE_ERR: 2 1004 }; 1005 1006 RangeException.prototype.toString = function() { 1007 return this.message; 1008 }; 1009 1010 /*----------------------------------------------------------------------------------------------------------------*/ 1011 1012 /** 1013 * Currently iterates through all nodes in the range on creation until I think of a decent way to do it 1014 * TODO: Look into making this a proper iterator, not requiring preloading everything first 1015 * @constructor 1016 */ 1017 function RangeNodeIterator(range, nodeTypes, filter) { 1018 this.nodes = getNodesInRange(range, nodeTypes, filter); 1019 this._next = this.nodes[0]; 1020 this._position = 0; 1021 } 1022 1023 RangeNodeIterator.prototype = { 1024 _current: null, 1025 1026 hasNext: function() { 1027 return !!this._next; 1028 }, 1029 1030 next: function() { 1031 this._current = this._next; 1032 this._next = this.nodes[ ++this._position ]; 1033 return this._current; 1034 }, 1035 1036 detach: function() { 1037 this._current = this._next = this.nodes = null; 1038 } 1039 }; 1040 1041 var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; 1042 var rootContainerNodeTypes = [2, 9, 11]; 1043 var readonlyNodeTypes = [5, 6, 10, 12]; 1044 var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; 1045 var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; 1046 1047 function createAncestorFinder(nodeTypes) { 1048 return function(node, selfIsAncestor) { 1049 var t, n = selfIsAncestor ? node : node.parentNode; 1050 while (n) { 1051 t = n.nodeType; 1052 if (dom.arrayContains(nodeTypes, t)) { 1053 return n; 1054 } 1055 n = n.parentNode; 1056 } 1057 return null; 1058 }; 1059 } 1060 1061 var getRootContainer = dom.getRootContainer; 1062 var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); 1063 var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); 1064 var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); 1065 1066 function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { 1067 if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1068 throw new RangeException("INVALID_NODE_TYPE_ERR"); 1069 } 1070 } 1071 1072 function assertNotDetached(range) { 1073 if (!range.startContainer) { 1074 throw new DOMException("INVALID_STATE_ERR"); 1075 } 1076 } 1077 1078 function assertValidNodeType(node, invalidTypes) { 1079 if (!dom.arrayContains(invalidTypes, node.nodeType)) { 1080 throw new RangeException("INVALID_NODE_TYPE_ERR"); 1081 } 1082 } 1083 1084 function assertValidOffset(node, offset) { 1085 if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) { 1086 throw new DOMException("INDEX_SIZE_ERR"); 1087 } 1088 } 1089 1090 function assertSameDocumentOrFragment(node1, node2) { 1091 if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { 1092 throw new DOMException("WRONG_DOCUMENT_ERR"); 1093 } 1094 } 1095 1096 function assertNodeNotReadOnly(node) { 1097 if (getReadonlyAncestor(node, true)) { 1098 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); 1099 } 1100 } 1101 1102 function assertNode(node, codeName) { 1103 if (!node) { 1104 throw new DOMException(codeName); 1105 } 1106 } 1107 1108 function isOrphan(node) { 1109 return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true); 1110 } 1111 1112 function isValidOffset(node, offset) { 1113 return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length); 1114 } 1115 1116 function assertRangeValid(range) { 1117 assertNotDetached(range); 1118 if (isOrphan(range.startContainer) || isOrphan(range.endContainer) || 1119 !isValidOffset(range.startContainer, range.startOffset) || 1120 !isValidOffset(range.endContainer, range.endOffset)) { 1121 throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")"); 1122 } 1123 } 1124 1125 /*----------------------------------------------------------------------------------------------------------------*/ 1126 1127 // Test the browser's innerHTML support to decide how to implement createContextualFragment 1128 var styleEl = document.createElement("style"); 1129 var htmlParsingConforms = false; 1130 try { 1131 styleEl.innerHTML = "<b>x</b>"; 1132 htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node 1133 } catch (e) { 1134 // IE 6 and 7 throw 1135 } 1136 1137 api.features.htmlParsingConforms = htmlParsingConforms; 1138 1139 var createContextualFragment = htmlParsingConforms ? 1140 1141 // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See 1142 // discussion and base code for this implementation at issue 67. 1143 // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface 1144 // Thanks to Aleks Williams. 1145 function(fragmentStr) { 1146 // "Let node the context object's start's node." 1147 var node = this.startContainer; 1148 var doc = dom.getDocument(node); 1149 1150 // "If the context object's start's node is null, raise an INVALID_STATE_ERR 1151 // exception and abort these steps." 1152 if (!node) { 1153 throw new DOMException("INVALID_STATE_ERR"); 1154 } 1155 1156 // "Let element be as follows, depending on node's interface:" 1157 // Document, Document Fragment: null 1158 var el = null; 1159 1160 // "Element: node" 1161 if (node.nodeType == 1) { 1162 el = node; 1163 1164 // "Text, Comment: node's parentElement" 1165 } else if (dom.isCharacterDataNode(node)) { 1166 el = dom.parentElement(node); 1167 } 1168 1169 // "If either element is null or element's ownerDocument is an HTML document 1170 // and element's local name is "html" and element's namespace is the HTML 1171 // namespace" 1172 if (el === null || ( 1173 el.nodeName == "HTML" 1174 && dom.isHtmlNamespace(dom.getDocument(el).documentElement) 1175 && dom.isHtmlNamespace(el) 1176 )) { 1177 1178 // "let element be a new Element with "body" as its local name and the HTML 1179 // namespace as its namespace."" 1180 el = doc.createElement("body"); 1181 } else { 1182 el = el.cloneNode(false); 1183 } 1184 1185 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." 1186 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." 1187 // "In either case, the algorithm must be invoked with fragment as the input 1188 // and element as the context element." 1189 el.innerHTML = fragmentStr; 1190 1191 // "If this raises an exception, then abort these steps. Otherwise, let new 1192 // children be the nodes returned." 1193 1194 // "Let fragment be a new DocumentFragment." 1195 // "Append all new children to fragment." 1196 // "Return fragment." 1197 return dom.fragmentFromNodeChildren(el); 1198 } : 1199 1200 // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that 1201 // previous versions of Rangy used (with the exception of using a body element rather than a div) 1202 function(fragmentStr) { 1203 assertNotDetached(this); 1204 var doc = getRangeDocument(this); 1205 var el = doc.createElement("body"); 1206 el.innerHTML = fragmentStr; 1207 1208 return dom.fragmentFromNodeChildren(el); 1209 }; 1210 1211 /*----------------------------------------------------------------------------------------------------------------*/ 1212 1213 var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", 1214 "commonAncestorContainer"]; 1215 1216 var s2s = 0, s2e = 1, e2e = 2, e2s = 3; 1217 var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; 1218 1219 function RangePrototype() {} 1220 1221 RangePrototype.prototype = { 1222 attachListener: function(type, listener) { 1223 this._listeners[type].push(listener); 1224 }, 1225 1226 compareBoundaryPoints: function(how, range) { 1227 assertRangeValid(this);
1228 assertSameDocumentOrFragment(this.startContainer, range.startContainer); 1229 1230 var nodeA, offsetA, nodeB, offsetB; 1231 var prefixA = (how == e2s || how == s2s) ? "start" : "end"; 1232 var prefixB = (how == s2e || how == s2s) ? "start" : "end"; 1233 nodeA = this[prefixA + "Container"]; 1234 offsetA = this[prefixA + "Offset"]; 1235 nodeB = range[prefixB + "Container"]; 1236 offsetB = range[prefixB + "Offset"]; 1237 return dom.comparePoints(nodeA, offsetA, nodeB, offsetB); 1238 }, 1239 1240 insertNode: function(node) { 1241 assertRangeValid(this); 1242 assertValidNodeType(node, insertableNodeTypes); 1243 assertNodeNotReadOnly(this.startContainer); 1244 1245 if (dom.isAncestorOf(node, this.startContainer, true)) { 1246 throw new DOMException("HIERARCHY_REQUEST_ERR"); 1247 } 1248 1249 // No check for whether the container of the start of the Range is of a type that does not allow 1250 // children of the type of node: the browser's DOM implementation should do this for us when we attempt 1251 // to add the node 1252 1253 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); 1254 this.setStartBefore(firstNodeInserted); 1255 }, 1256 1257 cloneContents: function() { 1258 assertRangeValid(this); 1259 1260 var clone, frag; 1261 if (this.collapsed) { 1262 return getRangeDocument(this).createDocumentFragment(); 1263 } else { 1264 if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) { 1265 clone = this.startContainer.cloneNode(true); 1266 clone.data = clone.data.slice(this.startOffset, this.endOffset); 1267 frag = getRangeDocument(this).createDocumentFragment(); 1268 frag.appendChild(clone); 1269 return frag; 1270 } else { 1271 var iterator = new RangeIterator(this, true); 1272 clone = cloneSubtree(iterator); 1273 iterator.detach(); 1274 } 1275 return clone; 1276 } 1277 }, 1278 1279 canSurroundContents: function() { 1280 assertRangeValid(this); 1281 assertNodeNotReadOnly(this.startContainer); 1282 assertNodeNotReadOnly(this.endContainer); 1283 1284 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects 1285 // no non-text nodes. 1286 var iterator = new RangeIterator(this, true); 1287 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || 1288 (iterator._last && isNonTextPartiallySelected(iterator._last, this))); 1289 iterator.detach(); 1290 return !boundariesInvalid; 1291 }, 1292 1293 surroundContents: function(node) { 1294 assertValidNodeType(node, surroundNodeTypes); 1295 1296 if (!this.canSurroundContents()) { 1297 throw new RangeException("BAD_BOUNDARYPOINTS_ERR"); 1298 } 1299 1300 // Extract the contents 1301 var content = this.extractContents(); 1302 1303 // Clear the children of the node 1304 if (node.hasChildNodes()) { 1305 while (node.lastChild) { 1306 node.removeChild(node.lastChild); 1307 } 1308 } 1309 1310 // Insert the new node and add the extracted contents 1311 insertNodeAtPosition(node, this.startContainer, this.startOffset); 1312 node.appendChild(content); 1313 1314 this.selectNode(node); 1315 }, 1316 1317 cloneRange: function() { 1318 assertRangeValid(this); 1319 var range = new Range(getRangeDocument(this)); 1320 var i = rangeProperties.length, prop; 1321 while (i--) { 1322 prop = rangeProperties[i]; 1323 range[prop] = this[prop]; 1324 } 1325 return range; 1326 }, 1327 1328 toString: function() { 1329 assertRangeValid(this); 1330 var sc = this.startContainer; 1331 if (sc === this.endContainer && dom.isCharacterDataNode(sc)) { 1332 return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset)
1332: ""; 1333 } else { 1334 var textBits = [], iterator = new RangeIterator(this, true); 1335 1336 iterateSubtree(iterator, function(node) { 1337 // Accept only text or CDATA nodes, not comments 1338 1339 if (node.nodeType == 3 || node.nodeType == 4) { 1340 textBits.push(node.data); 1341 } 1342 }); 1343 iterator.detach(); 1344 return textBits.join(""); 1345 } 1346 }, 1347 1348 // The methods below are all non-standard. The following batch were introduced by Mozilla but have since 1349 // been removed from Mozilla. 1350 1351 compareNode: function(node) { 1352 assertRangeValid(this); 1353 1354 var parent = node.parentNode; 1355 var nodeIndex = dom.getNodeIndex(node); 1356 1357 if (!parent) { 1358 throw new DOMException("NOT_FOUND_ERR"); 1359 } 1360 1361 var startComparison = this.comparePoint(parent, nodeIndex), 1362 endComparison = this.comparePoint(parent, nodeIndex + 1); 1363 1364 if (startComparison < 0) { // Node starts before 1365 return (endComparison > 0) ? n_b_a : n_b; 1366 } else { 1367 return (endComparison > 0) ? n_a : n_i; 1368 } 1369 }, 1370 1371 comparePoint: function(node, offset) { 1372 assertRangeValid(this); 1373 assertNode(node, "HIERARCHY_REQUEST_ERR"); 1374 assertSameDocumentOrFragment(node, this.startContainer); 1375 1376 if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { 1377 return -1; 1378 } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { 1379 return 1; 1380 } 1381 return 0; 1382 }, 1383 1384 createContextualFragment: createContextualFragment, 1385 1386 toHtml: function() { 1387 assertRangeValid(this); 1388 var container = getRangeDocument(this).createElement("div"); 1389 container.appendChild(this.cloneContents()); 1390 return container.innerHTML; 1391 }, 1392 1393 // touchingIsIntersecting determines whether this method considers a node that borders a range intersects 1394 // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) 1395 intersectsNode: function(node, touchingIsIntersecting) { 1396 assertRangeValid(this); 1397 assertNode(node, "NOT_FOUND_ERR"); 1398 if (dom.getDocument(node) !== getRangeDocument(this)) { 1399 return false; 1400 } 1401 1402 var parent = node.parentNode, offset = dom.getNodeIndex(node); 1403 assertNode(parent, "NOT_FOUND_ERR"); 1404 1405 var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset), 1406 endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset); 1407 1408 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; 1409 }, 1410 1411 1412 isPointInRange: function(node, offset) { 1413 assertRangeValid(this); 1414 assertNode(node, "HIERARCHY_REQUEST_ERR"); 1415 assertSameDocumentOrFragment(node, this.startContainer); 1416 1417 return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && 1418 (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); 1419 }, 1420 1421 // The methods below are non-standard and invented by me. 1422 1423 // Sharing a boundary start-to-end or end-to-start does not count as intersection. 1424 intersectsRange: function(range, touchingIsIntersecting) { 1425 assertRangeValid(this); 1426 1427 if (getRangeDocument(range) != getRangeDocument(this)) { 1428 throw new DOMException("WRONG_DOCUMENT_ERR"); 1429 } 1430 1431 var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset), 1432 endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset); 1433 1434 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
vendor: 10,745 bytes, lines 1435-1701
1435 }, 1436 1437 intersection: function(range) { 1438 if (this.intersectsRange(range)) { 1439 var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), 1440 endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); 1441 1442 var intersectionRange = this.cloneRange(); 1443 1444 if (startComparison == -1) { 1445 intersectionRange.setStart(range.startContainer, range.startOffset); 1446 } 1447 if (endComparison == 1) { 1448 intersectionRange.setEnd(range.endContainer, range.endOffset); 1449 } 1450 return intersectionRange; 1451 } 1452 return null; 1453 }, 1454 1455 union: function(range) { 1456 if (this.intersectsRange(range, true)) { 1457 var unionRange = this.cloneRange(); 1458 if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { 1459 unionRange.setStart(range.startContainer, range.startOffset); 1460 } 1461 if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { 1462 unionRange.setEnd(range.endContainer, range.endOffset); 1463 } 1464 return unionRange; 1465 } else { 1466 throw new RangeException("Ranges do not intersect"); 1467 } 1468 }, 1469 1470 containsNode: function(node, allowPartial) { 1471 if (allowPartial) { 1472 return this.intersectsNode(node, false); 1473 } else { 1474 return this.compareNode(node) == n_i; 1475 } 1476 }, 1477 1478 containsNodeContents: function(node) { 1479 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0; 1480 }, 1481 1482 containsRange: function(range) { 1483 return this.intersection(range).equals(range); 1484 }, 1485 1486 containsNodeText: function(node) { 1487 var nodeRange = this.cloneRange(); 1488 nodeRange.selectNode(node); 1489 var textNodes = nodeRange.getNodes([3]); 1490 if (textNodes.length > 0) { 1491 nodeRange.setStart(textNodes[0], 0); 1492 var lastTextNode = textNodes.pop(); 1493 nodeRange.setEnd(lastTextNode, lastTextNode.length); 1494 var contains = this.containsRange(nodeRange); 1495 nodeRange.detach(); 1496 return contains; 1497 } else { 1498 return this.containsNodeContents(node); 1499 } 1500 }, 1501 1502 createNodeIterator: function(nodeTypes, filter) { 1503 assertRangeValid(this); 1504 return new RangeNodeIterator(this, nodeTypes, filter); 1505 }, 1506 1507 getNodes: function(nodeTypes, filter) { 1508 assertRangeValid(this); 1509 return getNodesInRange(this, nodeTypes, filter); 1510 }, 1511 1512 getDocument: function() { 1513 return getRangeDocument(this); 1514 }, 1515 1516 collapseBefore: function(node) { 1517 assertNotDetached(this); 1518 1519 this.setEndBefore(node); 1520 this.collapse(false); 1521 }, 1522 1523 collapseAfter: function(node) { 1524 assertNotDetached(this); 1525 1526 this.setStartAfter(node); 1527 this.collapse(true); 1528 }, 1529 1530 getName: function() { 1531 return "DomRange"; 1532 }, 1533 1534 equals: function(range) { 1535 return Range.rangesEqual(this, range); 1536 }, 1537 1538 inspect: function() { 1539 return inspect(this); 1540 } 1541 }; 1542 1543 function copyComparisonConstantsToObject(obj) { 1544 obj.START_TO_START = s2s; 1545 obj.START_TO_END = s2e; 1546 obj.END_TO_END = e2e; 1547 obj.END_TO_START = e2s; 1548 1549 obj.NODE_BEFORE = n_b; 1550 obj.NODE_AFTER = n_a; 1551 obj.NODE_BEFORE_AND_AFTER = n_b_a; 1552 obj.NODE_INSIDE = n_i; 1553 } 1554 1555 function copyComparisonConstants(constructor) { 1556 copyComparisonConstantsToObject(constructor); 1557 copyComparisonConstantsToObject(constructor.prototype); 1558 } 1559 1560 function createRangeContentRemover(remover, boundaryUpdater) { 1561 return function() { 1562 assertRangeValid(this); 1563 1564 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; 1565 1566 var iterator = new RangeIterator(this, true); 1567 1568 // Work out where to position the range after content removal 1569 var node, boundary; 1570 if (sc !== root) { 1571 node = dom.getClosestAncestorIn(sc, root, true); 1572 boundary = getBoundaryAfterNode(node); 1573 sc = boundary.node; 1574 so = boundary.offset; 1575 } 1576 1577 // Check none of the range is read-only 1578 iterateSubtree(iterator, assertNodeNotReadOnly); 1579 1580 iterator.reset(); 1581 1582 // Remove the content 1583 var returnValue = remover(iterator); 1584 iterator.detach(); 1585 1586 // Move to the new position 1587 boundaryUpdater(this, sc, so, sc, so); 1588 1589 return returnValue; 1590 }; 1591 } 1592 1593 function createPrototypeRange(constructor, boundaryUpdater, detacher) { 1594 function createBeforeAfterNodeSetter(isBefore, isStart) { 1595 return function(node) { 1596 assertNotDetached(this); 1597 assertValidNodeType(node, beforeAfterNodeTypes); 1598 assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); 1599 1600 var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); 1601 (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); 1602 }; 1603 } 1604 1605 function setRangeStart(range, node, offset) { 1606 var ec = range.endContainer, eo = range.endOffset; 1607 if (node !== range.startContainer || offset !== range.startOffset) { 1608 // Check the root containers of the range and the new boundary, and also check whether the new boundary 1609 // is after the current end. In either case, collapse the range to the new position 1610 if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) { 1611 ec = node; 1612 eo = offset; 1613 } 1614 boundaryUpdater(range, node, offset, ec, eo); 1615 } 1616 } 1617 1618 function setRangeEnd(range, node, offset) { 1619 var sc = range.startContainer, so = range.startOffset; 1620 if (node !== range.endContainer || offset !== range.endOffset) { 1621 // Check the root containers of the range and the new boundary, and also check whether the new boundary 1622 // is after the current end. In either case, collapse the range to the new position 1623 if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) { 1624 sc = node; 1625 so = offset; 1626 } 1627 boundaryUpdater(range, sc, so, node, offset); 1628 } 1629 } 1630 1631 function setRangeStartAndEnd(range, node, offset) { 1632 if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) { 1633 boundaryUpdater(range, node, offset, node, offset); 1634 } 1635 } 1636 1637 constructor.prototype = new RangePrototype(); 1638 1639 api.util.extend(constructor.prototype, { 1640 setStart: function(node, offset) { 1641 assertNotDetached(this); 1642 assertNoDocTypeNotationEntityAncestor(node, true); 1643 assertValidOffset(node, offset); 1644 1645 setRangeStart(this, node, offset); 1646 }, 1647 1648 setEnd: function(node, offset) { 1649 assertNotDetached(this); 1650 assertNoDocTypeNotationEntityAncestor(node, true); 1651 assertValidOffset(node, offset); 1652 1653 setRangeEnd(this, node, offset); 1654 }, 1655 1656 setStartBefore: createBeforeAfterNodeSetter(true, true), 1657 setStartAfter: createBeforeAfterNodeSetter(false, true), 1658 setEndBefore: createBeforeAfterNodeSetter(true, false), 1659 setEndAfter: createBeforeAfterNodeSetter(false, false), 1660 1661 collapse: function(isStart) { 1662 assertRangeValid(this); 1663 if (isStart) { 1664 boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); 1665 } else { 1666 boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); 1667 } 1668 }, 1669 1670 selectNodeContents: function(node) { 1671 // This doesn't seem well specified: the spec talks only about selecting the node's contents, which 1672 // could be taken to mean only its children. However, browsers implement this the same as selectNode for 1673 // text nodes, so I shall do likewise 1674 assertNotDetached(this); 1675 assertNoDocTypeNotationEntityAncestor(node, true); 1676 1677 boundaryUpdater(this, node, 0, node, dom.getNodeLength(node)); 1678 }, 1679 1680 selectNode: function(node) { 1681 assertNotDetached(this); 1682 assertNoDocTypeNotationEntityAncestor(node, false); 1683 assertValidNodeType(node, beforeAfterNodeTypes); 1684 1685 var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); 1686 boundaryUpdater(this, start.node, start.offset, end.node, end.offset); 1687 }, 1688 1689 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), 1690 1691 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), 1692 1693 canSurroundContents: function() { 1694 assertRangeValid(this); 1695 assertNodeNotReadOnly(this.startContainer); 1696 assertNodeNotReadOnly(this.endContainer); 1697 1698 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects 1699 // no non-text nodes. 1700 var iterator = new RangeIterator(this, true); 1701 var boundariesInvalid = (iterator._first && (isNonTextParti
1701allySelected(iterator._first, this)) || 1702 (iterator._last && isNonTextPartiallySelected(iterator._last, this))); 1703 iterator.detach(); 1704 return !boundariesInvalid; 1705 }, 1706 1707 detach: function() { 1708 detacher(this); 1709 }, 1710 1711 splitBoundaries: function() { 1712 assertRangeValid(this); 1713 1714 1715 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; 1716 var startEndSame = (sc === ec); 1717 1718 if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { 1719 dom.splitDataNode(ec, eo); 1720 1721 } 1722 1723 if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) { 1724 1725 sc = dom.splitDataNode(sc, so); 1726 if (startEndSame) { 1727 eo -= so; 1728 ec = sc; 1729 } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) { 1730 eo++; 1731 } 1732 so = 0; 1733 1734 } 1735 boundaryUpdater(this, sc, so, ec, eo); 1736 }, 1737 1738 normalizeBoundaries: function() { 1739 assertRangeValid(this); 1740 1741 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; 1742 1743 var mergeForward = function(node) { 1744 var sibling = node.nextSibling; 1745 if (sibling && sibling.nodeType == node.nodeType) { 1746 ec = node; 1747 eo = node.length; 1748 node.appendData(sibling.data); 1749 sibling.parentNode.removeChild(sibling); 1750 } 1751 }; 1752 1753 var mergeBackward = function(node) { 1754 var sibling = node.previousSibling; 1755 if (sibling && sibling.nodeType == node.nodeType) { 1756 sc = node; 1757 var nodeLength = node.length; 1758 so = sibling.length; 1759 node.insertData(0, sibling.data); 1760 sibling.parentNode.removeChild(sibling); 1761 if (sc == ec) { 1762 eo += so; 1763 ec = sc; 1764 } else if (ec == node.parentNode) { 1765 var nodeIndex = dom.getNodeIndex(node); 1766 if (eo == nodeIndex) { 1767 ec = node; 1768 eo = nodeLength; 1769 } else if (eo > nodeIndex) { 1770 eo--; 1771 } 1772 } 1773 } 1774 }; 1775 1776 var normalizeStart = true; 1777 1778 if (dom.isCharacterDataNode(ec)) { 1779 if (ec.length == eo) { 1780 mergeForward(ec); 1781 } 1782 } else { 1783 if (eo > 0) { 1784 var endNode = ec.childNodes[eo - 1]; 1785 if (endNode && dom.isCharacterDataNode(endNode)) { 1786 mergeForward(endNode); 1787 } 1788 } 1789 normalizeStart = !this.collapsed; 1790 } 1791 1792 if (normalizeStart) { 1793 if (dom.isCharacterDataNode(sc)) { 1794 if (so == 0) { 1795 mergeBackward(sc); 1796 } 1797 } else { 1798 if (so < sc.childNodes.length) { 1799 var startNode = sc.childNodes[so]; 1800 if (startNode && dom.isCharacterDataNode(startNode)) { 1801 mergeBackward(startNode); 1802 } 1803 } 1804 } 1805 } else { 1806 sc = ec; 1807 so = eo; 1808 } 1809 1810 boundaryUpdater(this, sc, so, ec, eo); 1811 }, 1812 1813 collapseToPoint: function(node, offset) { 1814 assertNotDetached(this); 1815 1816 assertNoDocTypeNotationEntityAncestor(node, true); 1817 assertValidOffset(node, offset); 1818 1819 setRangeStartAndEnd(this, node, offset); 1820 } 1821 }); 1822 1823 copyComparisonConstants(constructor); 1824 } 1825 1826 /*----------------------------------------------------------------------------------------------------------------*/ 1827 1828 // Updates commonAncestorContainer and collapsed after boundary change 1829 function updateCollapsedAndCommonAncestor(range) { 1830 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); 1831 range.commonAncestorContainer = range.collapsed ? 1832 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); 1833 } 1834 1835 function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { 1836 var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset); 1837 var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset); 1838 1839 range.startContainer = startContainer; 1840 range.startOffset = startOffset; 1841 range.endContainer = endContainer; 1842 range.endOffset = endOffset; 1843 1844 updateCollapsedAndCommonAncestor(range); 1845 dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved}); 1846 } 1847 1848 function detach(range) { 1849 assertNotDetached(range); 1850 range.startContainer = range.startOffset = range.endContainer = range.endOffset = null; 1851 range.collapsed = range.commonAncestorContainer = null; 1852 dispatchEvent(range, "detach", null); 1853 range._listeners = null; 1854 } 1855 1856 /** 1857 * @constructor 1858 */ 1859 function Range(doc) { 1860 this.startContainer = doc; 1861 this.startOffset = 0; 1862 this.endContainer = doc; 1863 this.endOffset = 0; 1864 this._listeners = { 1865 boundarychange: [], 1866 detach: [] 1867 }; 1868 updateCollapsedAndCommonAncestor(this); 1869 } 1870 1871 createPrototypeRange(Range, updateBoundaries, detach); 1872 1873 api.rangePrototype = RangePrototype.prototype; 1874 1875 Range.rangeProperties = rangeProperties; 1876 Range.RangeIterator = RangeIterator; 1877 Range.copyComparisonConstants = copyComparisonConstants; 1878 Range.createPrototypeRange = createPrototypeRange; 1879 Range.inspect = inspect; 1880 Range.getRangeDocument = getRangeDocument; 1881 Range.rangesEqual = function(r1, r2) { 1882 return r1.startContainer === r2.startContainer && 1883 r1.startOffset === r2.startOffset && 1884 r1.endContainer === r2.endContainer && 1885 r1.endOffset === r2.endOffset; 1886 }; 1887 1888 api.DomRange = Range; 1889 api.RangeException = RangeException; 1890});rangy.createModule("WrappedRange", function(api, module) { 1891 api.requireModules( ["DomUtil", "DomRange"] ); 1892 1893 /** 1894 * @constructor 1895 */ 1896 var WrappedRange; 1897 var dom = api.dom; 1898 var DomPosition = dom.DomPosition; 1899 var DomRange = api.DomRange; 1900 1901 1902 1903 /*----------------------------------------------------------------------------------------------------------------*/ 1904 1905 /* 1906 This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() 1907 method. For example, in the following (where pipes denote the selection boundaries): 1908 1909 <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul> 1910 1911 var range = document.selection.createRange(); 1912 alert(range.parentElement().id); // Should alert "ul" but alerts "b" 1913 1914 This method returns the common ancestor node of the following: 1915 - the parentElement() of the textRange 1916 - the parentElement() of the textRange after calling collapse(true) 1917 - the parentElement() of the textRange after calling collapse(false) 1918 */ 1919 function getTextRangeContainerElement(textRange) { 1920 var parentEl = textRange.parentElement(); 1921 1922 var range = textRange.duplicate(); 1923 range.collapse(true); 1924 var startEl = range.parentElement(); 1925 range = textRange.duplicate(); 1926 range.collapse(false); 1927 var endEl = range.parentElement(); 1928 var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); 1929 1930 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); 1931 } 1932 1933 function textRangeIsCollapsed(textRange) { 1934 return textRange.compareEndPoints("StartToEnd", textRange) == 0; 1935 } 1936 1937 // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as 1938 // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has 1939 // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling 1940 // for inputs and images, plus optimizations. 1941 function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) { 1942 var workingRange = textRange.duplicate(); 1943 1944 workingRange.collapse(isStart); 1945 var containerElement = workingRange.parentElement(); 1946 1947 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so 1948 // check for that 1949 // TODO: Find out when. Workaround for wholeRangeContainerElement may break this 1950 if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) { 1951 containerElement = wholeRangeContainerElement; 1952 1953 } 1954 1955 1956 1957 // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
1958 // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx 1959 if (!containerElement.canHaveHTML) { 1960 return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); 1961 } 1962 1963 var workingNode = dom.getDocument(containerElement).createElement("span"); 1964 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; 1965 var previousNode, nextNode, boundaryPosition, boundaryNode; 1966 1967 // Move the working range through the container's children, starting at the end and working backwards, until the 1968 // working range reaches or goes past the boundary we're interested in 1969 do { 1970 containerElement.insertBefore(workingNode, workingNode.previousSibling); 1971 workingRange.moveToElementText(workingNode); 1972 } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 && 1973 workingNode.previousSibling); 1974 1975 // We've now reached or gone past the boundary of the text range we're interested in 1976 // so have identified the node we want 1977 boundaryNode = workingNode.nextSibling; 1978 1979 if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) { 1980 // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the 1981 // node containing the text range's boundary, so we move the end of the working range to the boundary point 1982 // and measure the length of its text to get the boundary's offset within the node. 1983 workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); 1984 1985 1986 var offset; 1987 1988 if (/[\r\n]/.test(boundaryNode.data)) { 1989 /* 1990 For the particular case of a boundary within a text node containing line breaks (within a <pre> element, 1991 for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts: 1992 1993 - Each line break is represented as \r in the text node's data/nodeValue properties 1994 - Each line break is represented as \r\n in the TextRange's 'text' property 1995 - The 'text' property of the TextRange does not contain trailing line breaks 1996 1997 To get round the problem presented by the final fact above, we can use the fact that TextRange's 1998 moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily 1999 the same as the number of characters it was instructed to move. The simplest approach is to use this to 2000 store the characters moved when moving both the start and end of the range to the start of the document 2001 body and subtracting the start offset from the end offset (the "move-negative-gazillion" method). 2002 However, this is extremely slow when the document is large and the range is near the end of it. Clearly 2003 doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same 2004 problem. 2005 2006 Another approach that works is to use moveStart() to move the start boundary of the range up to the end 2007 boundary one character at a time and incrementing a counter with the value returned by the moveStart() 2008 call. However, the check for whether the start boundary has reached the end boundary is expensive, so 2009 this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of 2010 the range within the document). 2011 2012 The method below is a hybrid of the two methods above. It uses the fact that a string containing the 2013 TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the 2014 text of the TextRange, so the start of the range is moved that length initially and then a character at 2015 a time to make up for any trailing line breaks not contained in the 'text' property. This has good 2016 performance in most situations compared to the previous two methods. 2017 */ 2018 var tempRange = workingRange.duplicate(); 2019 var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length; 2020 2021 offset = tempRange.moveStart("character", rangeLength); 2022 while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) { 2023 offset++; 2024 tempRange.moveStart("character", 1); 2025 } 2026 } else { 2027 offset = workingRange.text.length; 2028 }
2029 boundaryPosition = new DomPosition(boundaryNode, offset); 2030 } else { 2031 2032 2033 // If the boundary immediately follows a character data node and this is the end boundary, we should favour 2034 // a position within that, and likewise for a start boundary preceding a character data node 2035 previousNode = (isCollapsed || !isStart) && workingNode.previousSibling; 2036 nextNode = (isCollapsed || isStart) && workingNode.nextSibling; 2037 2038 2039 2040 if (nextNode && dom.isCharacterDataNode(nextNode)) { 2041 boundaryPosition = new DomPosition(nextNode, 0); 2042 } else if (previousNode && dom.isCharacterDataNode(previousNode)) { 2043 boundaryPosition = new DomPosition(previousNode, previousNode.length); 2044 } else { 2045 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode)); 2046 } 2047 } 2048 2049 // Clean up 2050 workingNode.parentNode.removeChild(workingNode); 2051 2052 return boundaryPosition; 2053 } 2054 2055 // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node. 2056 // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange 2057 // (http://code.google.com/p/ierange/) 2058 function createBoundaryTextRange(boundaryPosition, isStart) { 2059 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset; 2060 var doc = dom.getDocument(boundaryPosition.node); 2061 var workingNode, childNodes, workingRange = doc.body.createTextRange(); 2062 var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node); 2063 2064 if (nodeIsDataNode) { 2065 boundaryNode = boundaryPosition.node; 2066 boundaryParent = boundaryNode.parentNode; 2067 } else { 2068 childNodes = boundaryPosition.node.childNodes; 2069 boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null; 2070 boundaryParent = boundaryPosition.node; 2071 } 2072 2073 // Position the range immediately before the node containing the boundary 2074 workingNode = doc.createElement("span"); 2075 2076 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the 2077 // element rather than immediately before or after it, which is what we want 2078 workingNode.innerHTML = "&#feff;"; 2079 2080 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report 2081 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12 2082 if (boundaryNode) { 2083 boundaryParent.insertBefore(workingNode, boundaryNode); 2084 } else { 2085 boundaryParent.appendChild(workingNode); 2086 } 2087 2088 workingRange.moveToElementText(workingNode); 2089 workingRange.collapse(!isStart); 2090 2091 // Clean up 2092 boundaryParent.removeChild(workingNode); 2093 2094 // Move the working range to the text offset, if required 2095 if (nodeIsDataNode) { 2096 workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset); 2097 } 2098 2099 return workingRange; 2100 } 2101 2102 /*----------------------------------------------------------------------------------------------------------------*/ 2103 2104 if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) { 2105 // This is a wrapper around the browser's native DOM Range. It has two aims: 2106 // - Provide workarounds for specific browser bugs 2107 // - provide convenient extensions, which are inherited from Rangy's DomRange 2108 2109 (function() { 2110 var rangeProto; 2111 var rangeProperties = DomRange.rangeProperties; 2112 var canSetRangeStartAfterEnd; 2113 2114 function updateRangeProperties(range) { 2115 var i = rangeProperties.length, prop; 2116 while (i--) { 2117 prop = rangeProperties[i];
2118 range[prop] = range.nativeRange[prop]; 2119 } 2120 } 2121 2122 function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) { 2123 var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); 2124 var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); 2125 2126 // Always set both boundaries for the benefit of IE9 (see issue 35) 2127 if (startMoved || endMoved) { 2128 range.setEnd(endContainer, endOffset); 2129 range.setStart(startContainer, startOffset); 2130 } 2131 } 2132 2133 function detach(range) { 2134 range.nativeRange.detach(); 2135 range.detached = true; 2136 var i = rangeProperties.length, prop; 2137 while (i--) { 2138 prop = rangeProperties[i]; 2139 range[prop] = null; 2140 } 2141 } 2142 2143 var createBeforeAfterNodeSetter; 2144 2145 WrappedRange = function(range) { 2146 if (!range) { 2147 throw new Error("Range must be specified"); 2148 } 2149 this.nativeRange = range; 2150 updateRangeProperties(this); 2151 }; 2152 2153 DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach); 2154 2155 rangeProto = WrappedRange.prototype; 2156 2157 rangeProto.selectNode = function(node) { 2158 this.nativeRange.selectNode(node); 2159 updateRangeProperties(this); 2160 }; 2161 2162 rangeProto.deleteContents = function() { 2163 this.nativeRange.deleteContents(); 2164 updateRangeProperties(this); 2165 }; 2166 2167 rangeProto.extractContents = function() { 2168 var frag = this.nativeRange.extractContents(); 2169 updateRangeProperties(this); 2170 return frag; 2171 }; 2172 2173 rangeProto.cloneContents = function() { 2174 return this.nativeRange.cloneContents(); 2175 }; 2176 2177 // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still 2178 // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for 2179 // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of 2180 // insertNode, which works but is almost certainly slower than the native implementation. 2181/* 2182 rangeProto.insertNode = function(node) { 2183 this.nativeRange.insertNode(node); 2184 updateRangeProperties(this); 2185 }; 2186*/ 2187 2188 rangeProto.surroundContents = function(node) { 2189 this.nativeRange.surroundContents(node); 2190 updateRangeProperties(this); 2191 }; 2192 2193 rangeProto.collapse = function(isStart) { 2194 this.nativeRange.collapse(isStart); 2195 updateRangeProperties(this); 2196 }; 2197 2198 rangeProto.cloneRange = function() { 2199 return new WrappedRange(this.nativeRange.cloneRange()); 2200 }; 2201 2202 rangeProto.refresh = function() { 2203 updateRangeProperties(this); 2204 }; 2205 2206 rangeProto.toString = function() { 2207 return this.nativeRange.toString(); 2208 }; 2209 2210 // Create test range and node for feature detection 2211 2212 var testTextNode = document.createTextNode("test"); 2213 dom.getBody(document).appendChild(testTextNode); 2214 var range = document.createRange(); 2215 2216 /*--------------------------------------------------------------------------------------------------------*/ 2217 2218 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and 2219 // correct for it 2220 2221 range.setStart(testTextNode, 0); 2222 range.setEnd(testTextNode, 0); 2223 2224 try { 2225 range.setStart(testTextNode, 1); 2226 canSetRangeStartAfterEnd = true; 2227 2228 rangeProto.setStart = function(node, offset) { 2229 this.nativeRange.setStart(node, offset); 2230 updateRangeProperties(this); 2231 }; 2232 2233 rangeProto.setEnd = function(node, offset) { 2234 this.nativeRange.setEnd(node, offset); 2235 updateRangeProperties(this); 2236 }; 2237 2238 createBeforeAfterNodeSetter = function(name) { 2239 return function(node) { 2240 this.nativeRange[name](node); 2241 updateRangeProperties(this); 2242 }; 2243 }; 2244 2245 } catch(ex) { 2246 2247 2248 canSetRangeStartAfterEnd = false; 2249 2250 rangeProto.setStart = function(node, offset) { 2251 try { 2252 this.nativeRange.setStart(node, offset); 2253 } catch (ex) { 2254 this.nativeRange.setEnd(node, offset); 2255 this.nativeRange.setStart(node, offset); 2256 } 2257 updateRangeProperties(this); 2258 }; 2259 2260 rangeProto.setEnd = function(node, offset) { 2261 try { 2262 this.nativeRange.setEnd(node, offset); 2263 } catch (ex) { 2264 this.nativeRange.setStart(node, offset); 2265 this.nativeRange.setEnd(node, offset); 2266 } 2267 updateRangeProperties(this); 2268 }; 2269 2270 createBeforeAfterNodeSetter = function(name, oppositeName) { 2271 return function(node) { 2272 try { 2273 this.nativeRange[name](node); 2274 } catch (ex) { 2275 this.nativeRange[oppositeName](node); 2276 this.nativeRange[name](node); 2277 } 2278 updateRangeProperties(this); 2279 }; 2280 }; 2281 } 2282 2283 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); 2284 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); 2285 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); 2286 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); 2287 2288 /*--------------------------------------------------------------------------------------------------------*/ 2289 2290 // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to 2291 // the 0th character of the text node 2292 range.selectNodeContents(testTextNode); 2293 if (range.startContainer == testTextNode && range.endContainer == testTextNode && 2294 range.startOffset == 0 && range.endOffset == testTextNode.length) { 2295 rangeProto.selectNodeContents = function(node) { 2296 this.nativeRange.selectNodeContents(node); 2297 updateRangeProperties(this); 2298 }; 2299 } else { 2300 rangeProto.selectNodeContents = function(node) {
2301 this.setStart(node, 0); 2302 this.setEnd(node, DomRange.getEndOffset(node)); 2303 }; 2304 } 2305 2306 /*--------------------------------------------------------------------------------------------------------*/ 2307 2308 // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants 2309 // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 2310 2311 range.selectNodeContents(testTextNode); 2312 range.setEnd(testTextNode, 3); 2313 2314 var range2 = document.createRange(); 2315 range2.selectNodeContents(testTextNode); 2316 range2.setEnd(testTextNode, 4); 2317 range2.setStart(testTextNode, 2); 2318 2319 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 & 2320 range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { 2321 // This is the wrong way round, so correct for it 2322 2323 2324 rangeProto.compareBoundaryPoints = function(type, range) { 2325 range = range.nativeRange || range; 2326 if (type == range.START_TO_END) { 2327 type = range.END_TO_START; 2328 } else if (type == range.END_TO_START) { 2329 type = range.START_TO_END; 2330 } 2331 return this.nativeRange.compareBoundaryPoints(type, range); 2332 }; 2333 } else { 2334 rangeProto.compareBoundaryPoints = function(type, range) { 2335 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); 2336 }; 2337 } 2338 2339 /*--------------------------------------------------------------------------------------------------------*/ 2340 2341 // Test for existence of createContextualFragment and delegate to it if it exists 2342 if (api.util.isHostMethod(range, "createContextualFragment")) { 2343 rangeProto.createContextualFragment = function(fragmentStr) { 2344 return this.nativeRange.createContextualFragment(fragmentStr); 2345 }; 2346 } 2347 2348 /*--------------------------------------------------------------------------------------------------------*/ 2349 2350 // Clean up 2351 dom.getBody(document).removeChild(testTextNode); 2352 range.detach(); 2353 range2.detach(); 2354 })(); 2355 2356 api.createNativeRange = function(doc) { 2357 doc = doc || document; 2358 return doc.createRange(); 2359 }; 2360 } else if (api.features.implementsTextRange) { 2361 // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a 2362 // prototype 2363 2364 WrappedRange = function(textRange) { 2365 this.textRange = textRange; 2366 this.refresh(); 2367 }; 2368 2369 WrappedRange.prototype = new DomRange(document); 2370 2371 WrappedRange.prototype.refresh = function() { 2372 var start, end; 2373 2374 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that. 2375 var rangeContainerElement = getTextRangeContainerElement(this.textRange); 2376 2377 if (textRangeIsCollapsed(this.textRange)) { 2378 end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true); 2379 } else { 2380 2381 start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false); 2382 end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false); 2383 } 2384 2385 this.setStart(start.node, start.offset); 2386 this.setEnd(end.node, end.offset); 2387 }; 2388 2389 DomRange.copyComparisonConstants(WrappedRange); 2390 2391 // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work 2392 var globalObj = (function() { return this; })();
2393 if (typeof globalObj.Range == "undefined") { 2394 globalObj.Range = WrappedRange; 2395 } 2396 2397 api.createNativeRange = function(doc) { 2398 doc = doc || document; 2399 return doc.body.createTextRange(); 2400 }; 2401 } 2402 2403 if (api.features.implementsTextRange) { 2404 WrappedRange.rangeToTextRange = function(range) { 2405 if (range.collapsed) { 2406 var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 2407 2408 2409 2410 return tr; 2411 2412 //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 2413 } else { 2414 var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 2415 var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false); 2416 var textRange = dom.getDocument(range.startContainer).body.createTextRange(); 2417 textRange.setEndPoint("StartToStart", startRange); 2418 textRange.setEndPoint("EndToEnd", endRange); 2419 return textRange; 2420 } 2421 }; 2422 } 2423 2424 WrappedRange.prototype.getName = function() { 2425 return "WrappedRange"; 2426 }; 2427 2428 api.WrappedRange = WrappedRange; 2429 2430 api.createRange = function(doc) { 2431 doc = doc || document; 2432 return new WrappedRange(api.createNativeRange(doc)); 2433 }; 2434 2435 api.createRangyRange = function(doc) { 2436 doc = doc || document; 2437 return new DomRange(doc); 2438 }; 2439 2440 api.createIframeRange = function(iframeEl) { 2441 return api.createRange(dom.getIframeDocument(iframeEl)); 2442 }; 2443 2444 api.createIframeRangyRange = function(iframeEl) { 2445 return api.createRangyRange(dom.getIframeDocument(iframeEl)); 2446 }; 2447 2448 api.addCreateMissingNativeApiListener(function(win) { 2449 var doc = win.document; 2450 if (typeof doc.createRange == "undefined") { 2451 doc.createRange = function() { 2452 return api.createRange(this); 2453 }; 2454 } 2455 doc = win = null; 2456 }); 2457});rangy.createModule("WrappedSelection", function(api, module) { 2458 // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range 2459 // spec (http://html5.org/specs/dom-range.html) 2460 2461 api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] ); 2462 2463 api.config.checkSelectionRanges = true; 2464 2465 var BOOLEAN = "boolean", 2466 windowPropertyName = "_rangySelection", 2467 dom = api.dom, 2468 util = api.util, 2469 DomRange = api.DomRange, 2470 WrappedRange = api.WrappedRange, 2471 DOMException = api.DOMException, 2472 DomPosition = dom.DomPosition, 2473 getSelection, 2474 selectionIsCollapsed, 2475 CONTROL = "Control"; 2476 2477 2478 2479 function getWinSelection(winParam) { 2480 return (winParam || window).getSelection(); 2481 } 2482 2483 function getDocSelection(winParam) { 2484 return (winParam || window).document.selection; 2485 } 2486 2487 // Test for the Range/TextRange and Selection features required 2488 // Test for ability to retrieve selection 2489 var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"), 2490 implementsDocSelection = api.util.isHostObject(document, "selection"); 2491 2492 var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange); 2493 2494 if (useDocumentSelection) { 2495 getSelection = getDocSelection; 2496 api.isSelectionValid = function(winParam) { 2497 var doc = (winParam || window).document, nativeSel = doc.selection; 2498 2499 // Check whether the selection TextRange is actually contained within the correct document 2500 return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc); 2501 }; 2502 } else if (implementsWinGetSelection) { 2503 getSelection = getWinSelection; 2504 api.isSelectionValid = function() { 2505 return true; 2506 }; 2507 } else { 2508 module.fail("Neither document.selection or window.getSelection() detected."); 2509 } 2510 2511 api.getNativeSelection = getSelection; 2512 2513 var testSelection = getSelection(); 2514 var testRange = api.createNativeRange(document); 2515 var body = dom.getBody(document); 2516 2517 // Obtaining a range from a selection 2518 var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focu
2518sNode"] && 2519 util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"])); 2520 api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus; 2521 2522 // Test for existence of native selection extend() method 2523 var selectionHasExtend = util.isHostMethod(testSelection, "extend"); 2524 api.features.selectionHasExtend = selectionHasExtend; 2525 2526 // Test if rangeCount exists 2527 var selectionHasRangeCount = (typeof testSelection.rangeCount == "number"); 2528 api.features.selectionHasRangeCount = selectionHasRangeCount; 2529 2530 var selectionSupportsMultipleRanges = false; 2531 var collapsedNonEditableSelectionsSupported = true; 2532 2533 if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) && 2534 typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) { 2535 2536 (function() { 2537 var iframe = document.createElement("iframe"); 2538 body.appendChild(iframe); 2539 2540 var iframeDoc = dom.getIframeDocument(iframe); 2541 iframeDoc.open(); 2542 iframeDoc.write("<html><head></head><body>12</body></html>"); 2543 iframeDoc.close(); 2544 2545 var sel = dom.getIframeWindow(iframe).getSelection(); 2546 var docEl = iframeDoc.documentElement; 2547 var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild; 2548 2549 // Test whether the native selection will allow a collapsed selection within a non-editable element 2550 var r1 = iframeDoc.createRange(); 2551 r1.setStart(textNode, 1); 2552 r1.collapse(true); 2553 sel.addRange(r1); 2554 collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1); 2555 sel.removeAllRanges(); 2556 2557 // Test whether the native selection is capable of supporting multiple ranges 2558 var r2 = r1.cloneRange(); 2559 r1.setStart(textNode, 0); 2560 r2.setEnd(textNode, 2); 2561 sel.addRange(r1); 2562 sel.addRange(r2); 2563 2564 selectionSupportsMultipleRanges = (sel.rangeCount == 2); 2565 2566 // Clean up 2567 r1.detach(); 2568 r2.detach(); 2569 2570 body.removeChild(iframe); 2571 })(); 2572 } 2573 2574 api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
vendor: 7,635 bytes, lines 2575-2763
2575 api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported; 2576 2577 // ControlRanges 2578 var implementsControlRange = false, testControlRange; 2579 2580 if (body && util.isHostMethod(body, "createControlRange")) { 2581 testControlRange = body.createControlRange(); 2582 if (util.areHostProperties(testControlRange, ["item", "add"])) { 2583 implementsControlRange = true; 2584 } 2585 } 2586 api.features.implementsControlRange = implementsControlRange; 2587 2588 // Selection collapsedness 2589 if (selectionHasAnchorAndFocus) { 2590 selectionIsCollapsed = function(sel) { 2591 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset; 2592 }; 2593 } else { 2594 selectionIsCollapsed = function(sel) { 2595 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false; 2596 }; 2597 } 2598 2599 function updateAnchorAndFocusFromRange(sel, range, backwards) { 2600 var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end"; 2601 sel.anchorNode = range[anchorPrefix + "Container"]; 2602 sel.anchorOffset = range[anchorPrefix + "Offset"]; 2603 sel.focusNode = range[focusPrefix + "Container"]; 2604 sel.focusOffset = range[focusPrefix + "Offset"]; 2605 } 2606 2607 function updateAnchorAndFocusFromNativeSelection(sel) { 2608 var nativeSel = sel.nativeSelection; 2609 sel.anchorNode = nativeSel.anchorNode; 2610 sel.anchorOffset = nativeSel.anchorOffset; 2611 sel.focusNode = nativeSel.focusNode; 2612 sel.focusOffset = nativeSel.focusOffset; 2613 } 2614 2615 function updateEmptySelection(sel) { 2616 sel.anchorNode = sel.focusNode = null; 2617 sel.anchorOffset = sel.focusOffset = 0; 2618 sel.rangeCount = 0; 2619 sel.isCollapsed = true; 2620 sel._ranges.length = 0; 2621 } 2622 2623 function getNativeRange(range) { 2624 var nativeRange; 2625 if (range instanceof DomRange) { 2626 nativeRange = range._selectionNativeRange; 2627 if (!nativeRange) { 2628 nativeRange = api.createNativeRange(dom.getDocument(range.startContainer)); 2629 nativeRange.setEnd(range.endContainer, range.endOffset); 2630 nativeRange.setStart(range.startContainer, range.startOffset); 2631 range._selectionNativeRange = nativeRange; 2632 range.attachListener("detach", function() { 2633 2634 this._selectionNativeRange = null; 2635 }); 2636 } 2637 } else if (range instanceof WrappedRange) { 2638 nativeRange = range.nativeRange; 2639 } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) { 2640 nativeRange = range; 2641 } 2642 return nativeRange; 2643 } 2644 2645 function rangeContainsSingleElement(rangeNodes) { 2646 if (!rangeNodes.length || rangeNodes[0].nodeType != 1) { 2647 return false; 2648 } 2649 for (var i = 1, len = rangeNodes.length; i < len; ++i) { 2650 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) { 2651 return false; 2652 } 2653 } 2654 return true; 2655 } 2656 2657 function getSingleElementFromRange(range) { 2658 var nodes = range.getNodes(); 2659 if (!rangeContainsSingleElement(nodes)) { 2660 throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element"); 2661 } 2662 return nodes[0]; 2663 } 2664 2665 function isTextRange(range) { 2666 return !!range && typeof range.text != "undefined"; 2667 } 2668 2669 function updateFromTextRange(sel, range) { 2670 // Create a Range from the selected TextRange 2671 var wrappedRange = new WrappedRange(range); 2672 sel._ranges = [wrappedRange]; 2673 2674 updateAnchorAndFocusFromRange(sel, wrappedRange, false); 2675 sel.rangeCount = 1; 2676 sel.isCollapsed = wrappedRange.collapsed; 2677 } 2678 2679 function updateControlSelection(sel) { 2680 // Update the wrapped selection based on what's now in the native selection 2681 sel._ranges.length = 0; 2682 if (sel.docSelection.type == "None") { 2683 updateEmptySelection(sel); 2684 } else { 2685 var controlRange = sel.docSelection.createRange(); 2686 if (isTextRange(controlRange)) { 2687 // This case (where the selection type is "Control" and calling createRange() on the selection returns 2688 // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected 2689 // ControlRange have been removed from the ControlRange and removed from the document. 2690 updateFromTextRange(sel, controlRange); 2691 } else { 2692 sel.rangeCount = controlRange.length; 2693 var range, doc = dom.getDocument(controlRange.item(0)); 2694 for (var i = 0; i < sel.rangeCount; ++i) { 2695 range = api.createRange(doc); 2696 range.selectNode(controlRange.item(i)); 2697 sel._ranges.push(range); 2698 } 2699 sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed; 2700 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false); 2701 } 2702 } 2703 } 2704 2705 function addRangeToControlSelection(sel, range) { 2706 var controlRange = sel.docSelection.createRange(); 2707 var rangeElement = getSingleElementFromRange(range); 2708 2709 // Create a new ControlRange containing all the elements in the selected ControlRange plus the element 2710 // contained by the supplied range 2711 var doc = dom.getDocument(controlRange.item(0)); 2712 var newControlRange = dom.getBody(doc).createControlRange(); 2713 for (var i = 0, len = controlRange.length; i < len; ++i) { 2714 newControlRange.add(controlRange.item(i)); 2715 } 2716 try { 2717 newControlRange.add(rangeElement); 2718 } catch (ex) { 2719 throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)"); 2720 } 2721 newControlRange.select(); 2722 2723 // Update the wrapped selection based on what's now in the native selection 2724 updateControlSelection(sel); 2725 } 2726 2727 var getSelectionRangeAt; 2728 2729 if (util.isHostMethod(testSelection, "getRangeAt")) { 2730 getSelectionRangeAt = function(sel, index) { 2731 try { 2732 return sel.getRangeAt(index); 2733 } catch(ex) { 2734 return null; 2735 } 2736 }; 2737 } else if (selectionHasAnchorAndFocus) { 2738 getSelectionRangeAt = function(sel) { 2739 var doc = dom.getDocument(sel.anchorNode); 2740 var range = api.createRange(doc); 2741 range.setStart(sel.anchorNode, sel.anchorOffset); 2742 range.setEnd(sel.focusNode, sel.focusOffset); 2743 2744 // Handle the case when the selection was selected backwards (from the end to the start in the 2745 // document) 2746 if (range.collapsed !== this.isCollapsed) { 2747 range.setStart(sel.focusNode, sel.focusOffset); 2748 range.setEnd(sel.anchorNode, sel.anchorOffset); 2749 } 2750 2751 return range; 2752 }; 2753 } 2754 2755 /** 2756 * @constructor 2757 */ 2758 function WrappedSelection(selection, docSelection, win) { 2759 this.nativeSelection = selection; 2760 this.docSelection = docSelection; 2761 this._ranges = []; 2762 this.win = win; 2763 this.refresh();
2764 } 2765 2766 api.getSelection = function(win) { 2767 win = win || window; 2768 var sel = win[windowPropertyName]; 2769 var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null; 2770 if (sel) { 2771 sel.nativeSelection = nativeSel; 2772 sel.docSelection = docSel; 2773 sel.refresh(win); 2774 } else { 2775 sel = new WrappedSelection(nativeSel, docSel, win); 2776 win[windowPropertyName] = sel; 2777 } 2778 return sel; 2779 }; 2780 2781 api.getIframeSelection = function(iframeEl) { 2782 return api.getSelection(dom.getIframeWindow(iframeEl)); 2783 }; 2784 2785 var selProto = WrappedSelection.prototype; 2786 2787 function createControlSelection(sel, ranges) { 2788 // Ensure that the selection becomes of type "Control" 2789 var doc = dom.getDocument(ranges[0].startContainer); 2790 var controlRange = dom.getBody(doc).createControlRange(); 2791 for (var i = 0, el; i < rangeCount; ++i) { 2792 el = getSingleElementFromRange(ranges[i]); 2793 try { 2794 controlRange.add(el); 2795 } catch (ex) { 2796 throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)"); 2797 } 2798 } 2799 controlRange.select(); 2800 2801 // Update the wrapped selection based on what's now in the native selection 2802 updateControlSelection(sel); 2803 } 2804 2805 // Selecting a range 2806 if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) { 2807 selProto.removeAllRanges = function() { 2808 this.nativeSelection.removeAllRanges(); 2809 updateEmptySelection(this); 2810 }; 2811 2812 var addRangeBackwards = function(sel, range) { 2813 var doc = DomRange.getRangeDocument(range); 2814 var endRange = api.createRange(doc); 2815 endRange.collapseToPoint(range.endContainer, range.endOffset); 2816 sel.nativeSelection.addRange(getNativeRange(endRange)); 2817 sel.nativeSelection.extend(range.startContainer, range.startOffset); 2818 sel.refresh(); 2819 }; 2820 2821 if (selectionHasRangeCount) { 2822 selProto.addRange = function(range, backwards) { 2823 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { 2824 addRangeToControlSelection(this, range); 2825 } else { 2826 if (backwards && selectionHasExtend) { 2827 addRangeBackwards(this, range); 2828 } else { 2829 var previousRangeCount; 2830 if (selectionSupportsMultipleRanges) { 2831 previousRangeCount = this.rangeCount; 2832 } else { 2833 this.removeAllRanges(); 2834 previousRangeCount = 0; 2835 } 2836 this.nativeSelection.addRange(getNativeRange(range)); 2837 2838 // Check whether adding the range was successful 2839 this.rangeCount = this.nativeSelection.rangeCount; 2840 2841 if (this.rangeCount == previousRangeCount + 1) { 2842 // The range was added successfully 2843 2844 // Check whether the range that we added to the selection is reflected in the last range extracted from 2845 // the selection 2846 if (api.config.checkSelectionRanges) { 2847 var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1); 2848 if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) { 2849 // Happens in WebKit with, for example, a selection placed at the start of a text node 2850 range = new WrappedRange(nativeRange); 2851 } 2852 } 2853 this._ranges[this.rangeCount - 1] = range; 2854 updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection)); 2855 this.isCollapsed = selectionIsCollapsed(this); 2856 } else { 2857 // The range was not added successfully. The simplest thing is to refresh 2858 this.refresh();
vendor: 10,254 bytes, lines 2859-3114
2859 } 2860 } 2861 } 2862 }; 2863 } else { 2864 selProto.addRange = function(range, backwards) { 2865 if (backwards && selectionHasExtend) { 2866 addRangeBackwards(this, range); 2867 } else { 2868 this.nativeSelection.addRange(getNativeRange(range)); 2869 this.refresh(); 2870 } 2871 }; 2872 } 2873 2874 selProto.setRanges = function(ranges) { 2875 if (implementsControlRange && ranges.length > 1) { 2876 createControlSelection(this, ranges); 2877 } else { 2878 this.removeAllRanges(); 2879 for (var i = 0, len = ranges.length; i < len; ++i) { 2880 this.addRange(ranges[i]); 2881 } 2882 } 2883 }; 2884 } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") && 2885 implementsControlRange && useDocumentSelection) { 2886 2887 selProto.removeAllRanges = function() { 2888 // Added try/catch as fix for issue #21 2889 try { 2890 this.docSelection.empty(); 2891 2892 // Check for empty() not working (issue #24) 2893 if (this.docSelection.type != "None") { 2894 // Work around failure to empty a control selection by instead selecting a TextRange and then 2895 // calling empty() 2896 var doc; 2897 if (this.anchorNode) { 2898 doc = dom.getDocument(this.anchorNode); 2899 } else if (this.docSelection.type == CONTROL) { 2900 var controlRange = this.docSelection.createRange(); 2901 if (controlRange.length) { 2902 doc = dom.getDocument(controlRange.item(0)).body.createTextRange(); 2903 } 2904 } 2905 if (doc) { 2906 var textRange = doc.body.createTextRange(); 2907 textRange.select(); 2908 this.docSelection.empty(); 2909 } 2910 } 2911 } catch(ex) {} 2912 updateEmptySelection(this); 2913 }; 2914 2915 selProto.addRange = function(range) { 2916 if (this.docSelection.type == CONTROL) { 2917 addRangeToControlSelection(this, range); 2918 } else { 2919 WrappedRange.rangeToTextRange(range).select(); 2920 this._ranges[0] = range; 2921 this.rangeCount = 1; 2922 this.isCollapsed = this._ranges[0].collapsed; 2923 updateAnchorAndFocusFromRange(this, range, false); 2924 } 2925 }; 2926 2927 selProto.setRanges = function(ranges) { 2928 this.removeAllRanges(); 2929 var rangeCount = ranges.length; 2930 if (rangeCount > 1) { 2931 createControlSelection(this, ranges); 2932 } else if (rangeCount) { 2933 this.addRange(ranges[0]); 2934 } 2935 }; 2936 } else { 2937 module.fail("No means of selecting a Range or TextRange was found"); 2938 return false; 2939 } 2940 2941 selProto.getRangeAt = function(index) { 2942 if (index < 0 || index >= this.rangeCount) { 2943 throw new DOMException("INDEX_SIZE_ERR"); 2944 } else { 2945 return this._ranges[index]; 2946 } 2947 }; 2948 2949 var refreshSelection; 2950 2951 if (useDocumentSelection) { 2952 refreshSelection = function(sel) { 2953 var range; 2954 if (api.isSelectionValid(sel.win)) { 2955 range = sel.docSelection.createRange(); 2956 } else { 2957 range = dom.getBody(sel.win.document).createTextRange(); 2958 range.collapse(true); 2959 } 2960 2961 2962 if (sel.docSelection.type == CONTROL) { 2963 updateControlSelection(sel); 2964 } else if (isTextRange(range)) { 2965 updateFromTextRange(sel, range); 2966 } else { 2967 updateEmptySelection(sel); 2968 } 2969 }; 2970 } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") { 2971 refreshSelection = function(sel) { 2972 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) { 2973 updateControlSelection(sel); 2974 } else { 2975 sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount; 2976 if (sel.rangeCount) { 2977 for (var i = 0, len = sel.rangeCount; i < len; ++i) { 2978 sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i)); 2979 } 2980 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection)); 2981 sel.isCollapsed = selectionIsCollapsed(sel); 2982 } else { 2983 updateEmptySelection(sel); 2984 } 2985 } 2986 }; 2987 } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) { 2988 refreshSelection = function(sel) { 2989 var range, nativeSel = sel.nativeSelection; 2990 if (nativeSel.anchorNode) { 2991 range = getSelectionRangeAt(nativeSel, 0); 2992 sel._ranges = [range]; 2993 sel.rangeCount = 1; 2994 updateAnchorAndFocusFromNativeSelection(sel); 2995 sel.isCollapsed = selectionIsCollapsed(sel); 2996 } else { 2997 updateEmptySelection(sel); 2998 } 2999 }; 3000 } else { 3001 module.fail("No means of obtaining a Range or TextRange from the user's selection was found"); 3002 return false; 3003 } 3004 3005 selProto.refresh = function(checkForChanges) { 3006 var oldRanges = checkForChanges ? this._ranges.slice(0) : null; 3007 refreshSelection(this); 3008 if (checkForChanges) { 3009 var i = oldRanges.length; 3010 if (i != this._ranges.length) { 3011 return false; 3012 } 3013 while (i--) { 3014 if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) { 3015 return false; 3016 } 3017 } 3018 return true; 3019 } 3020 }; 3021 3022 // Removal of a single range 3023 var removeRangeManually = function(sel, range) { 3024 var ranges = sel.getAllRanges(), removed = false; 3025 sel.removeAllRanges(); 3026 for (var i = 0, len = ranges.length; i < len; ++i) { 3027 if (removed || range !== ranges[i]) { 3028 sel.addRange(ranges[i]); 3029 } else { 3030 // According to the draft WHATWG Range spec, the same range may be added to the selection multiple 3031 // times. removeRange should only remove the first instance, so the following ensures only the first 3032 // instance is removed 3033 removed = true; 3034 } 3035 } 3036 if (!sel.rangeCount) { 3037 updateEmptySelection(sel); 3038 } 3039 }; 3040 3041 if (implementsControlRange) { 3042 selProto.removeRange = function(range) { 3043 if (this.docSelection.type == CONTROL) { 3044 var controlRange = this.docSelection.createRange(); 3045 var rangeElement = getSingleElementFromRange(range); 3046 3047 // Create a new ControlRange containing all the elements in the selected ControlRange minus the 3048 // element contained by the supplied range 3049 var doc = dom.getDocument(controlRange.item(0)); 3050 var newControlRange = dom.getBody(doc).createControlRange(); 3051 var el, removed = false; 3052 for (var i = 0, len = controlRange.length; i < len; ++i) { 3053 el = controlRange.item(i); 3054 if (el !== rangeElement || removed) { 3055 newControlRange.add(controlRange.item(i)); 3056 } else { 3057 removed = true; 3058 } 3059 } 3060 newControlRange.select(); 3061 3062 // Update the wrapped selection based on what's now in the native selection 3063 updateControlSelection(this); 3064 } else { 3065 removeRangeManually(this, range); 3066 } 3067 }; 3068 } else { 3069 selProto.removeRange = function(range) { 3070 removeRangeManually(this, range); 3071 }; 3072 } 3073 3074 // Detecting if a selection is backwards 3075 var selectionIsBackwards; 3076 if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) { 3077 selectionIsBackwards = function(sel) { 3078 var backwards = false; 3079 if (sel.anchorNode) { 3080 backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1); 3081 } 3082 return backwards; 3083 }; 3084 3085 selProto.isBackwards = function() { 3086 return selectionIsBackwards(this); 3087 }; 3088 } else { 3089 selectionIsBackwards = selProto.isBackwards = function() { 3090 return false; 3091 }; 3092 } 3093 3094 // Selection text 3095 // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation 3096 selProto.toString = function() { 3097 3098 var rangeTexts = []; 3099 for (var i = 0, len = this.rangeCount; i < len; ++i) { 3100 rangeTexts[i] = "" + this._ranges[i]; 3101 } 3102 return rangeTexts.join(""); 3103 }; 3104 3105 function assertNodeInSameDocument(sel, node) { 3106 if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) { 3107 throw new DOMException("WRONG_DOCUMENT_ERR"); 3108 } 3109 } 3110 3111 // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used 3112 selProto.collapse = function(node, offset) { 3113 assertNodeInSameDocument(this, node); 3114 var range = api.createRange(dom.getDocument(node));
3115 range.collapseToPoint(node, offset); 3116 this.removeAllRanges(); 3117 this.addRange(range); 3118 this.isCollapsed = true; 3119 }; 3120 3121 selProto.collapseToStart = function() { 3122 if (this.rangeCount) { 3123 var range = this._ranges[0]; 3124 this.collapse(range.startContainer, range.startOffset); 3125 } else { 3126 throw new DOMException("INVALID_STATE_ERR"); 3127 } 3128 }; 3129 3130 selProto.collapseToEnd = function() { 3131 if (this.rangeCount) { 3132 var range = this._ranges[this.rangeCount - 1]; 3133 this.collapse(range.endContainer, range.endOffset); 3134 } else { 3135 throw new DOMException("INVALID_STATE_ERR"); 3136 } 3137 }; 3138 3139 // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is 3140 // never used by Rangy. 3141 selProto.selectAllChildren = function(node) { 3142 assertNodeInSameDocument(this, node); 3143 var range = api.createRange(dom.getDocument(node)); 3144 range.selectNodeContents(node); 3145 this.removeAllRanges(); 3146 this.addRange(range); 3147 }; 3148 3149 selProto.deleteFromDocument = function() { 3150 // Sepcial behaviour required for Control selections 3151 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { 3152 var controlRange = this.docSelection.createRange(); 3153 var element; 3154 while (controlRange.length) { 3155 element = controlRange.item(0); 3156 controlRange.remove(element); 3157 element.parentNode.removeChild(element); 3158 } 3159 this.refresh(); 3160 } else if (this.rangeCount) { 3161 var ranges = this.getAllRanges(); 3162 this.removeAllRanges(); 3163 for (var i = 0, len = ranges.length; i < len; ++i) { 3164 ranges[i].deleteContents(); 3165 } 3166 // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each 3167 // range. Firefox moves the selection to where the final selected range was, so we emulate that 3168 this.addRange(ranges[len - 1]); 3169 } 3170 }; 3171 3172 // The following are non-standard extensions 3173 selProto.getAllRanges = function() { 3174 return this._ranges.slice(0); 3175 }; 3176 3177 selProto.setSingleRange = function(range) { 3178 this.setRanges( [range] ); 3179 }; 3180 3181 selProto.containsNode = function(node, allowPartial) { 3182 for (var i = 0, len = this._ranges.length; i < len; ++i) { 3183 if (this._ranges[i].containsNode(node, allowPartial)) { 3184 return true; 3185 } 3186 } 3187 return false; 3188 }; 3189 3190 selProto.toHtml = function() { 3191 var html = ""; 3192 if (this.rangeCount) { 3193 var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div"); 3194 for (var i = 0, len = this._ranges.length; i < len; ++i) { 3195 container.appendChild(this._ranges[i].cloneContents()); 3196 } 3197 html = container.innerHTML; 3198 } 3199 return html; 3200 }; 3201 3202 function inspect(sel) { 3203 var rangeInspects = []; 3204 var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset); 3205 var focus = new DomPosition(sel.focusNode, sel.focusOffset); 3206 var name = (typeof sel.getName == "function") ? sel.getName() : "Selection"; 3207 3208 if (typeof sel.rangeCount != "undefined") { 3209 for (var i = 0, len = sel.rangeCount; i < len; ++i) { 3210 rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i)); 3211 } 3212 } 3213 return "[" + name + "(Ranges: " + rangeInspects.join(", ") + 3214 ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]"; 3215 3216 } 3217 3218 selProto.getName = function() { 3219 return "WrappedSelection"; 3220 }; 3221 3222 selProto.inspect = function() { 3223 return inspect(this); 3224 }; 3225 3226 selProto.detach = function() { 3227 this.win[windowPropertyName] = null; 3228 this.win = this.anchorNode = this.focusNode = null; 3229 }; 3230 3231 WrappedSelection.inspect = inspect; 3232 3233 api.Selection = WrappedSelection; 3234 3235 api.selectionPrototype = selProto; 3236 3237 api.addCreateMissingNativeApiListener(function(win) { 3238 if (typeof win.getSelection == "undefined") { 3239 win.getSelection = function() { 3240 return api.getSelection(this); 3241 }; 3242 } 3243 win = null; 3244 });
3245}); 3246/* 3247 Base.js, version 1.1a 3248 Copyright 2006-2010, Dean Edwards 3249 License: http://www.opensource.org/licenses/mit-license.php 3250*/ 3251 3252var Base = function() { 3253 // dummy 3254}; 3255 3256Base.extend = function(_instance, _static) { // subclass 3257 var extend = Base.prototype.extend; 3258 3259 // build the prototype 3260 Base._prototyping = true; 3261 var proto = new this; 3262 extend.call(proto, _instance); 3263 proto.base = function() { 3264 // call this method from any other method to invoke that method's ancestor 3265 }; 3266 delete Base._prototyping; 3267 3268 // create the wrapper for the constructor function 3269 //var constructor = proto.constructor.valueOf(); //-dean 3270 var constructor = proto.constructor; 3271 var klass = proto.constructor = function() { 3272 if (!Base._prototyping) { 3273 if (this._constructing || this.constructor == klass) { // instantiation 3274 this._constructing = true; 3275 constructor.apply(this, arguments); 3276 delete this._constructing; 3277 } else if (arguments[0] != null) { // casting 3278 return (arguments[0].extend || extend).call(arguments[0], proto); 3279 } 3280 } 3281 }; 3282 3283 // build the class interface 3284 klass.ancestor = this; 3285 klass.extend = this.extend; 3286 klass.forEach = this.forEach; 3287 klass.implement = this.implement; 3288 klass.prototype = proto; 3289 klass.toString = this.toString; 3290 klass.valueOf = function(type) { 3291 //return (type == "object") ? klass : constructor; //-dean 3292 return (type == "object") ? klass : constructor.valueOf(); 3293 }; 3294 extend.call(klass, _static); 3295 // class initialisation 3296 if (typeof klass.init == "function") klass.init(); 3297 return klass; 3298}; 3299 3300Base.prototype = { 3301 extend: function(source, value) { 3302 if (arguments.length > 1) { // extending with a name/value pair 3303 var ancestor = this[source]; 3304 if (ancestor && (typeof value == "function") && // overriding a method? 3305 // the valueOf() comparison is to avoid circular references 3306 (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) && 3307 /\bbase\b/.test(value)) { 3308 // get the underlying method 3309 var method = value.valueOf(); 3310 // override 3311 value = function() { 3312 var previous = this.base || Base.prototype.base; 3313 this.base = ancestor; 3314 var returnValue = method.apply(this, arguments); 3315 this.base = previous; 3316 return returnValue; 3317 }; 3318 // point to the underlying method 3319 value.valueOf = function(type) { 3320 return (type == "object") ? value : method; 3321 }; 3322 value.toString = Base.toString; 3323 } 3324 this[source] = value; 3325 } else if (source) { // extending with an object literal 3326 var extend = Base.prototype.extend; 3327 // if this object has a customised extend method then use it 3328 if (!Base._prototyping && typeof this != "function") { 3329 extend = this.extend || extend; 3330 } 3331 var proto = {toSource: null}; 3332 // do the "toString" and other methods manually 3333 var hidden = ["constructor", "toString", "valueOf"]; 3334 // if we are prototyping then include the constructor 3335 var i = Base._prototyping ? 0 : 1; 3336 while (key = hidden[i++]) { 3337 if (source[key] != proto[key]) { 3338 extend.call(this, key, source[key]); 3339 3340 } 3341 } 3342 // copy each of the source object's properties to this object 3343 for (var key in source) { 3344 if (!proto[key]) extend.call(this, key, source[key]); 3345 } 3346 } 3347 return this; 3348 } 3349}; 3350 3351// initialise 3352Base = Base.extend({ 3353 constructor: function() { 3354 this.extend(arguments[0]); 3355 } 3356}, { 3357 ancestor: Object, 3358 version: "1.1", 3359 3360 forEach: function(object, block, context) { 3361 for (var key in object) { 3362 if (this.prototype[key] === undefined) { 3363 block.call(context, object[key], key, object); 3364 } 3365 } 3366 }, 3367 3368 implement: function() { 3369 for (var i = 0; i < arguments.length; i++) { 3370 if (typeof arguments[i] == "function") { 3371 // if it's a function, call it 3372 arguments[i](this.prototype); 3373 } else { 3374 // add the interface using the extend method 3375 this.prototype.extend(arguments[i]); 3376 } 3377 } 3378 return this; 3379 }, 3380 3381 toString: function() { 3382 return String(this.valueOf()); 3383 } 3384});/** 3385 * Detect browser support for specific features 3386 */ 3387wysihtml5.browser = (function() { 3388 var userAgent = navigator.userAgent, 3389 testElement = document.createElement("div"), 3390 // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect 3391 isIE = userAgent.indexOf("MSIE") !== -1 && userAgent.indexOf("Opera") === -1, 3392 isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1, 3393 isWebKit = userAgent.indexOf("AppleWebKit/") !== -1, 3394 isChrome = userAgent.indexOf("Chrome/") !== -1, 3395 isOpera = userAgent.indexOf("Opera/") !== -1; 3396 3397 function iosVersion(userAgent) { 3398 return ((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [, 0])[1]; 3399 } 3400 3401 return { 3402 // Static variable needed, publicly accessible, to be able override it in unit tests 3403 USER_AGENT: userAgent, 3404 3405 /** 3406 * Exclude browsers that are not capable of displaying and handling
3407 * contentEditable as desired: 3408 * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable 3409 * - IE < 8 create invalid markup and crash randomly from time to time 3410 * 3411 * @return {Boolean} 3412 */ 3413 supported: function() { 3414 var userAgent = this.USER_AGENT.toLowerCase(), 3415 // Essential for making html elements editable 3416 hasContentEditableSupport = "contentEditable" in testElement, 3417 // Following methods are needed in order to interact with the contentEditable area 3418 hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState, 3419 // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+ 3420 hasQuerySelectorSupport = document.querySelector && document.querySelectorAll, 3421 // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05) 3422 isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1; 3423 3424 return hasContentEditableSupport 3425 && hasEditingApiSupport 3426 && hasQuerySelectorSupport 3427 && !isIncompatibleMobileBrowser; 3428 }, 3429 3430 isTouchDevice: function() { 3431 return this.supportsEvent("touchmove"); 3432 }, 3433 3434 isIos: function() { 3435 var userAgent = this.USER_AGENT.toLowerCase(); 3436 return userAgent.indexOf("webkit") !== -1 && userAgent.indexOf("mobile") !== -1; 3437 }, 3438 3439 /** 3440 * Whether the browser supports sandboxed iframes 3441 * Currently only IE 6+ offers such feature <iframe security="restricted"> 3442 * 3443 * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx 3444 * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx 3445 * 3446 * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage) 3447 */ 3448 supportsSandboxedIframes: function() { 3449 return isIE; 3450 }, 3451 3452 /** 3453 * IE6+7 throw a mixed content warning when the src of an iframe 3454 * is empty/unset or about:blank 3455 * window.querySelector is implemented as of IE8 3456 */ 3457 throwsMixedContentWarningWhenIframeSrcIsEmpty: function() { 3458 return !("querySelector" in document); 3459 }, 3460 3461 /** 3462 * Whether the caret is correctly displayed in contentEditable elements 3463 * Firefox sometimes shows a huge caret in the beginning after focusing 3464 */ 3465 displaysCaretInEmptyContentEditableCorrectly: function() { 3466 return !isGecko; 3467 }, 3468 3469 /** 3470 * Opera and IE are the only browsers who offer the css value 3471 * in the original unit, thx to the currentStyle object 3472 * All other browsers provide the computed style in px via window.getComputedStyle 3473 */ 3474 hasCurrentStyleProperty: function() { 3475 return "currentStyle" in testElement; 3476 }, 3477 3478 /** 3479 * Whether the browser inserts a <br> when pressing enter in a contentEditable element 3480 */ 3481 insertsLineBreaksOnReturn: function() { 3482 return isGecko; 3483 }, 3484 3485 supportsPlaceholderAttributeOn: function(element) { 3486 return "placeholder" in element; 3487 }, 3488 3489 supportsEvent: function(eventName) { 3490 return "on" + eventName in testElement || (function() { 3491 testElement.setAttribute("on" + eventName, "return;"); 3492 return typeof(testElement["on" + eventName]) === "function"; 3493 })(); 3494 }, 3495 3496 /** 3497 * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe 3498 */ 3499 supportsEventsInIframeCorrectly: function() { 3500 return !isOpera; 3501 }, 3502 3503 /** 3504 * Chrome & Safari only fire the ondrop/ondragend/... events when the ondragover event is cancelled 3505 * with event.preventDefault 3506 * Firefox 3.6 fires those events anyway, but the mozilla doc says that the dragover/dragenter event needs 3507 * to be cancelled 3508 */ 3509 firesOnDropOnlyWhenOnDragOverIsCancelled: function() { 3510 return isWebKit || isGecko; 3511 }, 3512 3513 /** 3514 * Whether the browser supports the event.dataTransfer property in a proper way 3515 */
3516 supportsDataTransfer: function() { 3517 try { 3518 // Firefox doesn't support dataTransfer in a safe way, it doesn't strip script code in the html payload (like Chrome does) 3519 return isWebKit && (window.Clipboard || window.DataTransfer).prototype.getData; 3520 } catch(e) { 3521 return false; 3522 } 3523 }, 3524 3525 /** 3526 * Everything below IE9 doesn't know how to treat HTML5 tags 3527 * 3528 * @param {Object} context The document object on which to check HTML5 support 3529 * 3530 * @example 3531 * wysihtml5.browser.supportsHTML5Tags(document); 3532 */ 3533 supportsHTML5Tags: function(context) { 3534 var element = context.createElement("div"), 3535 html5 = "<article>foo</article>"; 3536 element.innerHTML = html5; 3537 return element.innerHTML.toLowerCase() === html5; 3538 }, 3539 3540 /** 3541 * Checks whether a document supports a certain queryCommand 3542 * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree 3543 * in oder to report correct results 3544 * 3545 * @param {Object} doc Document object on which to check for a query command 3546 * @param {String} command The query command to check for 3547 * @return {Boolean} 3548 * 3549 * @example 3550 * wysihtml5.browser.supportsCommand(document, "bold"); 3551 */ 3552 supportsCommand: (function() { 3553 // Following commands are supported but contain bugs in some browsers 3554 var buggyCommands = { 3555 // formatBlock fails with some tags (eg. <blockquote>) 3556 "formatBlock": isIE, 3557 // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets 3558 // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>) 3559 // IE and Opera act a bit different here as they convert the entire content of the current block element into a list 3560 "insertUnorderedList": isIE || isOpera || isWebKit, 3561 "insertOrderedList": isIE || isOpera || isWebKit 3562 }; 3563 3564 // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands 3565 var supported = { 3566 "insertHTML": isGecko 3567 }; 3568 3569 return function(doc, command) { 3570 var isBuggy = buggyCommands[command]; 3571 if (!isBuggy) { 3572 // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled 3573 try { 3574 return doc.queryCommandSupported(command); 3575 } catch(e1) {} 3576 3577 try { 3578 return doc.queryCommandEnabled(command); 3579 } catch(e2) { 3580 return !!supported[command]; 3581 } 3582 } 3583 return false; 3584 }; 3585 })(), 3586 3587 /** 3588 * IE: URLs starting with: 3589 * www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://, 3590 * nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url: 3591 * will automatically be auto-linked when either the user inserts them via copy&paste or presses the 3592 * space bar when the caret is directly after such an url. 3593 * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll 3594 * (related blog post on msdn 3595 * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx). 3596 */ 3597 doesAutoLinkingInContentEditable: function() { 3598 return isIE; 3599 }, 3600 3601 /** 3602 * As stated above, IE auto links urls typed into contentEditable elements 3603 * Since IE9 it's possible to prevent this behavior 3604 */ 3605 canDisableAutoLinking: function() { 3606 return this.supportsCommand(document, "AutoUrlDetect"); 3607 }, 3608 3609 /** 3610 * IE leaves an empty paragraph in the contentEditable element after clearing it 3611 * Chrome/Safari sometimes an empty <div> 3612 */ 3613 clearsContentEditableCorrectly: function() { 3614 return isGecko || isOpera || isWebKit; 3615 }, 3616 3617 /** 3618 * IE gives wrong results for getAttribute 3619 */ 3620 supportsGetAttributeCorrectly: function() { 3621 var td = document.createElement("td"); 3622 return td.getAttribute("rowspan") != "1"; 3623 }, 3624 3625 /** 3626 * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them. 3627 * Chrome and Safari both don't support this 3628 */ 3629 canSelectImagesInContentEditable: function() { 3630 return isGecko || isIE || isOpera; 3631 }, 3632 3633 /** 3634 * When the caret is in an empty list (<ul><li>|</li></ul>) which is the first child in an contentEditable container 3635 * pressing backspace doesn't remove the entire list as done in other browsers 3636 */ 3637 clearsListsInContentEditableCorrectly: function() { 3638 return isGecko || isIE || isWebKit; 3639 }, 3640 3641 /** 3642 * All browsers except Safari and Chrome automatically scroll the range/caret position into view 3643 */ 3644 autoScrollsToCaret: function() { 3645 return !isWebKit; 3646 }, 3647 3648 /** 3649 * Check whether the browser automatically closes tags that don't need to be opened 3650 */ 3651 autoClosesUnclosedTags: function() { 3652 var clonedTestElement = testElement.cloneNode(false), 3653 returnValue, 3654 innerHTML; 3655 3656 clonedTestElement.innerHTML = "<p><div></div>"; 3657 innerHTML = clonedTestElement.innerHTML.toLowerCase(); 3658 returnValue = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>"; 3659 3660 // Cache result by overwriting current function 3661 this.autoClosesUnclosedTags = function() { return returnValue; }; 3662 3663 return returnValue; 3664 }, 3665 3666 /** 3667 * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists 3668 */ 3669 supportsNativeGetElementsByClassName: function() { 3670 return String(document.getElementsByClassName).indexOf("[native code]") !== -1; 3671 }, 3672 3673 /** 3674 * As of now (19.04.2011) only supported by Firefox 4 and Chrome 3675 * See https://developer.mozilla.org/en/DOM/Selection/modify 3676 */ 3677 supportsSelectionModify: function() { 3678 return "getSelection" in window && "modify" in window.getSelection(); 3679 }, 3680 3681 /** 3682 * Whether the browser supports the classList object for fast className manipulation 3683 * See https://developer.mozilla.org/en/DOM/element.classList 3684 */ 3685 supportsClassList: function() { 3686 return "classList" in testElement; 3687 }, 3688 3689 /** 3690 * Opera needs a white space after a <br> in order to position the caret correctly 3691 */ 3692 needsSpaceAfterLineBreak: function() { 3693 return isOpera; 3694 }, 3695 3696 /** 3697 * Whether the browser supports the speech api on the given element 3698 * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/ 3699 * 3700 * @example 3701 * var input = document.createElement("input"); 3702 * if (wysihtml5.browser.supportsSpeechApiOn(input)) { 3703 * // ... 3704 * } 3705 */ 3706 supportsSpeechApiOn: function(input) { 3707 var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [, 0]; 3708 return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input); 3709 }, 3710 3711 /**
3712 * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest 3713 * See https://connect.microsoft.com/ie/feedback/details/650112 3714 * or try the POC http://tifftiff.de/ie9_crash/ 3715 */ 3716 crashesWhenDefineProperty: function(property) { 3717 return isIE && (property === "XMLHttpRequest" || property === "XDomainRequest"); 3718 }, 3719 3720 /** 3721 * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element 3722 */ 3723 doesAsyncFocus: function() { 3724 return isIE; 3725 }, 3726 3727 /** 3728 * In IE it's impssible for the user and for the selection library to set the caret after an <img> when it's the lastChild in the document 3729 */ 3730 hasProblemsSettingCaretAfterImg: function() { 3731 return isIE; 3732 }, 3733 3734 hasUndoInContextMenu: function() { 3735 return isGecko || isChrome || isOpera; 3736 } 3737 }; 3738})();wysihtml5.lang.array = function(arr) { 3739 return { 3740 /** 3741 * Check whether a given object exists in an array 3742 * 3743 * @example 3744 * wysihtml5.lang.array([1, 2]).contains(1); 3745 * // => true 3746 */ 3747 contains: function(needle) { 3748 if (arr.indexOf) { 3749 return arr.indexOf(needle) !== -1; 3750 } else { 3751 for (var i=0, length=arr.length; i<length; i++) { 3752 if (arr[i] === needle) { return true; } 3753 } 3754 return false; 3755 } 3756 }, 3757 3758 /** 3759 * Substract one array from another 3760 * 3761 * @example 3762 * wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]); 3763 * // => [1, 2] 3764 */ 3765 without: function(arrayToSubstract) { 3766 arrayToSubstract = wysihtml5.lang.array(arrayToSubstract); 3767 var newArr = [], 3768 i = 0, 3769 length = arr.length; 3770 for (; i<length; i++) { 3771 if (!arrayToSubstract.contains(arr[i])) { 3772 newArr.push(arr[i]); 3773 } 3774 } 3775 return newArr; 3776 }, 3777 3778 /** 3779 * Return a clean native array 3780 * 3781 * Following will convert a Live NodeList to a proper Array 3782 * @example 3783 * var childNodes = wysihtml5.lang.array(document.body.childNodes).get(); 3784 */ 3785 get: function() { 3786 var i = 0, 3787 length = arr.length, 3788 newArray = []; 3789 for (; i<length; i++) { 3790 newArray.push(arr[i]); 3791 } 3792 return newArray; 3793 } 3794 }; 3795};wysihtml5.lang.Dispatcher = Base.extend( 3796 /** @scope wysihtml5.lang.Dialog.prototype */ { 3797 observe: function(eventName, handler) { 3798 this.events = this.events || {}; 3799 this.events[eventName] = this.events[eventName] || []; 3800 this.events[eventName].push(handler); 3801 return this; 3802 }, 3803 3804 on: function() { 3805 return this.observe.apply(this, wysihtml5.lang.array(arguments).get()); 3806 }, 3807 3808 fire: function(eventName, payload) { 3809 this.events = this.events || {}; 3810 var handlers = this.events[eventName] || [], 3811 i = 0; 3812 for (; i<handlers.length; i++) { 3813 handlers[i].call(this, payload); 3814 } 3815 return this; 3816 }, 3817 3818 stopObserving: function(eventName, handler) { 3819 this.events = this.events || {}; 3820 var i = 0, 3821 handlers, 3822 newHandlers; 3823 if (eventName) { 3824 handlers = this.events[eventName] || [], 3825 newHandlers = []; 3826 for (; i<handlers.length; i++) { 3827 if (handlers[i] !== handler && handler) { 3828 newHandlers.push(handlers[i]); 3829 } 3830 } 3831 this.events[eventName] = newHandlers; 3832 } else { 3833 // Clean up all events 3834 this.events = {}; 3835 } 3836 return this; 3837 } 3838});wysihtml5.lang.object = function(obj) { 3839 return { 3840 /** 3841 * @example 3842 * wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get(); 3843 * // => { foo: 1, bar: 2, baz: 3 } 3844 */ 3845 merge: function(otherObj) { 3846 for (var i in otherObj) { 3847 obj[i] = otherObj[i]; 3848 } 3849 return this; 3850 }, 3851 3852 get: function() { 3853 return obj; 3854 }, 3855 3856 /** 3857 * @example 3858 * wysihtml5.lang.object({ foo: 1 }).clone(); 3859 * // => { foo: 1 } 3860 */ 3861 clone: function() { 3862 var newObj = {}, 3863 i; 3864 for (i in obj) { 3865 newObj[i] = obj[i]; 3866 } 3867 return newObj; 3868 }, 3869 3870 /** 3871 * @example 3872 * wysihtml5.lang.object([]).isArray(); 3873 * // => true 3874 */ 3875 isArray: function() { 3876 return Object.prototype.toString.call(obj) === "[object Array]"; 3877 } 3878 }; 3879};(function() { 3880 var WHITE_SPACE_START = /^\s+/, 3881 WHITE_SPACE_END = /\s+$/;
3882 wysihtml5.lang.string = function(str) { 3883 str = String(str); 3884 return { 3885 /** 3886 * @example 3887 * wysihtml5.lang.string(" foo ").trim(); 3888 * // => "foo" 3889 */ 3890 trim: function() { 3891 return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, ""); 3892 }, 3893 3894 /** 3895 * @example 3896 * wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" }); 3897 * // => "Hello Christopher" 3898 */ 3899 interpolate: function(vars) { 3900 for (var i in vars) { 3901 str = this.replace("#{" + i + "}").by(vars[i]); 3902 } 3903 return str; 3904 }, 3905 3906 /** 3907 * @example 3908 * wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans"); 3909 * // => "Hello Hans" 3910 */ 3911 replace: function(search) { 3912 return { 3913 by: function(replace) { 3914 return str.split(search).join(replace); 3915 } 3916 } 3917 } 3918 }; 3919 }; 3920})();/** 3921 * Find urls in descendant text nodes of an element and auto-links them 3922 * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/ 3923 * 3924 * @param {Element} element Container element in which to search for urls 3925 * 3926 * @example 3927 * <div id="text-container">Please click here: www.google.com</div> 3928 * <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script> 3929 */ 3930(function(wysihtml5) { 3931 var /** 3932 * Don't auto-link urls that are contained in the following elements: 3933 */ 3934 IGNORE_URLS_IN = wysihtml5.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]), 3935 /** 3936 * revision 1: 3937 * /(\S+\.{1}[^\s\,\.\!]+)/g 3938 * 3939 * revision 2: 3940 * /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim 3941 * 3942 * put this in the beginning if you don't wan't to match within a word 3943 * (^|[\>\(\{\[\s\>]) 3944 */ 3945 URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi, 3946 TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i, 3947 MAX_DISPLAY_LENGTH = 100, 3948 BRACKETS = { ")": "(", "]": "[", "}": "{" }; 3949 3950 function autoLink(element) { 3951 if (_hasParentThatShouldBeIgnored(element)) { 3952 return element; 3953 } 3954 3955 if (element === element.ownerDocument.documentElement) { 3956 element = element.ownerDocument.body; 3957 } 3958 3959 return _parseNode(element); 3960 } 3961 3962 /** 3963 * This is basically a rebuild of 3964 * the rails auto_link_urls text helper 3965 */ 3966 function _convertUrlsToLinks(str) { 3967 return str.replace(URL_REG_EXP, function(match, url) { 3968 var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "", 3969 opening = BRACKETS[punctuation]; 3970 url = url.replace(TRAILING_CHAR_REG_EXP, ""); 3971 3972 if (url.split(opening).length > url.split(punctuation).length) { 3973 url = url + punctuation; 3974 punctuation = ""; 3975 } 3976 var realUrl = url, 3977 displayUrl = url; 3978 if (url.length > MAX_DISPLAY_LENGTH) { 3979 displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "..."; 3980 } 3981 // Add http prefix if necessary 3982 if (realUrl.substr(0, 4) === "www.") { 3983 realUrl = "http://" + realUrl; 3984 } 3985 3986 return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation; 3987 }); 3988 } 3989 3990 /** 3991 * Creates or (if already cached) returns a temp element 3992 * for the given document object 3993 */ 3994 function _getTempElement(context) { 3995 var tempElement = context._wysihtml5_tempElement; 3996 if (!tempElement) { 3997 tempElement = context._wysihtml5_tempElement = context.createElement("div"); 3998 } 3999 return tempElement; 4000 } 4001 4002 /** 4003 * Replaces the original text nodes with the newly auto-linked dom tree 4004 */ 4005 function _wrapMatchesInNode(textNode) { 4006 var parentNode = textNode.parentNode, 4007 tempElement = _getTempElement(parentNode.ownerDocument); 4008 4009 // We need to insert an empty/temporary <span /> to fix IE quirks 4010 // Elsewise IE would strip white space in the beginning 4011 tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(textNode.data); 4012 tempElement.removeChild(tempElement.firstChild); 4013 4014 while (tempElement.firstChild) { 4015 // inserts tempElement.firstChild before textNode 4016 parentNode.insertBefore(tempElement.firstChild, textNode); 4017 } 4018 parentNode.removeChild(textNode); 4019 } 4020 4021 function _hasParentThatShouldBeIgnored(node) { 4022 var nodeName; 4023 while (node.parentNode) { 4024 node = node.parentNode; 4025 nodeName = node.nodeName; 4026 if (IGNORE_URLS_IN.contains(nodeName)) { 4027 return true; 4028 } else if (nodeName === "body") { 4029 return false; 4030 } 4031 } 4032 return false; 4033 } 4034 4035 function _parseNode(element) { 4036 if (IGNORE_URLS_IN.contains(element.nodeName)) { 4037 return; 4038 } 4039 4040 if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) { 4041 _wrapMatchesInNode(element); 4042 return; 4043 } 4044 4045 var childNodes = wysihtml5.lang.array(element.childNodes).get(), 4046 childNodesLength = childNodes.length, 4047 i = 0; 4048 4049 for (; i<childNodesLength; i++) { 4050 _parseNode(childNodes[i]); 4051 } 4052 4053 return element; 4054 } 4055
4056 wysihtml5.dom.autoLink = autoLink; 4057 4058 // Reveal url reg exp to the outside 4059 wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP; 4060})(wysihtml5);(function(wysihtml5) { 4061 var supportsClassList = wysihtml5.browser.supportsClassList(), 4062 api = wysihtml5.dom; 4063 4064 api.addClass = function(element, className) { 4065 if (supportsClassList) { 4066 return element.classList.add(className); 4067 } 4068 if (api.hasClass(element, className)) { 4069 return; 4070 } 4071 element.className += " " + className; 4072 }; 4073 4074 api.removeClass = function(element, className) { 4075 if (supportsClassList) { 4076 return element.classList.remove(className); 4077 } 4078 4079 element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " "); 4080 }; 4081 4082 api.hasClass = function(element, className) { 4083 if (supportsClassList) { 4084 return element.classList.contains(className); 4085 } 4086 4087 var elementClassName = element.className; 4088 return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); 4089 }; 4090})(wysihtml5); 4091wysihtml5.dom.contains = (function() { 4092 var documentElement = document.documentElement; 4093 if (documentElement.contains) { 4094 return function(container, element) { 4095 if (element.nodeType !== wysihtml5.ELEMENT_NODE) { 4096 element = element.parentNode; 4097 } 4098 return container !== element && container.contains(element); 4099 }; 4100 } else if (documentElement.compareDocumentPosition) { 4101 return function(container, element) { 4102 // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition 4103 return !!(container.compareDocumentPosition(element) & 16); 4104 }; 4105 } 4106})();/** 4107 * Converts an HTML fragment/element into a unordered/ordered list 4108 * 4109 * @param {Element} element The element which should be turned into a list 4110 * @param {String} listType The list type in which to convert the tree (either "ul" or "ol") 4111 * @return {Element} The created list 4112 * 4113 * @example 4114 * <!-- Assume the following dom: --> 4115 * <span id="pseudo-list"> 4116 * eminem<br> 4117 * dr. dre 4118 * <div>50 Cent</div> 4119 * </span> 4120 * 4121 * <script> 4122 * wysihtml5.dom.convertToList(document.getElementById("pseudo-list"), "ul"); 4123 * </script> 4124 * 4125 * <!-- Will result in: --> 4126 * <ul> 4127 * <li>eminem</li> 4128 * <li>dr. dre</li> 4129 * <li>50 Cent</li> 4130 * </ul> 4131 */ 4132wysihtml5.dom.convertToList = (function() { 4133 function _createListItem(doc, list) { 4134 var listItem = doc.createElement("li"); 4135 list.appendChild(listItem); 4136 return listItem; 4137 } 4138 4139 function _createList(doc, type) { 4140 return doc.createElement(type); 4141 } 4142 4143 function convertToList(element, listType) { 4144 if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") { 4145 // Already a list 4146 return element; 4147 } 4148 4149 var doc = element.ownerDocument, 4150 list = _createList(doc, listType), 4151 lineBreaks = element.querySelectorAll("br"), 4152 lineBreaksLength = lineBreaks.length, 4153 childNodes, 4154 childNodesLength, 4155 childNode, 4156 lineBreak, 4157 parentNode, 4158 isBlockElement, 4159 isLineBreak, 4160 currentListItem, 4161 i; 4162 4163 // First find <br> at the end of inline elements and move them behind them 4164 for (i=0; i<lineBreaksLength; i++) { 4165 lineBreak = lineBreaks[i]; 4166 while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) { 4167 if (wysihtml5.dom.getStyle("display").from(parentNode) === "block") { 4168 parentNode.removeChild(lineBreak); 4169 break; 4170 } 4171 wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode); 4172 } 4173 } 4174 4175 childNodes = wysihtml5.lang.array(element.childNodes).get(); 4176 childNodesLength = childNodes.length; 4177 4178 for (i=0; i<childNodesLength; i++) { 4179 currentListItem = currentListItem || _createListItem(doc, list); 4180 childNode = childNodes[i];
4181 isBlockElement = wysihtml5.dom.getStyle("display").from(childNode) === "block"; 4182 isLineBreak = childNode.nodeName === "BR"; 4183 4184 if (isBlockElement) { 4185 // Append blockElement to current <li> if empty, otherwise create a new one 4186 currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem; 4187 currentListItem.appendChild(childNode); 4188 currentListItem = null; 4189 continue; 4190 } 4191 4192 if (isLineBreak) { 4193 // Only create a new list item in the next iteration when the current one has already content 4194 currentListItem = currentListItem.firstChild ? null : currentListItem; 4195 continue; 4196 } 4197 4198 currentListItem.appendChild(childNode); 4199 } 4200 4201 element.parentNode.replaceChild(list, element); 4202 return list; 4203 } 4204 4205 return convertToList; 4206})();/** 4207 * Copy a set of attributes from one element to another 4208 * 4209 * @param {Array} attributesToCopy List of attributes which should be copied 4210 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to 4211 * copy the attributes from., this again returns an object which provides a method named "to" which can be invoked 4212 * with the element where to copy the attributes to (see example) 4213 * 4214 * @example 4215 * var textarea = document.querySelector("textarea"), 4216 * div = document.querySelector("div[contenteditable=true]"), 4217 * anotherDiv = document.querySelector("div.preview"); 4218 * wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv); 4219 * 4220 */ 4221wysihtml5.dom.copyAttributes = function(attributesToCopy) { 4222 return { 4223 from: function(elementToCopyFrom) { 4224 return { 4225 to: function(elementToCopyTo) { 4226 var attribute, 4227 i = 0, 4228 length = attributesToCopy.length; 4229 for (; i<length; i++) { 4230 attribute = attributesToCopy[i]; 4231 if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") { 4232 elementToCopyTo[attribute] = elementToCopyFrom[attribute]; 4233 } 4234 } 4235 return { andTo: arguments.callee }; 4236 } 4237 }; 4238 } 4239 }; 4240};/** 4241 * Copy a set of styles from one element to another 4242 * Please note that this only works properly across browsers when the element from which to copy the styles 4243 * is in the dom 4244 * 4245 * Interesting article on how to copy styles 4246 * 4247 * @param {Array} stylesToCopy List of styles which should be copied 4248 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to 4249 * copy the styles from., this again returns an object which provides a method named "to" which can be invoked 4250 * with the element where to copy the styles to (see example) 4251 * 4252 * @example 4253 * var textarea = document.querySelector("textarea"), 4254 * div = document.querySelector("div[contenteditable=true]"), 4255 * anotherDiv = document.querySelector("div.preview"); 4256 * wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv); 4257 * 4258 */ 4259(function(dom) { 4260 4261 /** 4262 * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set 4263 * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then 4264 * its computed css width will be 198px 4265 */ 4266 var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"]; 4267 4268 var shouldIgnoreBoxSizingBorderBox = function(element) { 4269 if (hasBoxSizingBorderBox(element)) { 4270 return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth; 4271 } 4272 return false; 4273 }; 4274 4275 var hasBoxSizingBorderBox = function(element) { 4276 var i = 0, 4277 length = BOX_SIZING_PROPERTIES.length; 4278 for (; i<length; i++) { 4279 if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") { 4280 return BOX_SIZING_PROPERTIES[i]; 4281 } 4282 } 4283 }; 4284 4285 dom.copyStyles = function(stylesToCopy) { 4286 return { 4287 from: function(element) { 4288 if (shouldIgnoreBoxSizingBorderBox(element)) { 4289 stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES); 4290 } 4291 4292 var cssText = "", 4293 length = stylesToCopy.length, 4294 i = 0, 4295 property; 4296 for (; i<length; i++) { 4297 property = stylesToCopy[i]; 4298 cssText += property + ":" + dom.getStyle(property).from(element) + ";"; 4299 } 4300 4301 return { 4302 to: function(element) { 4303 dom.setStyles(cssText).on(element); 4304 return { andTo: arguments.callee }; 4305 } 4306 }; 4307 } 4308 }; 4309 }; 4310})(wysihtml5.dom);/** 4311 * Event Delegation 4312 * 4313 * @example 4314 * wysihtml5.dom.delegate(document.body, "a", "click", function() { 4315 * // foo 4316 * });
4317 */ 4318(function(wysihtml5) { 4319 4320 wysihtml5.dom.delegate = function(container, selector, eventName, handler) { 4321 return wysihtml5.dom.observe(container, eventName, function(event) { 4322 var target = event.target, 4323 match = wysihtml5.lang.array(container.querySelectorAll(selector)); 4324 4325 while (target && target !== container) { 4326 if (match.contains(target)) { 4327 handler.call(target, event); 4328 break; 4329 } 4330 target = target.parentNode; 4331 } 4332 }); 4333 }; 4334 4335})(wysihtml5);/** 4336 * Returns the given html wrapped in a div element 4337 * 4338 * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly 4339 * when inserted via innerHTML 4340 * 4341 * @param {String} html The html which should be wrapped in a dom element 4342 * @param {Obejct} [context] Document object of the context the html belongs to 4343 * 4344 * @example 4345 * wysihtml5.dom.getAsDom("<article>foo</article>"); 4346 */ 4347wysihtml5.dom.getAsDom = (function() { 4348 4349 var _innerHTMLShiv = function(html, context) { 4350 var tempElement = context.createElement("div"); 4351 tempElement.style.display = "none"; 4352 context.body.appendChild(tempElement); 4353 // IE throws an exception when trying to insert <frameset></frameset> via innerHTML 4354 try { tempElement.innerHTML = html; } catch(e) {} 4355 context.body.removeChild(tempElement); 4356 return tempElement; 4357 }; 4358 4359 /** 4360 * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element 4361 */ 4362 var _ensureHTML5Compatibility = function(context) { 4363 if (context._wysihtml5_supportsHTML5Tags) { 4364 return; 4365 } 4366 for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) { 4367 context.createElement(HTML5_ELEMENTS[i]); 4368 } 4369 context._wysihtml5_supportsHTML5Tags = true; 4370 }; 4371 4372 4373 /** 4374 * List of html5 tags 4375 * taken from http://simon.html5.org/html5-elements 4376 */ 4377 var HTML5_ELEMENTS = [ 4378 "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption", 4379 "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress", 4380 "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr" 4381 ]; 4382 4383 return function(html, context) { 4384 context = context || document; 4385 var tempElement; 4386 if (typeof(html) === "object" && html.nodeType) { 4387 tempElement = context.createElement("div"); 4388 tempElement.appendChild(html); 4389 } else if (wysihtml5.browser.supportsHTML5Tags(context)) { 4390 tempElement = context.createElement("div"); 4391 tempElement.innerHTML = html; 4392 } else { 4393 _ensureHTML5Compatibility(context); 4394 tempElement = _innerHTMLShiv(html, context); 4395 } 4396 return tempElement; 4397 }; 4398})();/** 4399 * Walks the dom tree from the given node up until it finds a match 4400 * Designed for optimal performance. 4401 * 4402 * @param {Element} node The from which to check the parent nodes 4403 * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp) 4404 * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50) 4405 * @return {null|Element} Returns the first element that matched the desiredNodeName(s) 4406 * @example 4407 * var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] }); 4408 * // ... or ... 4409 * var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" }); 4410 * // ... or ... 4411 * var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g }); 4412 */ 4413wysihtml5.dom.getParentElement = (function() { 4414 4415 function _isSameNodeName(nodeName, desiredNodeNames) { 4416 if (!desiredNodeNames || !desiredNodeNames.length) { 4417 return true; 4418 } 4419 4420 if (typeof(desiredNodeNames) === "string") { 4421 return nodeName === desiredNodeNames; 4422 } else { 4423 return wysihtml5.lang.array(desiredNodeNames).contains(nodeName); 4424 } 4425 } 4426 4427 function _isElement(node) { 4428 return node.nodeType === wysihtml5.ELEMENT_NODE; 4429 } 4430 4431 function _hasClassName(element, className, classRegExp) { 4432 var classNames = (element.className || "").match(classRegExp) || []; 4433 if (!className) { 4434 return !!classNames.length; 4435 } 4436 return classNames[classNames.length - 1] === className; 4437 } 4438 4439 function _getParentElementWithNodeName(node, nodeName, levels) { 4440 while (levels-- && node && node.nodeName !== "BODY") { 4441 if (_isSameNodeName(node.nodeName, nodeName)) { 4442 return node; 4443 } 4444 node = node.parentNode; 4445 } 4446 return null; 4447 } 4448 4449 function _getParentElementWithNodeNameAndClassName(node, nodeName, className, classRegExp, levels) { 4450 while (levels-- && node && node.nodeName !== "BODY") { 4451 if (_isElement(node) && 4452 _isSameNodeName(node.nodeName, nodeName) && 4453 _hasClassName(node, className, classRegExp)) { 4454 return node; 4455 } 4456 node = node.parentNode; 4457 } 4458 return null; 4459 } 4460 4461 return function(node, matchingSet, levels) { 4462 levels = levels || 50; // Go max 50 nodes upwards from current node 4463 if (matchingSet.className || matchingSet.classRegExp) { 4464 return _getParentElementWithNodeNameAndClassName( 4465 node, matchingSet.nodeName, matchingSet.className, matchingSet.classRegExp, levels 4466 ); 4467 } else { 4468 return _getParentElementWithNodeName( 4469 node, matchingSet.nodeName, levels 4470 ); 4471 } 4472 }; 4473})(); 4474/** 4475 * Get element's style for a specific css property 4476 * 4477 * @param {Element} element The element on which to retrieve the style 4478 * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...) 4479 * 4480 * @example 4481 * wysihtml5.dom.getStyle("display").from(document.body); 4482 * // => "block" 4483 */ 4484wysihtml5.dom.getStyle = (function() { 4485 var stylePropertyMapping = { 4486 "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat" 4487 }, 4488 REG_EXP_CAMELIZE = /\-[a-z]/g; 4489 4490 function camelize(str) { 4491 return str.replace(REG_EXP_CAMELIZE, function(match) { 4492 return match.charAt(1).toUpperCase(); 4493 });
4494 } 4495 4496 return function(property) { 4497 return { 4498 from: function(element) { 4499 if (element.nodeType !== wysihtml5.ELEMENT_NODE) { 4500 return; 4501 } 4502 4503 var doc = element.ownerDocument, 4504 camelizedProperty = stylePropertyMapping[property] || camelize(property), 4505 style = element.style, 4506 currentStyle = element.currentStyle, 4507 styleValue = style[camelizedProperty]; 4508 if (styleValue) { 4509 return styleValue; 4510 } 4511 4512 // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant 4513 // window.getComputedStyle, since it returns css property values in their original unit: 4514 // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle 4515 // gives you the original "50%". 4516 // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio 4517 if (currentStyle) { 4518 try { 4519 return currentStyle[camelizedProperty]; 4520 } catch(e) { 4521 //ie will occasionally fail for unknown reasons. swallowing exception 4522 } 4523 } 4524 4525 var win = doc.defaultView || doc.parentWindow, 4526 needsOverflowReset = (property === "height" || property === "width") && element.nodeName === "TEXTAREA", 4527 originalOverflow, 4528 returnValue; 4529 4530 if (win.getComputedStyle) { 4531 // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars 4532 // therfore we remove and restore the scrollbar and calculate the value in between 4533 if (needsOverflowReset) { 4534 originalOverflow = style.overflow; 4535 style.overflow = "hidden"; 4536 } 4537 returnValue = win.getComputedStyle(element, null).getPropertyValue(property); 4538 if (needsOverflowReset) { 4539 style.overflow = originalOverflow || ""; 4540 } 4541 return returnValue; 4542 } 4543 } 4544 }; 4545 }; 4546})();/** 4547 * High performant way to check whether an element with a specific tag name is in the given document 4548 * Optimized for being heavily executed 4549 * Unleashes the power of live node lists 4550 * 4551 * @param {Object} doc The document object of the context where to check 4552 * @param {String} tagName Upper cased tag name 4553 * @example 4554 * wysihtml5.dom.hasElementWithTagName(document, "IMG"); 4555 */ 4556wysihtml5.dom.hasElementWithTagName = (function() { 4557 var LIVE_CACHE = {}, 4558 DOCUMENT_IDENTIFIER = 1; 4559 4560 function _getDocumentIdentifier(doc) { 4561 return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); 4562 } 4563 4564 return function(doc, tagName) { 4565 var key = _getDocumentIdentifier(doc) + ":" + tagName, 4566 cacheEntry = LIVE_CACHE[key]; 4567 if (!cacheEntry) { 4568 cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName); 4569 } 4570 4571 return cacheEntry.length > 0; 4572 }; 4573})();/** 4574 * High performant way to check whether an element with a specific class name is in the given document 4575 * Optimized for being heavily executed 4576 * Unleashes the power of live node lists 4577 * 4578 * @param {Object} doc The document object of the context where to check 4579 * @param {String} tagName Upper cased tag name 4580 * @example 4581 * wysihtml5.dom.hasElementWithClassName(document, "foobar"); 4582 */ 4583(function(wysihtml5) { 4584 var LIVE_CACHE = {}, 4585 DOCUMENT_IDENTIFIER = 1; 4586 4587 function _getDocumentIdentifier(doc) { 4588 return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); 4589 } 4590
4591 wysihtml5.dom.hasElementWithClassName = function(doc, className) { 4592 // getElementsByClassName is not supported by IE<9 4593 // but is sometimes mocked via library code (which then doesn't return live node lists) 4594 if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) { 4595 return !!doc.querySelector("." + className); 4596 } 4597 4598 var key = _getDocumentIdentifier(doc) + ":" + className, 4599 cacheEntry = LIVE_CACHE[key]; 4600 if (!cacheEntry) { 4601 cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className); 4602 } 4603 4604 return cacheEntry.length > 0; 4605 }; 4606})(wysihtml5); 4607wysihtml5.dom.insert = function(elementToInsert) { 4608 return { 4609 after: function(element) { 4610 element.parentNode.insertBefore(elementToInsert, element.nextSibling); 4611 }, 4612 4613 before: function(element) { 4614 element.parentNode.insertBefore(elementToInsert, element); 4615 }, 4616 4617 into: function(element) { 4618 element.appendChild(elementToInsert); 4619 } 4620 }; 4621};wysihtml5.dom.insertCSS = function(rules) { 4622 rules = rules.join("\n"); 4623 4624 return { 4625 into: function(doc) { 4626 var head = doc.head || doc.getElementsByTagName("head")[0], 4627 styleElement = doc.createElement("style"); 4628 4629 styleElement.type = "text/css"; 4630 4631 if (styleElement.styleSheet) { 4632 styleElement.styleSheet.cssText = rules; 4633 } else { 4634 styleElement.appendChild(doc.createTextNode(rules)); 4635 } 4636 4637 if (head) { 4638 head.appendChild(styleElement); 4639 } 4640 } 4641 }; 4642};/** 4643 * Method to set dom events 4644 * 4645 * @example 4646 * wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... }); 4647 */ 4648wysihtml5.dom.observe = function(element, eventNames, handler) { 4649 eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames; 4650 4651 var handlerWrapper, 4652 eventName, 4653 i = 0, 4654 length = eventNames.length; 4655 4656 for (; i<length; i++) { 4657 eventName = eventNames[i]; 4658 if (element.addEventListener) { 4659 element.addEventListener(eventName, handler, false); 4660 } else { 4661 handlerWrapper = function(event) { 4662 if (!("target" in event)) { 4663 event.target = event.srcElement; 4664 } 4665 event.preventDefault = event.preventDefault || function() { 4666 this.returnValue = false; 4667 }; 4668 event.stopPropagation = event.stopPropagation || function() { 4669 this.cancelBubble = true; 4670 }; 4671 handler.call(element, event); 4672 }; 4673 element.attachEvent("on" + eventName, handlerWrapper); 4674 } 4675 } 4676 4677 return { 4678 stop: function() { 4679 var eventName, 4680 i = 0, 4681 length = eventNames.length; 4682 for (; i<length; i++) { 4683 eventName = eventNames[i]; 4684 if (element.removeEventListener) { 4685 element.removeEventListener(eventName, handler, false); 4686 } else { 4687 element.detachEvent("on" + eventName, handlerWrapper); 4688 } 4689 } 4690 } 4691 }; 4692}; 4693/** 4694 * HTML Sanitizer 4695 * Rewrites the HTML based on given rules 4696 * 4697 * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized 4698 * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will 4699 * be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the 4700 * desired substitution. 4701 * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing 4702 * 4703 * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element. 4704 * 4705 * @example 4706 * var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>'; 4707 * wysihtml5.dom.parse(userHTML, { 4708 * tags { 4709 * p: "div", // Rename p tags to div tags 4710 * font: "span" // Rename font tags to span tags 4711 * div: true, // Keep them, also possible (same result when passing: "div" or true) 4712 * script: undefined // Remove script elements 4713 * } 4714 * });
4715 * // => <div><div><span>foo bar</span></div></div> 4716 * 4717 * var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>'; 4718 * wysihtml5.dom.parse(userHTML); 4719 * // => '<span><span><span><span>I'm a table!</span></span></span></span>' 4720 * 4721 * var userHTML = '<div>foobar<br>foobar</div>'; 4722 * wysihtml5.dom.parse(userHTML, { 4723 * tags: { 4724 * div: undefined, 4725 * br: true 4726 * } 4727 * }); 4728 * // => '' 4729 * 4730 * var userHTML = '<div class="red">foo</div><div class="pink">bar</div>'; 4731 * wysihtml5.dom.parse(userHTML, { 4732 * classes: { 4733 * red: 1, 4734 * green: 1 4735 * }, 4736 * tags: { 4737 * div: { 4738 * rename_tag: "p" 4739 * } 4740 * } 4741 * }); 4742 * // => '<p class="red">foo</p><p>bar</p>' 4743 */ 4744wysihtml5.dom.parse = (function() { 4745 4746 /** 4747 * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML 4748 * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the 4749 * node isn't closed 4750 * 4751 * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML. 4752 */ 4753 var NODE_TYPE_MAPPING = { 4754 "1": _handleElement, 4755 "3": _handleText 4756 }, 4757 // Rename unknown tags to this 4758 DEFAULT_NODE_NAME = "span", 4759 WHITE_SPACE_REG_EXP = /\s+/, 4760 defaultRules = { tags: {}, classes: {} }, 4761 currentRules = {}; 4762 4763 /** 4764 * Iterates over all childs of the element, recreates them, appends them into a document fragment 4765 * which later replaces the entire body content 4766 */ 4767 function parse(elementOrHtml, rules, context, cleanUp) { 4768 wysihtml5.lang.object(currentRules).merge(defaultRules).merge(rules).get(); 4769 4770 context = context || elementOrHtml.ownerDocument || document; 4771 var fragment = context.createDocumentFragment(), 4772 isString = typeof(elementOrHtml) === "string", 4773 element, 4774 newNode, 4775 firstChild; 4776 4777 if (isString) { 4778 element = wysihtml5.dom.getAsDom(elementOrHtml, context); 4779 } else { 4780 element = elementOrHtml; 4781 } 4782 4783 while (element.firstChild) { 4784 firstChild = element.firstChild; 4785 element.removeChild(firstChild); 4786 newNode = _convert(firstChild, cleanUp); 4787 if (newNode) { 4788 fragment.appendChild(newNode); 4789 } 4790 } 4791 4792 // Clear element contents 4793 element.innerHTML = ""; 4794 4795 // Insert new DOM tree 4796 element.appendChild(fragment); 4797 4798 return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element; 4799 } 4800 4801 function _convert(oldNode, cleanUp) { 4802 var oldNodeType = oldNode.nodeType, 4803 oldChilds = oldNode.childNodes, 4804 oldChildsLength = oldChilds.length, 4805 newNode, 4806 method = NODE_TYPE_MAPPING[oldNodeType], 4807 i = 0; 4808 4809 newNode = method && method(oldNode); 4810 4811 if (!newNode) { 4812 return null; 4813 } 4814 4815 for (i=0; i<oldChildsLength; i++) { 4816 newChild = _convert(oldChilds[i], cleanUp); 4817 if (newChild) { 4818 newNode.appendChild(newChild); 4819 } 4820 } 4821 4822 // Cleanup senseless <span> elements 4823 if (cleanUp && 4824 newNode.childNodes.length <= 1 && 4825 newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME && 4826 !newNode.attributes.length) { 4827 return newNode.firstChild; 4828 } 4829 4830 return newNode; 4831 } 4832 4833 function _handleElement(oldNode) { 4834 var rule, 4835 newNode, 4836 endTag, 4837 tagRules = currentRules.tags, 4838 nodeName = oldNode.nodeName.toLowerCase(), 4839 scopeName = oldNode.scopeName; 4840 4841 /** 4842 * We already parsed that element 4843 * ignore it! (yes, this sometimes happens in IE8 when the html is invalid) 4844 */ 4845 if (oldNode._wysihtml5) { 4846 return null; 4847 } 4848 oldNode._wysihtml5 = 1; 4849 4850 if (oldNode.className === "wysihtml5-temp") { 4851 return null; 4852 } 4853 4854 /** 4855 * IE is the only browser who doesn't include the namespace in the
4856 * nodeName, that's why we have to prepend it by ourselves 4857 * scopeName is a proprietary IE feature 4858 * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx 4859 */ 4860 if (scopeName && scopeName != "HTML") { 4861 nodeName = scopeName + ":" + nodeName; 4862 } 4863 4864 /** 4865 * Repair node 4866 * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags 4867 * A <p> doesn't need to be closed according HTML4-5 spec, we simply replace it with a <div> to preserve its content and layout 4868 */ 4869 if ("outerHTML" in oldNode) { 4870 if (!wysihtml5.browser.autoClosesUnclosedTags() && 4871 oldNode.nodeName === "P" && 4872 oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") { 4873 nodeName = "div"; 4874 } 4875 } 4876 4877 if (nodeName in tagRules) { 4878 rule = tagRules[nodeName]; 4879 if (!rule || rule.remove) { 4880 return null; 4881 } 4882 4883 rule = typeof(rule) === "string" ? { rename_tag: rule } : rule; 4884 } else if (oldNode.firstChild) { 4885 rule = { rename_tag: DEFAULT_NODE_NAME }; 4886 } else { 4887 // Remove empty unknown elements 4888 return null; 4889 } 4890 4891 newNode = oldNode.ownerDocument.createElement(rule.rename_tag || nodeName); 4892 _handleAttributes(oldNode, newNode, rule); 4893 4894 oldNode = null; 4895 return newNode; 4896 } 4897 4898 function _handleAttributes(oldNode, newNode, rule) { 4899 var attributes = {}, // fresh new set of attributes to set on newNode 4900 setClass = rule.set_class, // classes to set 4901 addClass = rule.add_class, // add classes based on existing attributes 4902 setAttributes = rule.set_attributes, // attributes to set on the current node 4903 checkAttributes = rule.check_attributes, // check/convert values of attributes 4904 allowedClasses = currentRules.classes, 4905 i = 0, 4906 classes = [], 4907 newClasses = [], 4908 newUniqueClasses = [], 4909 oldClasses = [], 4910 classesLength, 4911 newClassesLength, 4912 currentClass, 4913 newClass, 4914 attributeName, 4915 newAttributeValue, 4916 method; 4917 4918 if (setAttributes) { 4919 attributes = wysihtml5.lang.object(setAttributes).clone(); 4920 } 4921 4922 if (checkAttributes) { 4923 for (attributeName in checkAttributes) { 4924 method = attributeCheckMethods[checkAttributes[attributeName]]; 4925 if (!method) { 4926 continue; 4927 } 4928 newAttributeValue = method(_getAttribute(oldNode, attributeName)); 4929 if (typeof(newAttributeValue) === "string") { 4930 attributes[attributeName] = newAttributeValue; 4931 } 4932 } 4933 } 4934 4935 if (setClass) { 4936 classes.push(setClass); 4937 } 4938 4939 if (addClass) { 4940 for (attributeName in addClass) { 4941 method = addClassMethods[addClass[attributeName]]; 4942 if (!method) { 4943 continue; 4944 } 4945 newClass = method(_getAttribute(oldNode, attributeName)); 4946 if (typeof(newClass) === "string") { 4947 classes.push(newClass); 4948 } 4949 } 4950 } 4951 4952 // make sure that wysihtml5 temp class doesn't get stripped out 4953 allowedClasses["_wysihtml5-temp-placeholder"] = 1; 4954 4955 // add old classes last 4956 oldClasses = oldNode.getAttribute("class"); 4957 if (oldClasses) {
4958 classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP)); 4959 } 4960 classesLength = classes.length; 4961 for (; i<classesLength; i++) { 4962 currentClass = classes[i]; 4963 if (allowedClasses[currentClass]) { 4964 newClasses.push(currentClass); 4965 } 4966 } 4967 4968 // remove duplicate entries and preserve class specificity 4969 newClassesLength = newClasses.length; 4970 while (newClassesLength--) { 4971 currentClass = newClasses[newClassesLength]; 4972 if (!wysihtml5.lang.array(newUniqueClasses).contains(currentClass)) { 4973 newUniqueClasses.unshift(currentClass); 4974 } 4975 } 4976 4977 if (newUniqueClasses.length) { 4978 attributes["class"] = newUniqueClasses.join(" "); 4979 } 4980 4981 // set attributes on newNode 4982 for (attributeName in attributes) { 4983 // Setting attributes can cause a js error in IE under certain circumstances 4984 // eg. on a <img> under https when it's new attribute value is non-https 4985 // TODO: Investigate this further and check for smarter handling 4986 try { 4987 newNode.setAttribute(attributeName, attributes[attributeName]); 4988 } catch(e) {} 4989 } 4990 4991 // IE8 sometimes loses the width/height attributes when those are set before the "src" 4992 // so we make sure to set them again 4993 if (attributes.src) { 4994 if (typeof(attributes.width) !== "undefined") { 4995 newNode.setAttribute("width", attributes.width); 4996 } 4997 if (typeof(attributes.height) !== "undefined") { 4998 newNode.setAttribute("height", attributes.height); 4999 } 5000 } 5001 } 5002 5003 /** 5004 * IE gives wrong results for hasAttribute/getAttribute, for example: 5005 * var td = document.createElement("td"); 5006 * td.getAttribute("rowspan"); // => "1" in IE 5007 * 5008 * Therefore we have to check the element's outerHTML for the attribute 5009 */ 5010 var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly(); 5011 function _getAttribute(node, attributeName) { 5012 attributeName = attributeName.toLowerCase(); 5013 var nodeName = node.nodeName; 5014 if (nodeName == "IMG" && attributeName == "src" && _isLoadedImage(node) === true) { 5015 // Get 'src' attribute value via object property since this will always contain the 5016 // full absolute url (http://...) 5017 // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host 5018 // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url) 5019 return node.src; 5020 } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) { 5021 // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML 5022 var outerHTML = node.outerHTML.toLowerCase(), 5023 // TODO: This might not work for attributes without value: <input disabled> 5024 hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1; 5025 5026 return hasAttribute ? node.getAttribute(attributeName) : null; 5027 } else{ 5028 return node.getAttribute(attributeName); 5029 } 5030 } 5031 5032 /** 5033 * Check whether the given node is a proper loaded image 5034 * FIXME: Returns undefined when unknown (Chrome, Safari) 5035 */ 5036 function _isLoadedImage(node) { 5037 try { 5038 return node.complete && !node.mozMatchesSelector(":-moz-broken"); 5039 } catch(e) { 5040 if (node.complete && node.readyState === "complete") { 5041 return true; 5042 } 5043 } 5044 } 5045 5046 function _handleText(oldNode) { 5047 return oldNode.ownerDocument.createTextNode(oldNode.data); 5048 } 5049 5050 5051 // ------------ attribute checks ------------ \\ 5052 var attributeCheckMethods = { 5053 url: (function() { 5054 var REG_EXP = /^https?:\/\//i; 5055 return function(attributeValue) { 5056 if (!attributeValue || !attributeValue.match(REG_EXP)) { 5057 return null; 5058 } 5059 return attributeValue.replace(REG_EXP, function(match) { 5060 return match.toLowerCase(); 5061 }); 5062 }; 5063 })(), 5064 5065 alt: (function() { 5066 var REG_EXP = /[^ a-z0-9_\-]/gi; 5067 return function(attributeValue) { 5068 if (!attributeValue) { 5069 return ""; 5070 } 5071 return attributeValue.replace(REG_EXP, ""); 5072 }; 5073 })(), 5074 5075 numbers: (function() { 5076 var REG_EXP = /\D/g; 5077 return function(attributeValue) { 5078 attributeValue = (attributeValue || "").replace(REG_EXP, ""); 5079 return attributeValue || null; 5080 }; 5081 })() 5082 }; 5083 5084 // ------------ class converter (converts an html attribute to a class name) ------------ \\ 5085 var addClassMethods = {
5086 align_img: (function() { 5087 var mapping = { 5088 left: "wysiwyg-float-left", 5089 right: "wysiwyg-float-right" 5090 }; 5091 return function(attributeValue) { 5092 return mapping[String(attributeValue).toLowerCase()]; 5093 }; 5094 })(), 5095 5096 align_text: (function() { 5097 var mapping = { 5098 left: "wysiwyg-text-align-left", 5099 right: "wysiwyg-text-align-right", 5100 center: "wysiwyg-text-align-center", 5101 justify: "wysiwyg-text-align-justify" 5102 }; 5103 return function(attributeValue) { 5104 return mapping[String(attributeValue).toLowerCase()]; 5105 }; 5106 })(), 5107 5108 clear_br: (function() { 5109 var mapping = { 5110 left: "wysiwyg-clear-left", 5111 right: "wysiwyg-clear-right", 5112 both: "wysiwyg-clear-both", 5113 all: "wysiwyg-clear-both" 5114 }; 5115 return function(attributeValue) { 5116 return mapping[String(attributeValue).toLowerCase()]; 5117 }; 5118 })(), 5119 5120 size_font: (function() { 5121 var mapping = { 5122 "1": "wysiwyg-font-size-xx-small", 5123 "2": "wysiwyg-font-size-small", 5124 "3": "wysiwyg-font-size-medium", 5125 "4": "wysiwyg-font-size-large", 5126 "5": "wysiwyg-font-size-x-large", 5127 "6": "wysiwyg-font-size-xx-large", 5128 "7": "wysiwyg-font-size-xx-large", 5129 "-": "wysiwyg-font-size-smaller", 5130 "+": "wysiwyg-font-size-larger" 5131 }; 5132 return function(attributeValue) { 5133 return mapping[String(attributeValue).charAt(0)]; 5134 }; 5135 })() 5136 }; 5137 5138 return parse; 5139})();/** 5140 * Checks for empty text node childs and removes them 5141 * 5142 * @param {Element} node The element in which to cleanup 5143 * @example 5144 * wysihtml5.dom.removeEmptyTextNodes(element); 5145 */ 5146wysihtml5.dom.removeEmptyTextNodes = function(node) { 5147 var childNode, 5148 childNodes = wysihtml5.lang.array(node.childNodes).get(), 5149 childNodesLength = childNodes.length, 5150 i = 0; 5151 for (; i<childNodesLength; i++) { 5152 childNode = childNodes[i]; 5153 if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") { 5154 childNode.parentNode.removeChild(childNode); 5155 } 5156 } 5157}; 5158/** 5159 * Renames an element (eg. a <div> to a <p>) and keeps its childs 5160 * 5161 * @param {Element} element The list element which should be renamed 5162 * @param {Element} newNodeName The desired tag name 5163 * 5164 * @example 5165 * <!-- Assume the following dom: --> 5166 * <ul id="list"> 5167 * <li>eminem</li> 5168 * <li>dr. dre</li> 5169 * <li>50 Cent</li> 5170 * </ul> 5171 * 5172 * <script> 5173 * wysihtml5.dom.renameElement(document.getElementById("list"), "ol"); 5174 * </script> 5175 * 5176 * <!-- Will result in: --> 5177 * <ol> 5178 * <li>eminem</li> 5179 * <li>dr. dre</li> 5180 * <li>50 Cent</li> 5181 * </ol> 5182 */ 5183wysihtml5.dom.renameElement = function(element, newNodeName) { 5184 var newElement = element.ownerDocument.createElement(newNodeName), 5185 firstChild; 5186 while (firstChild = element.firstChild) { 5187 newElement.appendChild(firstChild); 5188 } 5189 wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement); 5190 element.parentNode.replaceChild(newElement, element); 5191 return newElement; 5192};/** 5193 * Takes an element, removes it and replaces it with it's childs 5194 * 5195 * @param {Object} node The node which to replace with it's child nodes 5196 * @example 5197 * <div id="foo"> 5198 * <span>hello</span> 5199 * </div> 5200 * <script> 5201 * // Remove #foo and replace with it's children 5202 * wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo")); 5203 * </script> 5204 */ 5205wysihtml5.dom.replaceWithChildNodes = function(node) { 5206 if (!node.parentNode) { 5207 return; 5208 } 5209 5210 if (!node.firstChild) { 5211 node.parentNode.removeChild(node); 5212 return; 5213 } 5214 5215 var fragment = node.ownerDocument.createDocumentFragment(); 5216 while (node.firstChild) { 5217 fragment.appendChild(node.firstChild); 5218 } 5219 node.parentNode.replaceChild(fragment, node); 5220 node = fragment = null; 5221}; 5222/** 5223 * Unwraps an unordered/ordered list 5224 * 5225 * @param {Element} element The list element which should be unwrapped 5226 * 5227 * @example 5228 * <!-- Assume the following dom: --> 5229 * <ul id="list"> 5230 * <li>eminem</li> 5231 * <li>dr. dre</li> 5232 * <li>50 Cent</li> 5233 * </ul> 5234 * 5235 * <script> 5236 * wysihtml5.dom.resolveList(document.getElementById("list")); 5237 * </script> 5238 * 5239 * <!-- Will result in: --> 5240 * eminem<br> 5241 * dr. dre<br> 5242 * 50 Cent<br> 5243 */ 5244(function(dom) { 5245 function _isBlockElement(node) { 5246 return dom.getStyle("display").from(node) === "block"; 5247 } 5248 5249 function _isLineBreak(node) { 5250 return node.nodeName === "BR"; 5251 } 5252 5253 function _appendLineBreak(element) { 5254 var lineBreak = element.ownerDocument.createElement("br"); 5255 element.appendChild(lineBreak); 5256 } 5257 5258 function resolveList(list) { 5259 if (list.nodeName !== "MENU" && list.nodeName !== "UL" && list.nodeName !== "OL") { 5260 return; 5261 } 5262 5263 var doc = list.ownerDocument, 5264 fragment = doc.createDocumentFragment(), 5265 previousSibling = list.previousElementSibling || list.previousSibling, 5266 firstChild, 5267 lastChild, 5268 isLastChild, 5269 shouldAppendLineBreak, 5270 listItem; 5271 5272 if (previousSibling && !_isBlockElement(previousSibling)) { 5273 _appendLineBreak(fragment); 5274 } 5275 5276 while (listItem = list.firstChild) { 5277 lastChild = listItem.lastChild; 5278 while (firstChild = listItem.firstChild) { 5279 isLastChild = firstChild === lastChild; 5280 // This needs to be done before appending it to the fragment, as it otherwise will loose style information 5281 shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild); 5282 fragment.appendChild(firstChild); 5283 if (shouldAppendLineBreak) { 5284 _appendLineBreak(fragment); 5285 } 5286 } 5287 5288 listItem.parentNode.removeChild(listItem); 5289 } 5290 list.parentNode.replaceChild(fragment, list); 5291 } 5292 5293 dom.resolveList = resolveList; 5294})(wysihtml5.dom);/** 5295 * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way 5296 * 5297 * Browser Compatibility:
5298 * - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted" 5299 * - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...) 5300 * 5301 * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons: 5302 * - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'") 5303 * - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...) 5304 * - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire 5305 * - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe 5306 * can do anything as if the sandbox attribute wasn't set 5307 * 5308 * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready 5309 * @param {Object} [config] Optional parameters 5310 * 5311 * @example 5312 * new wysihtml5.dom.Sandbox(function(sandbox) { 5313 * sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">'; 5314 * }); 5315 */ 5316(function(wysihtml5) { 5317 var /** 5318 * Default configuration 5319 */ 5320 doc = document, 5321 /** 5322 * Properties to unset/protect on the window object 5323 */ 5324 windowProperties = [ 5325 "parent", "top", "opener", "frameElement", "frames", 5326 "localStorage", "globalStorage", "sessionStorage", "indexedDB" 5327 ], 5328 /** 5329 * Properties on the window object which are set to an empty function 5330 */ 5331 windowProperties2 = [ 5332 "open", "close", "openDialog", "showModalDialog", 5333 "alert", "confirm", "prompt", 5334 "openDatabase", "postMessage", 5335 "XMLHttpRequest", "XDomainRequest" 5336 ], 5337 /** 5338 * Properties to unset/protect on the document object 5339 */ 5340 documentProperties = [ 5341 "referrer", 5342 "write", "open", "close" 5343 ]; 5344 5345 wysihtml5.dom.Sandbox = Base.extend( 5346 /** @scope wysihtml5.dom.Sandbox.prototype */ { 5347 5348 constructor: function(readyCallback, config) { 5349 this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION; 5350 this.config = wysihtml5.lang.object({}).merge(config).get(); 5351 this.iframe = this._createIframe(); 5352 }, 5353 5354 insertInto: function(element) { 5355 if (typeof(element) === "string") { 5356 element = doc.getElementById(element); 5357 } 5358 5359 element.appendChild(this.iframe); 5360 }, 5361 5362 getIframe: function() { 5363 return this.iframe; 5364 }, 5365 5366 getWindow: function() { 5367 this._readyError(); 5368 }, 5369 5370 getDocument: function() { 5371 this._readyError(); 5372 }, 5373 5374 destroy: function() { 5375 var iframe = this.getIframe(); 5376 iframe.parentNode.removeChild(iframe); 5377 }, 5378 5379 _readyError: function() { 5380 throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet"); 5381 }, 5382 5383 /** 5384 * Creates the sandbox iframe 5385 * 5386 * Some important notes: 5387 * - We can't use HTML5 sandbox for now: 5388 * setting it causes that the iframe's dom can't be accessed from the outside 5389 * Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom 5390 * But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired. 5391 * In order to make this happen we need to set the "allow-scripts" flag. 5392 * A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all. 5393 * - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document) 5394 * - IE needs to have the security="restricted" attribute set before the iframe is 5395 * inserted into the dom tree 5396 * - Believe it or not but in IE "security" in document.createElement("iframe") is false, even 5397 * though it supports it 5398 * - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore 5399 * - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely 5400 * on the onreadystatechange event 5401 */ 5402 _createIframe: function() { 5403 var that = this, 5404 iframe = doc.createElement("iframe"); 5405 iframe.className = "wysihtml-sandbox"; 5406 wysihtml5.dom.setAttributes({ 5407 "security": "restricted", 5408 "allowtransparency": "true", 5409 "frameborder": 0, 5410 "width": 0, 5411 "height": 0, 5412 "marginwidth": 0, 5413 "marginheight": 0 5414 }).on(iframe); 5415 5416 // Setting the src like this prevents ssl warnings in IE6 5417 if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) { 5418 iframe.src = "javascript:'<html></html>'"; 5419 } 5420 5421 iframe.onload = function() { 5422 iframe.onreadystatechange = iframe.onload = null; 5423 that._onLoadIframe(iframe); 5424 }; 5425 5426 iframe.onreadystatechange = function() { 5427 if (/loaded|complete/.test(iframe.readyState)) { 5428 iframe.onreadystatechange = iframe.onload = null; 5429 that._onLoadIframe(iframe); 5430 } 5431 }; 5432 5433 return iframe; 5434 }, 5435 5436 /** 5437 * Callback for when the iframe has finished loading 5438 */ 5439 _onLoadIframe: function(iframe) { 5440 // don't resume when the iframe got unloaded (eg. by removing it from the dom) 5441 if (!wysihtml5.dom.contains(doc.documentElement, iframe)) { 5442 return; 5443 } 5444 5445 var that = this, 5446 iframeWindow = iframe.contentWindow, 5447 iframeDocument = iframe.contentWindow.document, 5448 charset = doc.characterSet || doc.charset || "utf-8", 5449 sandboxHtml = this._getHtml({ 5450 charset: charset, 5451 stylesheets: this.config.stylesheets 5452 }); 5453 5454 // Create the basic dom tree including proper DOCTYPE and charset 5455 iframeDocument.open("text/html", "replace"); 5456 iframeDocument.write(sandboxHtml); 5457 iframeDocument.close(); 5458 5459 this.getWindow = function() { return iframe.contentWindow; }; 5460 this.getDocument = function() { return iframe.contentWindow.document; }; 5461 5462 // Catch js errors and pass them to the parent's onerror event 5463 // addEventListener("error") doesn't work properly in some browsers 5464 // TODO: apparently this doesn't work in IE9! 5465 iframeWindow.onerror = function(errorMessage, fileName, lineNumber) { 5466 throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber); 5467 }; 5468 5469 if (!wysihtml5.browser.supportsSandboxedIframes()) { 5470 // Unset a bunch of sensitive variables 5471 // Please note: This isn't hack safe! 5472 // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information 5473 // IE is secure though, which is the most important thing, since IE is the only browser, who 5474 // takes over scripts & styles into contentEditable elements when copied from external websites 5475 // or applications (Microsoft Word, ...) 5476 var i, length; 5477 for (i=0, length=windowProperties.length; i<length; i++) { 5478 this._unset(iframeWindow, windowProperties[i]); 5479 } 5480 for (i=0, length=windowProperties2.length; i<length; i++) { 5481 this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION); 5482 } 5483 for (i=0, length=documentProperties.length; i<length; i++) { 5484 this._unset(iframeDocument, documentProperties[i]); 5485 } 5486 // This doesn't work in Safari 5 5487 // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit 5488 this._unset(iframeDocument, "cookie", "", true); 5489 } 5490 5491 this.loaded = true; 5492 5493 // Trigger the callback 5494 setTimeout(function() { that.callback(that); }, 0); 5495 }, 5496 5497 _getHtml: function(templateVars) { 5498 var stylesheets = templateVars.stylesheets, 5499 html = "", 5500 i = 0, 5501 length; 5502 stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets; 5503 if (stylesheets) { 5504 length = stylesheets.length; 5505 for (; i<length; i++) { 5506 html += '<link rel="stylesheet" href="' + stylesheets[i] + '">'; 5507 } 5508 } 5509 templateVars.stylesheets = html; 5510 5511 return wysihtml5.lang.string( 5512 '<!DOCTYPE html><html><head>' 5513 + '<meta charset="#{charset}">#{stylesheets}</head>' 5514 + '<body></body></html>' 5515 ).interpolate(templateVars); 5516 }, 5517 5518 /** 5519 * Method to unset/override existing variables 5520 * @example 5521 * // Make cookie unreadable and unwritable
5522 * this._unset(document, "cookie", "", true); 5523 */ 5524 _unset: function(object, property, value, setter) { 5525 try { object[property] = value; } catch(e) {} 5526 5527 try { object.__defineGetter__(property, function() { return value; }); } catch(e) {} 5528 if (setter) { 5529 try { object.__defineSetter__(property, function() {}); } catch(e) {} 5530 } 5531 5532 if (!wysihtml5.browser.crashesWhenDefineProperty(property)) { 5533 try { 5534 var config = { 5535 get: function() { return value; } 5536 }; 5537 if (setter) { 5538 config.set = function() {}; 5539 } 5540 Object.defineProperty(object, property, config); 5541 } catch(e) {} 5542 } 5543 } 5544 }); 5545})(wysihtml5); 5546(function() { 5547 var mapping = { 5548 "className": "class" 5549 }; 5550 wysihtml5.dom.setAttributes = function(attributes) { 5551 return { 5552 on: function(element) { 5553 for (var i in attributes) { 5554 element.setAttribute(mapping[i] || i, attributes[i]); 5555 } 5556 } 5557 } 5558 }; 5559})();wysihtml5.dom.setStyles = function(styles) { 5560 return { 5561 on: function(element) { 5562 var style = element.style; 5563 if (typeof(styles) === "string") { 5564 style.cssText += ";" + styles; 5565 return; 5566 } 5567 for (var i in styles) { 5568 if (i === "float") { 5569 style.cssFloat = styles[i]; 5570 style.styleFloat = styles[i]; 5571 } else { 5572 style[i] = styles[i]; 5573 } 5574 } 5575 } 5576 }; 5577};/** 5578 * Simulate HTML5 placeholder attribute 5579 * 5580 * Needed since 5581 * - div[contentEditable] elements don't support it 5582 * - older browsers (such as IE8 and Firefox 3.6) don't support it at all 5583 * 5584 * @param {Object} parent Instance of main wysihtml5.Editor class 5585 * @param {Element} view Instance of wysihtml5.views.* class 5586 * @param {String} placeholderText 5587 * 5588 * @example 5589 * wysihtml.dom.simulatePlaceholder(this, composer, "Foobar"); 5590 */ 5591(function(dom) { 5592 dom.simulatePlaceholder = function(editor, view, placeholderText) { 5593 var CLASS_NAME = "placeholder", 5594 unset = function() { 5595 if (view.hasPlaceholderSet()) { 5596 view.clear(); 5597 } 5598 dom.removeClass(view.element, CLASS_NAME); 5599 }, 5600 set = function() { 5601 if (view.isEmpty()) { 5602 view.setValue(placeholderText); 5603 dom.addClass(view.element, CLASS_NAME); 5604 } 5605 }; 5606 5607 editor 5608 .observe("set_placeholder", set) 5609 .observe("unset_placeholder", unset) 5610 .observe("focus:composer", unset) 5611 .observe("paste:composer", unset) 5612 .observe("blur:composer", set); 5613 5614 set(); 5615 }; 5616})(wysihtml5.dom); 5617(function(dom) { 5618 var documentElement = document.documentElement; 5619 if ("textContent" in documentElement) { 5620 dom.setTextContent = function(element, text) { 5621 element.textContent = text; 5622 }; 5623 5624 dom.getTextContent = function(element) { 5625 return element.textContent; 5626 }; 5627 } else if ("innerText" in documentElement) { 5628 dom.setTextContent = function(element, text) { 5629 element.innerText = text; 5630 }; 5631 5632 dom.getTextContent = function(element) { 5633 return element.innerText; 5634 }; 5635 } else { 5636 dom.setTextContent = function(element, text) { 5637 element.nodeValue = text; 5638 }; 5639 5640 dom.getTextContent = function(element) { 5641 return element.nodeValue; 5642 }; 5643 } 5644})(wysihtml5.dom); 5645 5646/** 5647 * Fix most common html formatting misbehaviors of browsers implementation when inserting 5648 * content via copy & paste contentEditable 5649 * 5650 * @author Christopher Blum 5651 */ 5652wysihtml5.quirks.cleanPastedHTML = (function() { 5653 // TODO: We probably need more rules here 5654 var defaultRules = { 5655 // When pasting underlined links <a> into a contentEditable, IE thinks, it has to insert <u> to keep the styling 5656 "a u": wysihtml5.dom.replaceWithChildNodes 5657 }; 5658 5659 function cleanPastedHTML(elementOrHtml, rules, context) { 5660 rules = rules || defaultRules; 5661 context = context || elementOrHtml.ownerDocument || document; 5662 5663 var element, 5664 isString = typeof(elementOrHtml) === "string", 5665 method, 5666 matches, 5667 matchesLength, 5668 i, 5669 j = 0; 5670 if (isString) { 5671 element = wysihtml5.dom.getAsDom(elementOrHtml, context); 5672 } else { 5673 element = elementOrHtml; 5674 } 5675 5676 for (i in rules) { 5677 matches = element.querySelectorAll(i); 5678 method = rules[i]; 5679 matchesLength = matches.length; 5680 for (; j<matchesLength; j++) { 5681 method(matches[j]); 5682 } 5683 } 5684 5685 matches = elementOrHtml = rules = null; 5686 5687 return isString ? element.innerHTML : element; 5688 } 5689 5690 return cleanPastedHTML; 5691})();/** 5692 * IE and Opera leave an empty paragraph in the contentEditable element after clearing it 5693 * 5694 * @param {Object} contentEditableElement The contentEditable element to observe for clearing events 5695 * @exaple 5696 * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); 5697 */ 5698(function(wysihtml5) { 5699 var dom = wysihtml5.dom; 5700
5701 wysihtml5.quirks.ensureProperClearing = (function() { 5702 var clearIfNecessary = function(event) { 5703 var element = this; 5704 setTimeout(function() { 5705 var innerHTML = element.innerHTML.toLowerCase(); 5706 if (innerHTML == "<p> </p>" || 5707 innerHTML == "<p> </p><p> </p>") { 5708 element.innerHTML = ""; 5709 } 5710 }, 0); 5711 }; 5712 5713 return function(composer) { 5714 dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary); 5715 }; 5716 })(); 5717 5718 5719 5720 /** 5721 * In Opera when the caret is in the first and only item of a list (<ul><li>|</li></ul>) and the list is the first child of the contentEditable element, it's impossible to delete the list by hitting backspace 5722 * 5723 * @param {Object} contentEditableElement The contentEditable element to observe for clearing events 5724 * @exaple 5725 * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); 5726 */ 5727 wysihtml5.quirks.ensureProperClearingOfLists = (function() { 5728 var ELEMENTS_THAT_CONTAIN_LI = ["OL", "UL", "MENU"]; 5729 5730 var clearIfNecessary = function(element, contentEditableElement) { 5731 if (!contentEditableElement.firstChild || !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI).contains(contentEditableElement.firstChild.nodeName)) { 5732 return; 5733 } 5734 5735 var list = dom.getParentElement(element, { nodeName: ELEMENTS_THAT_CONTAIN_LI }); 5736 if (!list) { 5737 return; 5738 } 5739 5740 var listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild; 5741 if (!listIsFirstChildOfContentEditable) { 5742 return; 5743 } 5744 5745 var hasOnlyOneListItem = list.childNodes.length <= 1; 5746 if (!hasOnlyOneListItem) { 5747 return; 5748 } 5749 5750 var onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === "" : true; 5751 if (!onlyListItemIsEmpty) { 5752 return; 5753 } 5754 5755 list.parentNode.removeChild(list); 5756 }; 5757 5758 return function(composer) { 5759 dom.observe(composer.element, "keydown", function(event) { 5760 if (event.keyCode !== wysihtml5.BACKSPACE_KEY) { 5761 return; 5762 } 5763 5764 var element = composer.selection.getSelectedNode(); 5765 clearIfNecessary(element, composer.element); 5766 }); 5767 }; 5768 })(); 5769 5770})(wysihtml5); 5771// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398 5772// 5773// In Firefox this: 5774// var d = document.createElement("div"); 5775// d.innerHTML ='<a href="~"></a>'; 5776// d.innerHTML; 5777// will result in: 5778// <a href="%7E"></a> 5779// which is wrong 5780(function(wysihtml5) { 5781 var TILDE_ESCAPED = "%7E"; 5782 wysihtml5.quirks.getCorrectInnerHTML = function(element) { 5783 var innerHTML = element.innerHTML; 5784 if (innerHTML.indexOf(TILDE_ESCAPED) === -1) { 5785 return innerHTML; 5786 } 5787 5788 var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"), 5789 url, 5790 urlToSearch, 5791 length, 5792 i; 5793 for (i=0, length=elementsWithTilde.length; i<length; i++) { 5794 url = elementsWithTilde[i].href || elementsWithTilde[i].src; 5795 urlToSearch = wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED); 5796 innerHTML = wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url); 5797 } 5798 return innerHTML; 5799 }; 5800})(wysihtml5);/** 5801 * Some browsers don't insert line breaks when hitting return in a contentEditable element 5802 * - Opera & IE insert new <p> on return 5803 * - Chrome & Safari insert new <div> on return 5804 * - Firefox inserts <br> on return (yippie!) 5805 * 5806 * @param {Element} element 5807 * 5808 * @example 5809 * wysihtml5.quirks.insertLineBreakOnReturn(element); 5810 */ 5811(function(wysihtml5) { 5812 var dom = wysihtml5.dom, 5813 USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"], 5814 LIST_TAGS = ["UL", "OL", "MENU"]; 5815 5816 wysihtml5.quirks.insertLineBreakOnReturn = function(composer) { 5817 function unwrap(selectedNode) { 5818 var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2); 5819 if (!parentElement) { 5820 return; 5821 } 5822 5823 var invisibleSpace = document.createTextNode(wysihtml5.INVISIBLE_SPACE); 5824 dom.insert(invisibleSpace).before(parentElement); 5825 dom.replaceWithChildNodes(parentElement); 5826 composer.selection.selectNode(invisibleSpace); 5827 } 5828 5829 function keyDown(event) { 5830 var keyCode = event.keyCode; 5831 if (event.shiftKey || (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) { 5832 return; 5833 } 5834 5835 var element = event.target, 5836 selectedNode = composer.selection.getSelectedNode(), 5837 blockElement = dom.getParentElement(selectedNode, { nodeName: USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS }, 4); 5838 if (blockElement) { 5839 // Some browsers create <p> elements after leaving a list 5840 // check after keydown of backspace and return whether a <p> got inserted and unwrap it 5841 if (blockElement.nodeName === "LI" && (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) { 5842 setTimeout(function() { 5843 var selectedNode = composer.selection.getSelectedNode(), 5844 list, 5845 div; 5846 if (!selectedNode) { 5847 return; 5848 } 5849 5850 list = dom.getParentElement(selectedNode, { 5851 nodeName: LIST_TAGS 5852 }, 2); 5853 5854 if (list) { 5855 return; 5856 } 5857 5858 unwrap(selectedNode); 5859 }, 0); 5860 } else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === wysihtml5.ENTER_KEY) { 5861 setTimeout(function() { 5862 unwrap(composer.selection.getSelectedNode()); 5863 }, 0); 5864 } 5865 return; 5866 } 5867 5868 if (keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) { 5869 composer.commands.exec("insertLineBreak"); 5870 event.preventDefault(); 5871 } 5872 } 5873 5874 // keypress doesn't fire when you hit backspace 5875 dom.observe(composer.element.ownerDocument, "keydown", keyDown); 5876 }; 5877})(wysihtml5);/** 5878 * Force rerendering of a given element 5879 * Needed to fix display misbehaviors of IE 5880 * 5881 * @param {Element} element The element object which needs to be rerendered 5882 * @example 5883 * wysihtml5.quirks.redraw(document.body); 5884 */ 5885(function(wysihtml5) { 5886 var CLASS_NAME = "wysihtml5-quirks-redraw"; 5887
5888 wysihtml5.quirks.redraw = function(element) { 5889 wysihtml5.dom.addClass(element, CLASS_NAME); 5890 wysihtml5.dom.removeClass(element, CLASS_NAME); 5891 5892 // Following hack is needed for firefox to make sure that image resize handles are properly removed 5893 try { 5894 var doc = element.ownerDocument; 5895 doc.execCommand("italic", false, null); 5896 doc.execCommand("italic", false, null); 5897 } catch(e) {} 5898 }; 5899})(wysihtml5);/** 5900 * Selection API 5901 * 5902 * @example 5903 * var selection = new wysihtml5.Selection(editor); 5904 */ 5905(function(wysihtml5) { 5906 var dom = wysihtml5.dom; 5907 5908 function _getCumulativeOffsetTop(element) { 5909 var top = 0; 5910 if (element.parentNode) { 5911 do { 5912 top += element.offsetTop || 0; 5913 element = element.offsetParent; 5914 } while (element); 5915 } 5916 return top; 5917 } 5918 5919 wysihtml5.Selection = Base.extend( 5920 /** @scope wysihtml5.Selection.prototype */ { 5921 constructor: function(editor) { 5922 // Make sure that our external range library is initialized 5923 window.rangy.init(); 5924 5925 this.editor = editor; 5926 this.composer = editor.composer; 5927 this.doc = this.composer.doc; 5928 }, 5929 5930 /** 5931 * Get the current selection as a bookmark to be able to later restore it 5932 * 5933 * @return {Object} An object that represents the current selection 5934 */ 5935 getBookmark: function() { 5936 var range = this.getRange(); 5937 return range && range.cloneRange(); 5938 }, 5939 5940 /** 5941 * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark 5942 * 5943 * @param {Object} bookmark An object that represents the current selection 5944 */ 5945 setBookmark: function(bookmark) { 5946 if (!bookmark) { 5947 return; 5948 } 5949 5950 this.setSelection(bookmark); 5951 }, 5952 5953 /** 5954 * Set the caret in front of the given node 5955 * 5956 * @param {Object} node The element or text node where to position the caret in front of 5957 * @example 5958 * selection.setBefore(myElement); 5959 */ 5960 setBefore: function(node) { 5961 var range = rangy.createRange(this.doc); 5962 range.setStartBefore(node); 5963 range.setEndBefore(node); 5964 return this.setSelection(range); 5965 }, 5966 5967 /** 5968 * Set the caret after the given node 5969 * 5970 * @param {Object} node The element or text node where to position the caret in front of 5971 * @example 5972 * selection.setBefore(myElement); 5973 */ 5974 setAfter: function(node) { 5975 var range = rangy.createRange(this.doc); 5976 range.setStartAfter(node); 5977 range.setEndAfter(node); 5978 return this.setSelection(range); 5979 }, 5980 5981 /** 5982 * Ability to select/mark nodes 5983 * 5984 * @param {Element} node The node/element to select 5985 * @example 5986 * selection.selectNode(document.getElementById("my-image")); 5987 */ 5988 selectNode: function(node) { 5989 var range = rangy.createRange(this.doc), 5990 isElement = node.nodeType === wysihtml5.ELEMENT_NODE, 5991 canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"), 5992 content = isElement ? node.innerHTML : node.data, 5993 isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE), 5994 displayStyle = dom.getStyle("display").from(node), 5995 isBlockElement = (displayStyle === "block" || displayStyle === "list-item"); 5996 5997 if (isEmpty && isElement && canHaveHTML) { 5998 // Make sure that caret is visible in node by inserting a zero width no breaking space 5999 try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} 6000 } 6001 6002 if (canHaveHTML) { 6003 range.selectNodeContents(node); 6004 } else { 6005 range.selectNode(node); 6006 } 6007 6008 if (canHaveHTML && isEmpty && isElement) { 6009 range.collapse(isBlockElement); 6010 } else if (canHaveHTML && isEmpty) { 6011 range.setStartAfter(node); 6012 range.setEndAfter(node); 6013 } 6014 6015 this.setSelection(range); 6016 }, 6017 6018 /** 6019 * Get the node which contains the selection 6020 * 6021 * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
6022 * @return {Object} The node that contains the caret 6023 * @example 6024 * var nodeThatContainsCaret = selection.getSelectedNode(); 6025 */ 6026 getSelectedNode: function(controlRange) { 6027 var selection, 6028 range; 6029 6030 if (controlRange && this.doc.selection && this.doc.selection.type === "Control") { 6031 range = this.doc.selection.createRange(); 6032 if (range && range.length) { 6033 return range.item(0); 6034 } 6035 } 6036 6037 selection = this.getSelection(this.doc); 6038 if (selection.focusNode === selection.anchorNode) { 6039 return selection.focusNode; 6040 } else { 6041 range = this.getRange(this.doc); 6042 return range ? range.commonAncestorContainer : this.doc.body; 6043 } 6044 }, 6045 6046 executeAndRestore: function(method, restoreScrollPosition) { 6047 var body = this.doc.body, 6048 oldScrollTop = restoreScrollPosition && body.scrollTop, 6049 oldScrollLeft = restoreScrollPosition && body.scrollLeft, 6050 className = "_wysihtml5-temp-placeholder", 6051 placeholderHTML = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>', 6052 range = this.getRange(this.doc), 6053 newRange; 6054 6055 // Nothing selected, execute and say goodbye 6056 if (!range) { 6057 method(body, body); 6058 return; 6059 } 6060 6061 var node = range.createContextualFragment(placeholderHTML); 6062 range.insertNode(node); 6063 6064 // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder 6065 try { 6066 method(range.startContainer, range.endContainer); 6067 } catch(e3) { 6068 setTimeout(function() { throw e3; }, 0); 6069 } 6070 6071 caretPlaceholder = this.doc.querySelector("." + className); 6072 if (caretPlaceholder) { 6073 newRange = rangy.createRange(this.doc); 6074 newRange.selectNode(caretPlaceholder); 6075 newRange.deleteContents(); 6076 this.setSelection(newRange); 6077 } else { 6078 // fallback for when all hell breaks loose 6079 body.focus(); 6080 } 6081 6082 if (restoreScrollPosition) { 6083 body.scrollTop = oldScrollTop; 6084 body.scrollLeft = oldScrollLeft; 6085 } 6086 6087 // Remove it again, just to make sure that the placeholder is definitely out of the dom tree 6088 try { 6089 caretPlaceholder.parentNode.removeChild(caretPlaceholder); 6090 } catch(e4) {} 6091 }, 6092 6093 /** 6094 * Different approach of preserving the selection (doesn't modify the dom) 6095 * Takes all text nodes in the selection and saves the selection position in the first and last one 6096 */ 6097 executeAndRestoreSimple: function(method) { 6098 var range = this.getRange(), 6099 body = this.doc.body, 6100 newRange, 6101 firstNode, 6102 lastNode, 6103 textNodes, 6104 rangeBackup; 6105 6106 // Nothing selected, execute and say goodbye 6107 if (!range) { 6108 method(body, body); 6109 return; 6110 } 6111 6112 textNodes = range.getNodes([3]); 6113 firstNode = textNodes[0] || range.startContainer; 6114 lastNode = textNodes[textNodes.length - 1] || range.endContainer; 6115 6116 rangeBackup = { 6117 collapsed: range.collapsed, 6118 startContainer: firstNode, 6119 startOffset: firstNode === range.startContainer ? range.startOffset : 0, 6120 endContainer: lastNode, 6121 endOffset: lastNode === range.endContainer ? range.endOffset : lastNode.length 6122 }; 6123 6124 try { 6125 method(range.startContainer, range.endContainer); 6126 } catch(e) { 6127 setTimeout(function() { throw e; }, 0); 6128 } 6129 6130 newRange = rangy.createRange(this.doc); 6131 try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {} 6132 try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {} 6133 try { this.setSelection(newRange); } catch(e3) {} 6134 }, 6135 6136 /** 6137 * Insert html at the caret position and move the cursor after the inserted html 6138 * 6139 * @param {String} html HTML string to insert 6140 * @example 6141 * selection.insertHTML("<p>foobar</p>"); 6142 */ 6143 insertHTML: function(html) { 6144 var range = rangy.createRange(this.doc), 6145 node = range.createContextualFragment(html), 6146 lastChild = node.lastChild; 6147 this.insertNode(node); 6148 if (lastChild) { 6149 this.setAfter(lastChild); 6150 } 6151 }, 6152 6153 /** 6154 * Insert a node at the caret position and move the cursor behind it 6155 * 6156 * @param {Object}
6156 node HTML string to insert 6157 * @example 6158 * selection.insertNode(document.createTextNode("foobar")); 6159 */ 6160 insertNode: function(node) { 6161 var range = this.getRange(); 6162 if (range) { 6163 range.insertNode(node); 6164 } 6165 }, 6166 6167 /** 6168 * Wraps current selection with the given node 6169 * 6170 * @param {Object} node The node to surround the selected elements with 6171 */ 6172 surround: function(node) { 6173 var range = this.getRange(); 6174 if (!range) { 6175 return; 6176 } 6177 6178 try { 6179 // This only works when the range boundaries are not overlapping other elements 6180 range.surroundContents(node); 6181 this.selectNode(node); 6182 } catch(e) { 6183 // fallback 6184 node.appendChild(range.extractContents()); 6185 range.insertNode(node); 6186 } 6187 }, 6188 6189 /** 6190 * Scroll the current caret position into the view 6191 * FIXME: This is a bit hacky, there might be a smarter way of doing this 6192 * 6193 * @example 6194 * selection.scrollIntoView(); 6195 */ 6196 scrollIntoView: function() { 6197 var doc = this.doc, 6198 hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight, 6199 tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() { 6200 var element = doc.createElement("span"); 6201 // The element needs content in order to be able to calculate it's position properly 6202 element.innerHTML = wysihtml5.INVISIBLE_SPACE; 6203 return element; 6204 })(), 6205 offsetTop; 6206 6207 if (hasScrollBars) { 6208 this.insertNode(tempElement); 6209 offsetTop = _getCumulativeOffsetTop(tempElement); 6210 tempElement.parentNode.removeChild(tempElement); 6211 if (offsetTop > doc.body.scrollTop) { 6212 doc.body.scrollTop = offsetTop; 6213 } 6214 } 6215 }, 6216 6217 /** 6218 * Select line where the caret is in 6219 */ 6220 selectLine: function() { 6221 if (wysihtml5.browser.supportsSelectionModify()) { 6222 this._selectLine_W3C(); 6223 } else if (this.doc.selection) { 6224 this._selectLine_MSIE(); 6225 } 6226 }, 6227 6228 /** 6229 * See https://developer.mozilla.org/en/DOM/Selection/modify 6230 */ 6231 _selectLine_W3C: function() { 6232 var win = this.doc.defaultView, 6233 selection = win.getSelection(); 6234 selection.modify("extend", "left", "lineboundary"); 6235 selection.modify("extend", "right", "lineboundary"); 6236 }, 6237 6238 _selectLine_MSIE: function() { 6239 var range = this.doc.selection.createRange(), 6240 rangeTop = range.boundingTop, 6241 rangeHeight = range.boundingHeight, 6242 scrollWidth = this.doc.body.scrollWidth, 6243 rangeBottom, 6244 rangeEnd, 6245 measureNode, 6246 i, 6247 j; 6248 6249 if (!range.moveToPoint) { 6250 return; 6251 } 6252 6253 if (rangeTop === 0) { 6254 // Don't know why, but when the selection ends at the end of a line 6255 // range.boundingTop is 0 6256 measureNode = this.doc.createElement("span"); 6257 this.insertNode(measureNode); 6258 rangeTop = measureNode.offsetTop; 6259 measureNode.parentNode.removeChild(measureNode); 6260 } 6261 6262 rangeTop += 1; 6263 6264 for (i=-10; i<scrollWidth; i+=2) { 6265 try { 6266 range.moveToPoint(i, rangeTop); 6267 break; 6268 } catch(e1) {} 6269 } 6270 6271 // Investigate the following in order to handle multi line selections 6272 // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0); 6273 rangeBottom = rangeTop; 6274 rangeEnd = this.doc.selection.createRange(); 6275 for (j=scrollWidth; j>=0; j--) { 6276 try { 6277 rangeEnd.moveToPoint(j, rangeBottom); 6278 break; 6279 } catch(e2) {} 6280 } 6281 6282 range.setEndPoint("EndToEnd", rangeEnd); 6283 range.select(); 6284 }, 6285 6286 getText: function() { 6287 var selection = this.getSelection(); 6288 return selection ? selection.toString() : ""; 6289 }, 6290 6291 getNodes: function(nodeType, filter) { 6292 var range = this.getRange(); 6293 if (range) { 6294 return range.getNodes([nodeType], filter); 6295 } else { 6296 return []; 6297 } 6298 }, 6299 6300 getRange: function() {
6301 var selection = this.getSelection(); 6302 return selection && selection.rangeCount && selection.getRangeAt(0); 6303 }, 6304 6305 getSelection: function() { 6306 return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow); 6307 }, 6308 6309 setSelection: function(range) { 6310 var win = this.doc.defaultView || this.doc.parentWindow, 6311 selection = rangy.getSelection(win); 6312 return selection.setSingleRange(range); 6313 } 6314 }); 6315 6316})(wysihtml5); 6317/** 6318 * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license. 6319 * http://code.google.com/p/rangy/ 6320 * 6321 * changed in order to be able ... 6322 * - to use custom tags 6323 * - to detect and replace similar css classes via reg exp 6324 */ 6325(function(wysihtml5, rangy) { 6326 var defaultTagName = "span"; 6327 6328 var REG_EXP_WHITE_SPACE = /\s+/g; 6329 6330 function hasClass(el, cssClass, regExp) { 6331 if (!el.className) { 6332 return false; 6333 } 6334 6335 var matchingClassNames = el.className.match(regExp) || []; 6336 return matchingClassNames[matchingClassNames.length - 1] === cssClass; 6337 } 6338 6339 function addClass(el, cssClass, regExp) { 6340 if (el.className) { 6341 removeClass(el, regExp); 6342 el.className += " " + cssClass; 6343 } else { 6344 el.className = cssClass; 6345 } 6346 } 6347 6348 function removeClass(el, regExp) { 6349 if (el.className) { 6350 el.className = el.className.replace(regExp, ""); 6351 } 6352 } 6353 6354 function hasSameClasses(el1, el2) { 6355 return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " "); 6356 } 6357 6358 function replaceWithOwnChildren(el) { 6359 var parent = el.parentNode; 6360 while (el.firstChild) { 6361 parent.insertBefore(el.firstChild, el); 6362 } 6363 parent.removeChild(el); 6364 } 6365 6366 function elementsHaveSameNonClassAttributes(el1, el2) { 6367 if (el1.attributes.length != el2.attributes.length) { 6368 return false; 6369 } 6370 for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) { 6371 attr1 = el1.attributes[i]; 6372 name = attr1.name; 6373 if (name != "class") { 6374 attr2 = el2.attributes.getNamedItem(name); 6375 if (attr1.specified != attr2.specified) { 6376 return false; 6377 } 6378 if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) { 6379 return false; 6380 } 6381 } 6382 } 6383 return true; 6384 } 6385 6386 function isSplitPoint(node, offset) { 6387 if (rangy.dom.isCharacterDataNode(node)) { 6388 if (offset == 0) { 6389 return !!node.previousSibling; 6390 } else if (offset == node.length) { 6391 return !!node.nextSibling; 6392 } else { 6393 return true; 6394 } 6395 } 6396 6397 return offset > 0 && offset < node.childNodes.length; 6398 } 6399 6400 function splitNodeAt(node, descendantNode, descendantOffset) { 6401 var newNode; 6402 if (rangy.dom.isCharacterDataNode(descendantNode)) { 6403 if (descendantOffset == 0) { 6404 descendantOffset = rangy.dom.getNodeIndex(descendantNode); 6405 descendantNode = descendantNode.parentNode; 6406 } else if (descendantOffset == descendantNode.length) { 6407 descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1; 6408 descendantNode = descendantNode.parentNode; 6409 } else { 6410 newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset); 6411 } 6412 } 6413 if (!newNode) { 6414 newNode = descendantNode.cloneNode(false); 6415 if (newNode.id) { 6416 newNode.removeAttribute("id"); 6417 } 6418 var child; 6419 while ((child = descendantNode.childNodes[descendantOffset])) { 6420 newNode.appendChild(child); 6421 } 6422 rangy.dom.insertAfter(newNode, descendantNode); 6423 } 6424 return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode)); 6425 } 6426 6427 function Merge(firstNode) { 6428 this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE); 6429 this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; 6430 this.textNodes = [this.firstTextNode]; 6431 } 6432 6433 Merge.prototype = { 6434 doMerge: function() { 6435 var textBits = [], textNode, parent, text; 6436 for (var i = 0, len = this.textNodes.length; i < len; ++i) { 6437 textNode = this.textNodes[i];
6438 parent = textNode.parentNode; 6439 textBits[i] = textNode.data; 6440 if (i) { 6441 parent.removeChild(textNode); 6442 if (!parent.hasChildNodes()) { 6443 parent.parentNode.removeChild(parent); 6444 } 6445 } 6446 } 6447 this.firstTextNode.data = text = textBits.join(""); 6448 return text; 6449 }, 6450 6451 getLength: function() { 6452 var i = this.textNodes.length, len = 0; 6453 while (i--) { 6454 len += this.textNodes[i].length; 6455 } 6456 return len; 6457 }, 6458 6459 toString: function() { 6460 var textBits = []; 6461 for (var i = 0, len = this.textNodes.length; i < len; ++i) { 6462 textBits[i] = "'" + this.textNodes[i].data + "'"; 6463 } 6464 return "[Merge(" + textBits.join(",") + ")]"; 6465 } 6466 }; 6467 6468 function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) { 6469 this.tagNames = tagNames || [defaultTagName]; 6470 this.cssClass = cssClass || ""; 6471 this.similarClassRegExp = similarClassRegExp; 6472 this.normalize = normalize; 6473 this.applyToAnyTagName = false; 6474 } 6475 6476 HTMLApplier.prototype = { 6477 getAncestorWithClass: function(node) { 6478 var cssClassMatch; 6479 while (node) { 6480 cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true; 6481 if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) { 6482 return node; 6483 } 6484 node = node.parentNode; 6485 } 6486 return false; 6487 }, 6488 6489 // Normalizes nodes after applying a CSS class to a Range. 6490 postApply: function(textNodes, range) { 6491 var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; 6492 6493 var merges = [], currentMerge; 6494 6495 var rangeStartNode = firstNode, rangeEndNode = lastNode; 6496 var rangeStartOffset = 0, rangeEndOffset = lastNode.length; 6497 6498 var textNode, precedingTextNode; 6499 6500 for (var i = 0, len = textNodes.length; i < len; ++i) { 6501 textNode = textNodes[i]; 6502 precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false); 6503 if (precedingTextNode) { 6504 if (!currentMerge) { 6505 currentMerge = new Merge(precedingTextNode); 6506 merges.push(currentMerge); 6507 } 6508 currentMerge.textNodes.push(textNode); 6509 if (textNode === firstNode) { 6510 rangeStartNode = currentMerge.firstTextNode; 6511 rangeStartOffset = rangeStartNode.length; 6512 } 6513 if (textNode === lastNode) { 6514 rangeEndNode = currentMerge.firstTextNode; 6515 rangeEndOffset = currentMerge.getLength(); 6516 } 6517 } else { 6518 currentMerge = null; 6519 } 6520 } 6521 6522 // Test whether the first node after the range needs merging 6523 var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true); 6524 if (nextTextNode) { 6525 if (!currentMerge) { 6526 currentMerge = new Merge(lastNode); 6527 merges.push(currentMerge); 6528 } 6529 currentMerge.textNodes.push(nextTextNode); 6530 } 6531 6532 // Do the merges 6533 if (merges.length) { 6534 for (i = 0, len = merges.length; i < len; ++i) { 6535 merges[i].doMerge(); 6536 } 6537 // Set the range boundaries 6538 range.setStart(rangeStartNode, rangeStartOffset); 6539 range.setEnd(rangeEndNode, rangeEndOffset); 6540 } 6541 }, 6542 6543 getAdjacentMergeableTextNode: function(node, forward) { 6544 var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE); 6545 var el = isTextNode ? node.parentNode : node; 6546 var adjacentNode; 6547 var propName = forward ? "nextSibling" : "previousSibling"; 6548 if (isTextNode) { 6549 // Can merge if the node's previous/next sibling is a text node 6550 adjacentNode = node[propName]; 6551 if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) { 6552 return adjacentNode; 6553 } 6554 } else { 6555 // Compare element with its sibling 6556 adjacentNode = el[propName]; 6557 if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) { 6558 return adjacentNode[forward ? "firstChild" : "lastChild"]; 6559 } 6560 } 6561 return null; 6562 }, 6563 6564 areElementsMergeable: function(el1, el2) { 6565 return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase()) 6566 && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase()) 6567 && hasSameClasses(el1, el2) 6568 && elementsHaveSameNonClassAttributes(el1, el2); 6569 }, 6570 6571 createContainer: function(doc) { 6572 var el = doc.createElement(this.tagNames[0]); 6573 if (this.cssClass) { 6574 el.className = this.cssClass; 6575 } 6576 return el; 6577 }, 6578 6579 applyToTextNode: function(textNode) { 6580 var parent = textNode.parentNode; 6581 if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) { 6582 if (this.cssClass) { 6583 addClass(parent, this.cssClass, this.similarClassRegExp); 6584 } 6585 } else { 6586 var el = this.createContainer(rangy.dom.getDocument(textNode)); 6587 textNode.parentNode.insertBefore(el, textNode); 6588 el.appendChild(textNode); 6589 } 6590 }, 6591 6592 isRemovable: function(el) { 6593 return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass; 6594 }, 6595 6596 undoToTextNode: function(textNode, range, ancestorWithClass) { 6597 if (!range.containsNode(ancestorWithClass)) { 6598 // Split out the portion of the ancestor from which we can remove the CSS class 6599 var ancestorRange = range.cloneRange(); 6600 ancestorRange.selectNode(ancestorWithClass); 6601 6602 if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) { 6603 splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset); 6604 range.setEndAfter(ancestorWithClass); 6605 } 6606 if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) { 6607 ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset); 6608 } 6609 } 6610 6611 if (this.similarClassRegExp) { 6612 removeClass(ancestorWithClass, this.similarClassRegExp); 6613 } 6614 if (this.isRemovable(ancestorWithClass)) { 6615 replaceWithOwnChildren(ancestorWithClass); 6616 } 6617 }, 6618 6619 applyToRange: function(range) { 6620 var textNodes = range.getNodes([wysihtml5.TEXT_NODE]); 6621 if (!textNodes.length) { 6622 try { 6623 var node = this.createContainer(range.endContainer.ownerDocument); 6624 range.surroundContents(node); 6625 this.selectNode(range, node); 6626 return; 6627 } catch(e) {} 6628 } 6629 6630 range.splitBoundaries(); 6631 textNodes = range.getNodes([wysihtml5.TEXT_NODE]); 6632 6633 if (textNodes.length) { 6634 var textNode; 6635 6636 for (var i = 0, len = textNodes.length; i < len; ++i) { 6637 textNode = textNodes[i];
6638 if (!this.getAncestorWithClass(textNode)) { 6639 this.applyToTextNode(textNode); 6640 } 6641 } 6642 6643 range.setStart(textNodes[0], 0); 6644 textNode = textNodes[textNodes.length - 1]; 6645 range.setEnd(textNode, textNode.length); 6646 6647 if (this.normalize) { 6648 this.postApply(textNodes, range); 6649 } 6650 } 6651 }, 6652 6653 undoToRange: function(range) { 6654 var textNodes = range.getNodes([wysihtml5.TEXT_NODE]), textNode, ancestorWithClass; 6655 if (textNodes.length) { 6656 range.splitBoundaries(); 6657 textNodes = range.getNodes([wysihtml5.TEXT_NODE]); 6658 } else { 6659 var doc = range.endContainer.ownerDocument, 6660 node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE); 6661 range.insertNode(node); 6662 range.selectNode(node); 6663 textNodes = [node]; 6664 } 6665 6666 for (var i = 0, len = textNodes.length; i < len; ++i) { 6667 textNode = textNodes[i]; 6668 ancestorWithClass = this.getAncestorWithClass(textNode); 6669 if (ancestorWithClass) { 6670 this.undoToTextNode(textNode, range, ancestorWithClass); 6671 } 6672 } 6673 6674 if (len == 1) { 6675 this.selectNode(range, textNodes[0]); 6676 } else { 6677 range.setStart(textNodes[0], 0); 6678 textNode = textNodes[textNodes.length - 1]; 6679 range.setEnd(textNode, textNode.length); 6680 6681 if (this.normalize) { 6682 this.postApply(textNodes, range); 6683 } 6684 } 6685 }, 6686 6687 selectNode: function(range, node) { 6688 var isElement = node.nodeType === wysihtml5.ELEMENT_NODE, 6689 canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true, 6690 content = isElement ? node.innerHTML : node.data, 6691 isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE); 6692 6693 if (isEmpty && isElement && canHaveHTML) { 6694 // Make sure that caret is visible in node by inserting a zero width no breaking space 6695 try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} 6696 } 6697 range.selectNodeContents(node); 6698 if (isEmpty && isElement) { 6699 range.collapse(false); 6700 } else if (isEmpty) { 6701 range.setStartAfter(node); 6702 range.setEndAfter(node); 6703 } 6704 }, 6705 6706 getTextSelectedByRange: function(textNode, range) { 6707 var textRange = range.cloneRange(); 6708 textRange.selectNodeContents(textNode); 6709 6710 var intersectionRange = textRange.intersection(range); 6711 var text = intersectionRange ? intersectionRange.toString() : ""; 6712 textRange.detach(); 6713 6714 return text; 6715 }, 6716 6717 isAppliedToRange: function(range) { 6718 var ancestors = [], 6719 ancestor, 6720 textNodes = range.getNodes([wysihtml5.TEXT_NODE]); 6721 if (!textNodes.length) { 6722 ancestor = this.getAncestorWithClass(range.startContainer); 6723 return ancestor ? [ancestor] : false; 6724 } 6725 6726 for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) { 6727 selectedText = this.getTextSelectedByRange(textNodes[i], range); 6728 ancestor = this.getAncestorWithClass(textNodes[i]); 6729 if (selectedText != "" && !ancestor) { 6730 return false; 6731 } else { 6732 ancestors.push(ancestor); 6733 } 6734 } 6735 return ancestors; 6736 }, 6737 6738 toggleRange: function(range) { 6739 if (this.isAppliedToRange(range)) { 6740 this.undoToRange(range); 6741 } else { 6742 this.applyToRange(range); 6743 } 6744 } 6745 }; 6746 6747 wysihtml5.selection.HTMLApplier = HTMLApplier; 6748 6749})(wysihtml5, rangy);/** 6750 * Rich Text Query/Formatting Commands 6751 * 6752 * @example 6753 * var commands = new wysihtml5.Commands(editor); 6754 */ 6755wysihtml5.Commands = Base.extend( 6756 /** @scope wysihtml5.Commands.prototype */ { 6757 constructor: function(editor) { 6758 this.editor = editor; 6759 this.composer = editor.composer; 6760 this.doc = this.composer.doc; 6761 }, 6762 6763 /** 6764 * Check whether the browser supports the given command 6765 * 6766 * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") 6767 * @example 6768 * commands.supports("createLink"); 6769 */ 6770 support: function(command) { 6771 return wysihtml5.browser.supportsCommand(this.doc, command); 6772 }, 6773 6774 /** 6775 * Check whether the browser supports the given command 6776 * 6777 * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList") 6778 * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...) 6779 * @example 6780 * commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg"); 6781 */ 6782 exec: function(command, value) {
6783 var obj = wysihtml5.commands[command], 6784 args = wysihtml5.lang.array(arguments).get(), 6785 method = obj && obj.exec, 6786 result = null; 6787 6788 this.editor.fire("beforecommand:composer"); 6789 6790 if (method) { 6791 args.unshift(this.composer); 6792 result = method.apply(obj, args); 6793 } else { 6794 try { 6795 // try/catch for buggy firefox 6796 result = this.doc.execCommand(command, false, value); 6797 } catch(e) {} 6798 } 6799 6800 this.editor.fire("aftercommand:composer"); 6801 return result; 6802 }, 6803 6804 /** 6805 * Check whether the current command is active 6806 * If the caret is within a bold text, then calling this with command "bold" should return true 6807 * 6808 * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") 6809 * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src) 6810 * @return {Boolean} Whether the command is active 6811 * @example 6812 * var isCurrentSelectionBold = commands.state("bold"); 6813 */ 6814 state: function(command, commandValue) { 6815 var obj = wysihtml5.commands[command], 6816 args = wysihtml5.lang.array(arguments).get(), 6817 method = obj && obj.state; 6818 if (method) { 6819 args.unshift(this.composer); 6820 return method.apply(obj, args); 6821 } else { 6822 try { 6823 // try/catch for buggy firefox 6824 return this.doc.queryCommandState(command); 6825 } catch(e) { 6826 return false; 6827 } 6828 } 6829 }, 6830 6831 /** 6832 * Get the current command's value 6833 * 6834 * @param {String} command The command string which to check (eg. "formatBlock") 6835 * @return {String} The command value 6836 * @example 6837 * var currentBlockElement = commands.value("formatBlock"); 6838 */ 6839 value: function(command) { 6840 var obj = wysihtml5.commands[command], 6841 method = obj && obj.value; 6842 if (method) { 6843 return method.call(obj, this.composer, command); 6844 } else { 6845 try { 6846 // try/catch for buggy firefox 6847 return this.doc.queryCommandValue(command); 6848 } catch(e) { 6849 return null; 6850 } 6851 } 6852 } 6853}); 6854(function(wysihtml5) { 6855 var undef; 6856 6857 wysihtml5.commands.bold = { 6858 exec: function(composer, command) { 6859 return wysihtml5.commands.formatInline.exec(composer, command, "b"); 6860 }, 6861 6862 state: function(composer, command, color) { 6863 // element.ownerDocument.queryCommandState("bold") results: 6864 // firefox: only <b> 6865 // chrome: <b>, <strong>, <h1>, <h2>, ... 6866 // ie: <b>, <strong> 6867 // opera: <b>, <strong> 6868 return wysihtml5.commands.formatInline.state(composer, command, "b"); 6869 }, 6870 6871 value: function() { 6872 return undef; 6873 } 6874 }; 6875})(wysihtml5); 6876 6877(function(wysihtml5) { 6878 var undef, 6879 NODE_NAME = "A", 6880 dom = wysihtml5.dom; 6881 6882 function _removeFormat(composer, anchors) { 6883 var length = anchors.length, 6884 i = 0, 6885 anchor, 6886 codeElement, 6887 textContent; 6888 for (; i<length; i++) { 6889 anchor = anchors[i]; 6890 codeElement = dom.getParentElement(anchor, { nodeName: "code" }); 6891 textContent = dom.getTextContent(anchor); 6892 6893 // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking 6894 // else replace <a> with its childNodes 6895 if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) { 6896 // <code> element is used to prevent later auto-linking of the content 6897 codeElement = dom.renameElement(anchor, "code"); 6898 } else { 6899 dom.replaceWithChildNodes(anchor); 6900 } 6901 } 6902 } 6903 6904 function _format(composer, attributes) { 6905 var doc = composer.doc, 6906 tempClass = "_wysihtml5-temp-" + (+new Date()), 6907 tempClassRegExp = /non-matching-class/g, 6908 i = 0, 6909 length, 6910 anchors, 6911 anchor, 6912 hasElementChild, 6913 isEmpty, 6914 elementToSetCaretAfter, 6915 textContent, 6916 whiteSpace, 6917 j;
6918 wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp); 6919 anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass); 6920 length = anchors.length; 6921 for (; i<length; i++) { 6922 anchor = anchors[i]; 6923 anchor.removeAttribute("class"); 6924 for (j in attributes) { 6925 anchor.setAttribute(j, attributes[j]); 6926 } 6927 } 6928 6929 elementToSetCaretAfter = anchor; 6930 if (length === 1) { 6931 textContent = dom.getTextContent(anchor); 6932 hasElementChild = !!anchor.querySelector("*"); 6933 isEmpty = textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE; 6934 if (!hasElementChild && isEmpty) { 6935 dom.setTextContent(anchor, attributes.text || anchor.href); 6936 whiteSpace = doc.createTextNode(" "); 6937 composer.selection.setAfter(anchor); 6938 composer.selection.insertNode(whiteSpace); 6939 elementToSetCaretAfter = whiteSpace; 6940 } 6941 } 6942 composer.selection.setAfter(elementToSetCaretAfter); 6943 } 6944 6945 wysihtml5.commands.createLink = { 6946 /** 6947 * TODO: Use HTMLApplier or formatInline here 6948 * 6949 * Turns selection into a link 6950 * If selection is already a link, it removes the link and wraps it with a <code> element 6951 * The <code> element is needed to avoid auto linking 6952 * 6953 * @example 6954 * // either ... 6955 * wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de"); 6956 * // ... or ... 6957 * wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" }); 6958 */ 6959 exec: function(composer, command, value) { 6960 var anchors = this.state(composer, command); 6961 if (anchors) { 6962 // Selection contains links 6963 composer.selection.executeAndRestore(function() { 6964 _removeFormat(composer, anchors); 6965 }); 6966 } else { 6967 // Create links 6968 value = typeof(value) === "object" ? value : { href: value }; 6969 _format(composer, value); 6970 } 6971 }, 6972 6973 state: function(composer, command) { 6974 return wysihtml5.commands.formatInline.state(composer, command, "A"); 6975 }, 6976 6977 value: function() { 6978 return undef; 6979 } 6980 }; 6981})(wysihtml5);/** 6982 * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags 6983 * which we don't want 6984 * Instead we set a css class 6985 */ 6986(function(wysihtml5) { 6987 var undef, 6988 REG_EXP = /wysiwyg-font-size-[a-z\-]+/g; 6989 6990 wysihtml5.commands.fontSize = { 6991 exec: function(composer, command, size) { 6992 return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP); 6993 }, 6994 6995 state: function(composer, command, size) { 6996 return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP); 6997 }, 6998 6999 value: function() { 7000 return undef; 7001 } 7002 }; 7003})(wysihtml5); 7004/** 7005 * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags 7006 * which we don't want 7007 * Instead we set a css class 7008 */ 7009(function(wysihtml5) { 7010 var undef, 7011 REG_EXP = /wysiwyg-color-[a-z]+/g; 7012 7013 wysihtml5.commands.foreColor = { 7014 exec: function(composer, command, color) { 7015 return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-color-" + color, REG_EXP); 7016 }, 7017 7018 state: function(composer, command, color) { 7019 return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP); 7020 }, 7021 7022 value: function() { 7023 return undef; 7024 } 7025 }; 7026})(wysihtml5);(function(wysihtml5) { 7027 var undef, 7028 dom = wysihtml5.dom, 7029 DEFAULT_NODE_NAME = "DIV", 7030 // Following elements are grouped 7031 // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4 7032 // instead of creating a H4 within a H1 which would result in semantically invalid html 7033 BLOCK_ELEMENTS_GROUP = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", DEFAULT_NODE_NAME]; 7034 7035 /** 7036 * Remove similiar classes (based on classRegExp) 7037 * and add the desired class name 7038 */ 7039 function _addClass(element, className, classRegExp) { 7040 if (element.className) { 7041 _removeClass(element, classRegExp); 7042 element.className += " " + className; 7043 } else { 7044 element.className = className; 7045 } 7046 } 7047 7048 function _removeClass(element, classRegExp) { 7049 element.className = element.className.replace(classRegExp, ""); 7050 } 7051 7052 /** 7053 * Check whether given node is a text node and whether it's empty 7054 */ 7055 function _isBlankTextNode(node) { 7056 return node.nodeType === wysihtml5.TEXT_NODE && !wysihtml5.lang.string(node.data).trim(); 7057 } 7058 7059 /** 7060 * Returns previous sibling node that is not a blank text node 7061 */ 7062 function _getPreviousSiblingThatIsNotBlank(node) { 7063 var previousSibling = node.previousSibling; 7064 while (previousSibling && _isBlankTextNode(previousSibling)) { 7065 previousSibling = previousSibling.previousSibling; 7066 } 7067 return previousSibling; 7068 } 7069 7070 /** 7071 * Returns next sibling node that is not a blank text node 7072 */ 7073 function _getNextSiblingThatIsNotBlank(node) { 7074 var nextSibling = node.nextSibling; 7075 while (nextSibling && _isBlankTextNode(nextSibling)) { 7076 nextSibling = nextSibling.nextSibling; 7077 } 7078 return nextSibling; 7079 } 7080 7081 /** 7082 * Adds line breaks before and after the given node if the previous and next siblings 7083 * aren't already causing a visual line break (block element or <br>) 7084 */ 7085 function _addLineBreakBeforeAndAfter(node) { 7086 var doc = node.ownerDocument, 7087 nextSibling = _getNextSiblingThatIsNotBlank(node), 7088 previousSibling = _getPreviousSiblingThatIsNotBlank(node); 7089 7090 if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) { 7091 node.parentNode.insertBefore(doc.createElement("br"), nextSibling); 7092 } 7093 if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) { 7094 node.parentNode.insertBefore(doc.createElement("br"), node); 7095 } 7096 } 7097 7098 /** 7099 * Removes line breaks before and after the given node 7100 */ 7101 function _removeLineBreakBeforeAndAfter(node) { 7102 var nextSibling = _getNextSiblingThatIsNotBlank(node), 7103 previousSibling = _getPreviousSiblingThatIsNotBlank(node); 7104 7105 if (nextSibling && _isLineBreak(nextSibling)) { 7106 nextSibling.parentNode.removeChild(nextSibling); 7107 } 7108 if (previousSibling && _isLineBreak(previousSibling)) { 7109 previousSibling.parentNode.removeChild(previousSibling); 7110 } 7111 } 7112 7113 function _removeLastChildIfLineBreak(node) { 7114 var lastChild = node.lastChild; 7115 if (lastChild && _isLineBreak(lastChild)) { 7116 lastChild.parentNode.removeChild(lastChild); 7117 } 7118 } 7119 7120 function _isLineBreak(node) { 7121 return node.nodeName === "BR"; 7122 } 7123 7124 /** 7125 * Checks whether the elment causes a visual line break 7126 * (<br> or block elements) 7127 */ 7128 function _isLineBreakOrBlockElement(element) { 7129 if (_isLineBreak(element)) { 7130 return true; 7131 } 7132 7133 if (dom.getStyle("display").from(element) === "block") { 7134 return true; 7135 } 7136 7137 return false; 7138 } 7139 7140 /** 7141 * Execute native query command 7142 * and if necessary modify the inserted node's className 7143 */ 7144 function _execCommand(doc, command, nodeName, className) { 7145 if (className) { 7146 var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) { 7147 var target = event.target, 7148 displayStyle; 7149 if (target.nodeType !== wysihtml5.ELEMENT_NODE) { 7150 return; 7151 } 7152 displayStyle = dom.getStyle("display").from(target); 7153 if (displayStyle.substr(0, 6) !== "inline") { 7154 // Make sure that only block elements receive the given class 7155 target.className += " " + className; 7156 } 7157 }); 7158 } 7159 doc.execCommand(command, false, nodeName); 7160 if (eventListener) { 7161 eventListener.stop(); 7162 } 7163 } 7164 7165 function _selectLineAndWrap(composer, element) { 7166 composer.selection.selectLine(); 7167 composer.selection.surround(element); 7168 _removeLineBreakBeforeAndAfter(element); 7169 _removeLastChildIfLineBreak(element); 7170 composer.selection.selectNode(element); 7171 } 7172 7173 function _hasClasses(element) { 7174 return !!wysihtml5.lang.string(element.className).trim(); 7175 } 7176
7177 wysihtml5.commands.formatBlock = { 7178 exec: function(composer, command, nodeName, className, classRegExp) { 7179 var doc = composer.doc, 7180 blockElement = this.state(composer, command, nodeName, className, classRegExp), 7181 selectedNode; 7182 7183 nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName; 7184 7185 if (blockElement) { 7186 composer.selection.executeAndRestoreSimple(function() { 7187 if (classRegExp) { 7188 _removeClass(blockElement, classRegExp); 7189 } 7190 var hasClasses = _hasClasses(blockElement); 7191 if (!hasClasses && blockElement.nodeName === (nodeName || DEFAULT_NODE_NAME)) { 7192 // Insert a line break afterwards and beforewards when there are siblings 7193 // that are not of type line break or block element 7194 _addLineBreakBeforeAndAfter(blockElement); 7195 dom.replaceWithChildNodes(blockElement); 7196 } else if (hasClasses) { 7197 // Make sure that styling is kept by renaming the element to <div> and copying over the class name 7198 dom.renameElement(blockElement, DEFAULT_NODE_NAME); 7199 } 7200 }); 7201 return; 7202 } 7203 7204 // Find similiar block element and rename it (<h2 class="foo"></h2> => <h1 class="foo"></h1>) 7205 if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) { 7206 selectedNode = composer.selection.getSelectedNode(); 7207 blockElement = dom.getParentElement(selectedNode, { 7208 nodeName: BLOCK_ELEMENTS_GROUP 7209 }); 7210 7211 if (blockElement) { 7212 composer.selection.executeAndRestoreSimple(function() { 7213 // Rename current block element to new block element and add class 7214 if (nodeName) { 7215 blockElement = dom.renameElement(blockElement, nodeName); 7216 } 7217 if (className) { 7218 _addClass(blockElement, className, classRegExp); 7219 } 7220 }); 7221 return; 7222 } 7223 } 7224 7225 if (composer.commands.support(command)) { 7226 _execCommand(doc, command, nodeName || DEFAULT_NODE_NAME, className); 7227 return; 7228 } 7229 7230 blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME); 7231 if (className) { 7232 blockElement.className = className; 7233 } 7234 _selectLineAndWrap(composer, blockElement); 7235 }, 7236 7237 state: function(composer, command, nodeName, className, classRegExp) { 7238 nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName; 7239 var selectedNode = composer.selection.getSelectedNode(); 7240 return dom.getParentElement(selectedNode, { 7241 nodeName: nodeName, 7242 className: className, 7243 classRegExp: classRegExp 7244 }); 7245 }, 7246 7247 value: function() { 7248 return undef; 7249 } 7250 }; 7251})(wysihtml5);/** 7252 * formatInline scenarios for tag "B" (| = caret, |foo| = selected text) 7253 * 7254 * #1 caret in unformatted text: 7255 * abcdefg| 7256 * output: 7257 * abcdefg<b>|</b> 7258 * 7259 * #2 unformatted text selected: 7260 * abc|deg|h 7261 * output: 7262 * abc<b>|deg|</b>h 7263 * 7264 * #3 unformatted text selected across boundaries: 7265 * ab|c <span>defg|h</span> 7266 * output: 7267 * ab<b>|c </b><span><b>defg</b>|h</span> 7268 * 7269 * #4 formatted text entirely selected 7270 * <b>|abc|</b> 7271 * output: 7272 * |abc| 7273 * 7274 * #5 formatted text partially selected 7275 * <b>ab|c|</b> 7276 * output: 7277 * <b>ab</b>|c| 7278 * 7279 * #6 formatted text selected across boundaries 7280 * <span>ab|c</span> <b>de|fgh</b> 7281 * output: 7282 * <span>ab|c</span> de|<b>fgh</b> 7283 */ 7284(function(wysihtml5) { 7285 var undef, 7286 // Treat <b> as <strong> and vice versa 7287 ALIAS_MAPPING = { 7288 "strong": "b", 7289 "em": "i", 7290 "b": "strong", 7291 "i": "em" 7292 }, 7293 htmlApplier = {}; 7294 7295 function _getTagNames(tagName) { 7296 var alias = ALIAS_MAPPING[tagName]; 7297 return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()]; 7298 } 7299 7300 function _getApplier(tagName, className, classRegExp) { 7301 var identifier = tagName + ":" + className; 7302 if (!htmlApplier[identifier]) { 7303 htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true); 7304 } 7305 return htmlApplier[identifier]; 7306 } 7307
7308 wysihtml5.commands.formatInline = { 7309 exec: function(composer, command, tagName, className, classRegExp) { 7310 var range = composer.selection.getRange(); 7311 if (!range) { 7312 return false; 7313 } 7314 _getApplier(tagName, className, classRegExp).toggleRange(range); 7315 composer.selection.setSelection(range); 7316 }, 7317 7318 state: function(composer, command, tagName, className, classRegExp) { 7319 var doc = composer.doc, 7320 aliasTagName = ALIAS_MAPPING[tagName] || tagName, 7321 range; 7322 7323 // Check whether the document contains a node with the desired tagName 7324 if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) && 7325 !wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) { 7326 return false; 7327 } 7328 7329 // Check whether the document contains a node with the desired className 7330 if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) { 7331 return false; 7332 } 7333 7334 range = composer.selection.getRange(); 7335 if (!range) { 7336 return false; 7337 } 7338 7339 return _getApplier(tagName, className, classRegExp).isAppliedToRange(range); 7340 }, 7341 7342 value: function() { 7343 return undef; 7344 } 7345 }; 7346})(wysihtml5);(function(wysihtml5) { 7347 var undef; 7348 7349 wysihtml5.commands.insertHTML = { 7350 exec: function(composer, command, html) { 7351 if (composer.commands.support(command)) { 7352 composer.doc.execCommand(command, false, html); 7353 } else { 7354 composer.selection.insertHTML(html); 7355 } 7356 }, 7357 7358 state: function() { 7359 return false; 7360 }, 7361 7362 value: function() { 7363 return undef; 7364 } 7365 }; 7366})(wysihtml5);(function(wysihtml5) { 7367 var NODE_NAME = "IMG"; 7368 7369 wysihtml5.commands.insertImage = { 7370 /** 7371 * Inserts an <img> 7372 * If selection is already an image link, it removes it 7373 * 7374 * @example 7375 * // either ... 7376 * wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg"); 7377 * // ... or ... 7378 * wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" }); 7379 */ 7380 exec: function(composer, command, value) { 7381 value = typeof(value) === "object" ? value : { src: value }; 7382 7383 var doc = composer.doc, 7384 image = this.state(composer), 7385 textNode, 7386 i, 7387 parent; 7388 7389 if (image) { 7390 // Image already selected, set the caret before it and delete it 7391 composer.selection.setBefore(image); 7392 parent = image.parentNode; 7393 parent.removeChild(image); 7394 7395 // and it's parent <a> too if it hasn't got any other relevant child nodes 7396 wysihtml5.dom.removeEmptyTextNodes(parent); 7397 if (parent.nodeName === "A" && !parent.firstChild) { 7398 composer.selection.setAfter(parent); 7399 parent.parentNode.removeChild(parent); 7400 } 7401 7402 // firefox and ie sometimes don't remove the image handles, even though the image got removed 7403 wysihtml5.quirks.redraw(composer.element); 7404 return; 7405 } 7406 7407 image = doc.createElement(NODE_NAME); 7408 7409 for (i in value) { 7410 image[i] = value[i]; 7411 } 7412 7413 composer.selection.insertNode(image); 7414 if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) { 7415 textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE); 7416 composer.selection.insertNode(textNode); 7417 composer.selection.setAfter(textNode); 7418 } else { 7419 composer.selection.setAfter(image); 7420 } 7421 }, 7422 7423 state: function(composer) { 7424 var doc = composer.doc, 7425 selectedNode, 7426 text, 7427 imagesInSelection; 7428 7429 if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) { 7430 return false; 7431 } 7432 7433 selectedNode = composer.selection.getSelectedNode(); 7434 if (!selectedNode) { 7435 return false; 7436 } 7437 7438 if (selectedNode.nodeName === NODE_NAME) { 7439 // This works perfectly in IE 7440 return selectedNode; 7441 } 7442 7443 if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) { 7444 return false; 7445 } 7446 7447 text = composer.selection.getText(); 7448 text = wysihtml5.lang.string(text).trim(); 7449 if (text) { 7450 return false; 7451 } 7452 7453 imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) { 7454 return node.nodeName === "IMG"; 7455 }); 7456 7457 if (imagesInSelection.length !== 1) { 7458 return false; 7459 } 7460 7461 return imagesInSelection[0]; 7462 }, 7463 7464 value: function(composer) { 7465 var image = this.state(composer); 7466 return image && image.src; 7467 } 7468 }; 7469})(wysihtml5);(function(wysihtml5) { 7470 var undef, 7471 LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : ""); 7472
7473 wysihtml5.commands.insertLineBreak = { 7474 exec: function(composer, command) { 7475 if (composer.commands.support(command)) { 7476 composer.doc.execCommand(command, false, null); 7477 if (!wysihtml5.browser.autoScrollsToCaret()) { 7478 composer.selection.scrollIntoView(); 7479 } 7480 } else { 7481 composer.commands.exec("insertHTML", LINE_BREAK); 7482 } 7483 }, 7484 7485 state: function() { 7486 return false; 7487 }, 7488 7489 value: function() { 7490 return undef; 7491 } 7492 }; 7493})(wysihtml5);(function(wysihtml5) { 7494 var undef; 7495 7496 wysihtml5.commands.insertOrderedList = { 7497 exec: function(composer, command) { 7498 var doc = composer.doc, 7499 selectedNode = composer.selection.getSelectedNode(), 7500 list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), 7501 otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), 7502 tempClassName = "_wysihtml5-temp-" + new Date().getTime(), 7503 isEmpty, 7504 tempElement; 7505 7506 if (composer.commands.support(command)) { 7507 doc.execCommand(command, false, null); 7508 return; 7509 } 7510 7511 if (list) { 7512 // Unwrap list 7513 // <ol><li>foo</li><li>bar</li></ol> 7514 // becomes: 7515 // foo<br>bar<br> 7516 composer.selection.executeAndRestoreSimple(function() { 7517 wysihtml5.dom.resolveList(list); 7518 }); 7519 } else if (otherList) { 7520 // Turn an unordered list into an ordered list 7521 // <ul><li>foo</li><li>bar</li></ul> 7522 // becomes: 7523 // <ol><li>foo</li><li>bar</li></ol> 7524 composer.selection.executeAndRestoreSimple(function() { 7525 wysihtml5.dom.renameElement(otherList, "ol"); 7526 }); 7527 } else { 7528 // Create list 7529 composer.commands.exec("formatBlock", "div", tempClassName); 7530 tempElement = doc.querySelector("." + tempClassName); 7531 isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE; 7532 composer.selection.executeAndRestoreSimple(function() { 7533 list = wysihtml5.dom.convertToList(tempElement, "ol"); 7534 }); 7535 if (isEmpty) { 7536 composer.selection.selectNode(list.querySelector("li")); 7537 } 7538 } 7539 }, 7540 7541 state: function(composer) { 7542 var selectedNode = composer.selection.getSelectedNode(); 7543 return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }); 7544 }, 7545 7546 value: function() { 7547 return undef; 7548 } 7549 }; 7550})(wysihtml5);(function(wysihtml5) { 7551 var undef; 7552 7553 wysihtml5.commands.insertUnorderedList = { 7554 exec: function(composer, command) { 7555 var doc = composer.doc, 7556 selectedNode = composer.selection.getSelectedNode(), 7557 list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), 7558 otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), 7559 tempClassName = "_wysihtml5-temp-" + new Date().getTime(), 7560 isEmpty, 7561 tempElement; 7562 7563 if (composer.commands.support(command)) { 7564 doc.execCommand(command, false, null); 7565 return; 7566 } 7567 7568 if (list) { 7569 // Unwrap list 7570 // <ul><li>foo</li><li>bar</li></ul> 7571 // becomes: 7572 // foo<br>bar<br> 7573 composer.selection.executeAndRestoreSimple(function() { 7574 wysihtml5.dom.resolveList(list); 7575 }); 7576 } else if (otherList) { 7577 // Turn an ordered list into an unordered list 7578 // <ol><li>foo</li><li>bar</li></ol> 7579 // becomes: 7580 // <ul><li>foo</li><li>bar</li></ul> 7581 composer.selection.executeAndRestoreSimple(function() { 7582 wysihtml5.dom.renameElement(otherList, "ul"); 7583 }); 7584 } else { 7585 // Create list 7586 composer.commands.exec("formatBlock", "div", tempClassName); 7587 tempElement = doc.querySelector("." + tempClassName); 7588 isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE; 7589 composer.selection.executeAndRestoreSimple(function() { 7590 list = wysihtml5.dom.convertToList(tempElement, "ul"); 7591 }); 7592 if (isEmpty) { 7593 composer.selection.selectNode(list.querySelector("li")); 7594 } 7595 } 7596 }, 7597 7598 state: function(composer) { 7599 var selectedNode = composer.selection.getSelectedNode(); 7600 return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" });
7601 }, 7602 7603 value: function() { 7604 return undef; 7605 } 7606 }; 7607})(wysihtml5);(function(wysihtml5) { 7608 var undef; 7609 7610 wysihtml5.commands.italic = { 7611 exec: function(composer, command) { 7612 return wysihtml5.commands.formatInline.exec(composer, command, "i"); 7613 }, 7614 7615 state: function(composer, command, color) { 7616 // element.ownerDocument.queryCommandState("italic") results: 7617 // firefox: only <i> 7618 // chrome: <i>, <em>, <blockquote>, ... 7619 // ie: <i>, <em> 7620 // opera: only <i> 7621 return wysihtml5.commands.formatInline.state(composer, command, "i"); 7622 }, 7623 7624 value: function() { 7625 return undef; 7626 } 7627 }; 7628})(wysihtml5);(function(wysihtml5) { 7629 var undef, 7630 CLASS_NAME = "wysiwyg-text-align-center", 7631 REG_EXP = /wysiwyg-text-align-[a-z]+/g; 7632 7633 wysihtml5.commands.justifyCenter = { 7634 exec: function(composer, command) { 7635 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); 7636 }, 7637 7638 state: function(composer, command) { 7639 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); 7640 }, 7641 7642 value: function() { 7643 return undef; 7644 } 7645 }; 7646})(wysihtml5);(function(wysihtml5) { 7647 var undef, 7648 CLASS_NAME = "wysiwyg-text-align-left", 7649 REG_EXP = /wysiwyg-text-align-[a-z]+/g; 7650 7651 wysihtml5.commands.justifyLeft = { 7652 exec: function(composer, command) { 7653 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); 7654 }, 7655 7656 state: function(composer, command) { 7657 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); 7658 }, 7659 7660 value: function() { 7661 return undef; 7662 } 7663 }; 7664})(wysihtml5);(function(wysihtml5) { 7665 var undef, 7666 CLASS_NAME = "wysiwyg-text-align-right", 7667 REG_EXP = /wysiwyg-text-align-[a-z]+/g; 7668 7669 wysihtml5.commands.justifyRight = { 7670 exec: function(composer, command) { 7671 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); 7672 }, 7673 7674 state: function(composer, command) { 7675 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); 7676 }, 7677 7678 value: function() { 7679 return undef; 7680 } 7681 }; 7682})(wysihtml5);(function(wysihtml5) { 7683 var undef; 7684 wysihtml5.commands.underline = { 7685 exec: function(composer, command) { 7686 return wysihtml5.commands.formatInline.exec(composer, command, "u"); 7687 }, 7688 7689 state: function(composer, command) { 7690 return wysihtml5.commands.formatInline.state(composer, command, "u"); 7691 }, 7692 7693 value: function() { 7694 return undef; 7695 } 7696 }; 7697})(wysihtml5);/** 7698 * Undo Manager for wysihtml5 7699 * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface 7700 */ 7701(function(wysihtml5) { 7702 var Z_KEY = 90, 7703 Y_KEY = 89, 7704 BACKSPACE_KEY = 8, 7705 DELETE_KEY = 46, 7706 MAX_HISTORY_ENTRIES = 40, 7707 UNDO_HTML = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>', 7708 REDO_HTML = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>', 7709 dom = wysihtml5.dom; 7710 7711 function cleanTempElements(doc) { 7712 var tempElement; 7713 while (tempElement = doc.querySelector("._wysihtml5-temp")) { 7714 tempElement.parentNode.removeChild(tempElement); 7715 } 7716 } 7717
7718 wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend( 7719 /** @scope wysihtml5.UndoManager.prototype */ { 7720 constructor: function(editor) { 7721 this.editor = editor; 7722 this.composer = editor.composer; 7723 this.element = this.composer.element; 7724 this.history = [this.composer.getValue()]; 7725 this.position = 1; 7726 7727 // Undo manager currently only supported in browsers who have the insertHTML command (not IE) 7728 if (this.composer.commands.support("insertHTML")) { 7729 this._observe(); 7730 } 7731 }, 7732 7733 _observe: function() { 7734 var that = this, 7735 doc = this.composer.sandbox.getDocument(), 7736 lastKey; 7737 7738 // Catch CTRL+Z and CTRL+Y 7739 dom.observe(this.element, "keydown", function(event) { 7740 if (event.altKey || (!event.ctrlKey && !event.metaKey)) { 7741 return; 7742 } 7743 7744 var keyCode = event.keyCode, 7745 isUndo = keyCode === Z_KEY && !event.shiftKey, 7746 isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY); 7747 7748 if (isUndo) { 7749 that.undo(); 7750 event.preventDefault(); 7751 } else if (isRedo) { 7752 that.redo(); 7753 event.preventDefault(); 7754 } 7755 }); 7756 7757 // Catch delete and backspace 7758 dom.observe(this.element, "keydown", function(event) { 7759 var keyCode = event.keyCode; 7760 if (keyCode === lastKey) { 7761 return; 7762 } 7763 7764 lastKey = keyCode; 7765 7766 if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) { 7767 that.transact(); 7768 } 7769 }); 7770 7771 // Now this is very hacky: 7772 // These days browsers don't offer a undo/redo event which we could hook into 7773 // to be notified when the user hits undo/redo in the contextmenu. 7774 // Therefore we simply insert two elements as soon as the contextmenu gets opened. 7775 // The last element being inserted will be immediately be removed again by a exexCommand("undo") 7776 // => When the second element appears in the dom tree then we know the user clicked "redo" in the context menu 7777 // => When the first element disappears from the dom tree then we know the user clicked "undo" in the context menu 7778 if (wysihtml5.browser.hasUndoInContextMenu()) { 7779 var interval, observed, cleanUp = function() { 7780 cleanTempElements(doc); 7781 clearInterval(interval); 7782 }; 7783 7784 dom.observe(this.element, "contextmenu", function() { 7785 cleanUp(); 7786 that.composer.selection.executeAndRestoreSimple(function() { 7787 if (that.element.lastChild) { 7788 that.composer.selection.setAfter(that.element.lastChild); 7789 } 7790 7791 // enable undo button in context menu 7792 doc.execCommand("insertHTML", false, UNDO_HTML); 7793 // enable redo button in context menu 7794 doc.execCommand("insertHTML", false, REDO_HTML); 7795 doc.execCommand("undo", false, null); 7796 }); 7797 7798 interval = setInterval(function() { 7799 if (doc.getElementById("_wysihtml5-redo")) { 7800 cleanUp(); 7801 that.redo(); 7802 } else if (!doc.getElementById("_wysihtml5-undo")) { 7803 cleanUp(); 7804 that.undo(); 7805 } 7806 }, 400); 7807 7808 if (!observed) { 7809 observed = true; 7810 dom.observe(document, "mousedown", cleanUp); 7811 dom.observe(doc, ["mousedown", "paste", "cut", "copy"], cleanUp); 7812 } 7813 }); 7814 } 7815 7816 this.editor 7817 .observe("newword:composer", function() { 7818 that.transact(); 7819 }) 7820 7821 .observe("beforecommand:composer", function() { 7822 that.transact(); 7823 }); 7824 }, 7825 7826 transact: function() { 7827 var previousHtml = this.history[this.position - 1], 7828 currentHtml = this.composer.getValue(); 7829 7830 if (currentHtml == previousHtml) { 7831 return; 7832 } 7833 7834 var length = this.history.length = this.position; 7835 if (length > MAX_HISTORY_ENTRIES) { 7836 this.history.shift(); 7837 this.position--; 7838 } 7839 7840 this.position++; 7841 this.history.push(currentHtml); 7842 }, 7843 7844 undo: function() { 7845 this.transact(); 7846 7847 if (this.position <= 1) { 7848 return; 7849 } 7850 7851 this.set(this.history[--this.position - 1]); 7852 this.editor.fire("undo:composer"); 7853 }, 7854 7855 redo: function() { 7856 if (this.position >= this.history.length) { 7857 return; 7858 } 7859 7860 this.set(this.history[++this.position - 1]); 7861 this.editor.fire("redo:composer"); 7862 }, 7863 7864 set: function(html) { 7865 this.composer.setValue(html); 7866 this.editor.focus(true); 7867 } 7868 }); 7869})(wysihtml5); 7870/** 7871 * TODO: the following methods still need unit test coverage 7872 */ 7873wysihtml5.views.View = Base.extend( 7874 /** @scope wysihtml5.views.View.prototype */ { 7875 constructor: function(parent, textareaElement, config) { 7876 this.parent = parent; 7877 this.element = textareaElement; 7878 this.config = config; 7879 7880 this._observeViewChange(); 7881 }, 7882 7883 _observeViewChange: function() { 7884 var that = this; 7885 this.parent.observe("beforeload", function() { 7886 that.parent.observe("change_view", function(view) { 7887 if (view === that.name) { 7888 that.parent.currentView = that; 7889 that.show(); 7890 // Using tiny delay here to make sure that the placeholder is set before focusing 7891 setTimeout(function() { that.focus(); }, 0); 7892 } else { 7893 that.hide(); 7894 } 7895 }); 7896 });
7897 }, 7898 7899 focus: function() { 7900 if (this.element.ownerDocument.querySelector(":focus") === this.element) { 7901 return; 7902 } 7903 7904 try { this.element.focus(); } catch(e) {} 7905 }, 7906 7907 hide: function() { 7908 this.element.style.display = "none"; 7909 }, 7910 7911 show: function() { 7912 this.element.style.display = ""; 7913 }, 7914 7915 disable: function() { 7916 this.element.setAttribute("disabled", "disabled"); 7917 }, 7918 7919 enable: function() { 7920 this.element.removeAttribute("disabled"); 7921 } 7922});(function(wysihtml5) { 7923 var dom = wysihtml5.dom, 7924 browser = wysihtml5.browser; 7925 7926 wysihtml5.views.Composer = wysihtml5.views.View.extend( 7927 /** @scope wysihtml5.views.Composer.prototype */ { 7928 name: "composer", 7929 7930 // Needed for firefox in order to display a proper caret in an empty contentEditable 7931 CARET_HACK: "<br>", 7932 7933 constructor: function(parent, textareaElement, config) { 7934 this.base(parent, textareaElement, config); 7935 this.textarea = this.parent.textarea; 7936 this._initSandbox();
7937 }, 7938 7939 clear: function() { 7940 this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK; 7941 }, 7942 7943 getValue: function(parse) { 7944 var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element); 7945 7946 if (parse) { 7947 value = this.parent.parse(value); 7948 } 7949 7950 // Replace all "zero width no breaking space" chars 7951 // which are used as hacks to enable some functionalities 7952 // Also remove all CARET hacks that somehow got left 7953 value = wysihtml5.lang.string(value).replace(wysihtml5.INVISIBLE_SPACE).by(""); 7954 7955 return value; 7956 }, 7957 7958 setValue: function(html, parse) { 7959 if (parse) { 7960 html = this.parent.parse(html); 7961 } 7962 this.element.innerHTML = html; 7963 }, 7964 7965 show: function() { 7966 this.iframe.style.display = this._displayStyle || ""; 7967 7968 // Firefox needs this, otherwise contentEditable becomes uneditable 7969 this.disable(); 7970 this.enable(); 7971 }, 7972 7973 hide: function() { 7974 this._displayStyle = dom.getStyle("display").from(this.iframe); 7975 if (this._displayStyle === "none") { 7976 this._displayStyle = null; 7977 } 7978 this.iframe.style.display = "none"; 7979 }, 7980 7981 disable: function() { 7982 this.element.removeAttribute("contentEditable"); 7983 this.base(); 7984 }, 7985 7986 enable: function() { 7987 this.element.setAttribute("contentEditable", "true"); 7988 this.base(); 7989 }, 7990 7991 focus: function(setToEnd) { 7992 // IE 8 fires the focus event after .focus() 7993 // This is needed by our simulate_placeholder.js to work 7994 // therefore we clear it ourselves this time 7995 if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) { 7996 this.clear(); 7997 } 7998 7999 this.base(); 8000 8001 var lastChild = this.element.lastChild; 8002 if (setToEnd && lastChild) { 8003 if (lastChild.nodeName === "BR") { 8004 this.selection.setBefore(this.element.lastChild); 8005 } else { 8006 this.selection.setAfter(this.element.lastChild); 8007 } 8008 } 8009 }, 8010 8011 getTextContent: function() { 8012 return dom.getTextContent(this.element); 8013 }, 8014 8015 hasPlaceholderSet: function() { 8016 return this.getTextContent() == this.textarea.element.getAttribute("placeholder"); 8017 }, 8018 8019 isEmpty: function() { 8020 var innerHTML = this.element.innerHTML, 8021 elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea"; 8022 return innerHTML === "" || 8023 innerHTML === this.CARET_HACK || 8024 this.hasPlaceholderSet() || 8025 (this.getTextContent() === "" && !this.element.querySelector(elementsWithVisualValue)); 8026 }, 8027 8028 _initSandbox: function() { 8029 var that = this; 8030 8031 this.sandbox = new dom.Sandbox(function() { 8032 that._create(); 8033 }, { 8034 stylesheets: this.config.stylesheets 8035 }); 8036 this.iframe = this.sandbox.getIframe(); 8037 8038 // Create hidden field which tells the server after submit, that the user used an wysiwyg editor 8039 var hiddenField = document.createElement("input"); 8040 hiddenField.type = "hidden"; 8041 hiddenField.name = "_wysihtml5_mode"; 8042 hiddenField.value = 1; 8043 8044 // Store reference to current wysihtml5 instance on the textarea element 8045 var textareaElement = this.textarea.element; 8046 dom.insert(this.iframe).after(textareaElement); 8047 dom.insert(hiddenField).after(textareaElement); 8048 }, 8049 8050 _create: function() { 8051 var that = this; 8052 8053 this.doc = this.sandbox.getDocument(); 8054 this.element = this.doc.body; 8055 this.textarea = this.parent.textarea; 8056 this.element.innerHTML = this.textarea.getValue(true); 8057 this.enable(); 8058 8059 // Make sure our selection handler is ready 8060 this.selection = new wysihtml5.Selection(this.parent); 8061 8062 // Make sure commands dispatcher is ready 8063 this.commands = new wysihtml5.Commands(this.parent); 8064 8065 dom.copyAttributes([ 8066 "className", "spellcheck", "title", "lang", "dir", "accessKey" 8067 ]).from(this.textarea.element).to(this.element); 8068 8069 dom.addClass(this.element, this.config.composerClassName); 8070 8071 // Make the editor look like the original textarea, by syncing styles 8072 if (this.config.style) { 8073 this.style(); 8074 } 8075 8076 this.observe(); 8077 8078 var name = this.config.name; 8079 if (name) { 8080 dom.addClass(this.element, name); 8081 dom.addClass(this.iframe, name); 8082 } 8083 8084 // Simulate html5 placeholder attribute on contentEditable element 8085 var placeholderText = typeof(this.config.placeholder) === "string" 8086 ? this.config.placeholder 8087 : this.textarea.element.getAttribute("placeholder"); 8088 if (placeholderText) { 8089 dom.simulatePlaceholder(this.parent, this, placeholderText); 8090 } 8091 8092 // Make sure that the browser avoids using inline styles whenever possible 8093 this.commands.exec("styleWithCSS", false); 8094 8095 this._initAutoLinking(); 8096 this._initObjectResizing(); 8097 this._initUndoManager(); 8098 8099 // Simulate html5 autofocus on contentEditable element 8100 if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) { 8101 setTimeout(function() { that.focus(); }, 100); 8102 } 8103 8104 wysihtml5.quirks.insertLineBreakOnReturn(this); 8105 8106 // IE sometimes leaves a single paragraph, which can't be removed by the user 8107 if (!browser.clearsContentEditableCorrectly()) { 8108 wysihtml5.quirks.ensureProperClearing(this); 8109 } 8110 8111 if (!browser.clearsListsInContentEditableCorrectly()) { 8112 wysihtml5.quirks.ensureProperClearingOfLists(this); 8113 } 8114 8115 // Set up a sync that makes sure that textarea and editor have the same content 8116 if (this.initSync && this.config.sync) { 8117 this.initSync(); 8118 } 8119 8120 // Okay hide the textarea, we are ready to go 8121 this.textarea.hide(); 8122 8123 // Fire global (before-)load event 8124 this.parent.fire("beforeload").fire("load"); 8125 }, 8126 8127 _initAutoLinking: function() { 8128 var that = this, 8129 supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(), 8130 supportsAutoLinking = browser.doesAutoLinkingInContentEditable(); 8131 if (supportsDisablingOfAutoLinking) { 8132 this.commands.exec("autoUrlDetect", false); 8133 } 8134 8135 if (!this.config.autoLink) { 8136 return; 8137 } 8138 8139 // Only do the auto linking by ourselves when the browser doesn't support auto linking 8140 // OR when he supports auto linking but we were able to turn it off (IE9+) 8141 if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) { 8142 this.parent.observe("newword:composer", function() { 8143 that.selection.executeAndRestore(function(startContainer, endContainer) { 8144 dom.autoLink(endContainer.parentNode); 8145 }); 8146 }); 8147 } 8148 8149 // Assuming we have the following: 8150 // <a href="http://www.google.de">http://www.google.de</a> 8151 // If a user now changes the url in the innerHTML we want to make sure that 8152 // it's synchronized with the href attribute (as long as the innerHTML is still a url) 8153 var // Use a live NodeList to check whether there are any links in the document 8154 links = this.sandbox.getDocument().getElementsByTagName("a"), 8155 // The autoLink helper method reveals a reg exp to detect correct urls 8156 urlRegExp = dom.autoLink.URL_REG_EXP, 8157 getTextContent = function(element) { 8158 var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim(); 8159 if (textContent.substr(0, 4) === "www.") { 8160 textContent = "http://" + textContent; 8161 } 8162 return textContent; 8163 }; 8164 8165 dom.observe(this.element, "keydown", function(event) { 8166 if (!links.length) { 8167 return; 8168 } 8169 8170 var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument), 8171 link = dom.getParentElement(selectedNode, { nodeName: "A" }, 4), 8172 textContent; 8173 8174 if (!link) { 8175 return; 8176 } 8177 8178 textContent = getTextContent(link); 8179 // keydown is fired before the actual content is changed 8180 // therefore we set a timeout to change the href 8181 setTimeout(function() { 8182 var newTextContent = getTextContent(link); 8183 if (newTextContent === textContent) { 8184 return; 8185 } 8186 8187 // Only set href when new href looks like a valid url 8188 if (newTextContent.match(urlRegExp)) { 8189 link.setAttribute("href", newTextContent); 8190 } 8191 }, 0); 8192 }); 8193 }, 8194 8195 _initObjectResizing: function() { 8196 var properties = ["width", "height"], 8197 propertiesLength = properties.length, 8198 element = this.element; 8199 8200 this.commands.exec("enableObjectResizing", this.config.allowObjectResizing); 8201 8202 if (this.config.allowObjectResizing) { 8203 // IE sets inline styles after resizing objects 8204 // The following lines make sure that the width/height css properties 8205 // are copied over to the width/height attributes 8206 if (browser.supportsEvent("resizeend")) { 8207 dom.observe(element, "resizeend", function(event) { 8208 var target = event.target || event.srcElement, 8209 style = target.style, 8210 i = 0, 8211 property; 8212 for(; i<propertiesLength; i++) { 8213 property = properties[i];
8214 if (style[property]) { 8215 target.setAttribute(property, parseInt(style[property], 10)); 8216 style[property] = ""; 8217 } 8218 } 8219 // After resizing IE sometimes forgets to remove the old resize handles 8220 wysihtml5.quirks.redraw(element); 8221 }); 8222 } 8223 } else { 8224 if (browser.supportsEvent("resizestart")) { 8225 dom.observe(element, "resizestart", function(event) { event.preventDefault(); }); 8226 } 8227 } 8228 }, 8229 8230 _initUndoManager: function() { 8231 new wysihtml5.UndoManager(this.parent); 8232 } 8233 }); 8234})(wysihtml5);(function(wysihtml5) { 8235 var dom = wysihtml5.dom, 8236 doc = document, 8237 win = window, 8238 HOST_TEMPLATE = doc.createElement("div"), 8239 /** 8240 * Styles to copy from textarea to the composer element 8241 */ 8242 TEXT_FORMATTING = [ 8243 "background-color", 8244 "color", "cursor", 8245 "font-family", "font-size", "font-style", "font-variant", "font-weight", 8246 "line-height", "letter-spacing", 8247 "text-align", "text-decoration", "text-indent", "text-rendering", 8248 "word-break", "word-wrap", "word-spacing" 8249 ], 8250 /** 8251 * Styles to copy from textarea to the iframe 8252 */ 8253 BOX_FORMATTING = [ 8254 "background-color", 8255 "border-collapse", 8256 "border-bottom-color", "border-bottom-style", "border-bottom-width", 8257 "border-left-color", "border-left-style", "border-left-width", 8258 "border-right-color", "border-right-style", "border-right-width", 8259 "border-top-color", "border-top-style", "border-top-width", 8260 "clear", "display", "float", 8261 "margin-bottom", "margin-left", "margin-right", "margin-top", 8262 "outline-color", "outline-offset", "outline-width", "outline-style", 8263 "padding-left", "padding-right", "padding-top", "padding-bottom", 8264 "position", "top", "left", "right", "bottom", "z-index", 8265 "vertical-align", "text-align", 8266 "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing", 8267 "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow", 8268 "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius", 8269 "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius", 8270 "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius", 8271 "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius", 8272 "width", "height" 8273 ], 8274 /** 8275 * Styles to sync while the window gets resized 8276 */ 8277 RESIZE_STYLE = [ 8278 "width", "height", 8279 "top", "left", "right", "bottom" 8280 ], 8281 ADDITIONAL_CSS_RULES = [ 8282 "html { height: 100%; }", 8283 "body { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }", 8284 "._wysihtml5-temp { display: none; }", 8285 wysihtml5.browser.isGecko ? 8286 "body.placeholder { color: graytext !important; }" : 8287 "body.placeholder { color: #a9a9a9 !important; }", 8288 "body[disabled] { background-color: #eee !important; color: #999 !important; cursor: default !important; }", 8289 // Ensure that user see's broken images and can delete them 8290 "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }" 8291 ]; 8292 8293 /** 8294 * With "setActive" IE offers a smart way of focusing elements without scrolling them into view: 8295 * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx 8296 * 8297 * Other browsers need a more hacky way: (pssst don't tell my mama) 8298 * In order to prevent the element being scrolled into view when focusing it, we simply 8299 * move it out of the scrollable area, focus it, and reset it's position 8300 */ 8301 var focusWithoutScrolling = function(element) { 8302 if (element.setActive) { 8303 // Following line could cause a js error when the textarea is invisible 8304 // See https://github.com/xing/wysihtml5/issues/9
8305 try { element.setActive(); } catch(e) {} 8306 } else { 8307 var elementStyle = element.style, 8308 originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop, 8309 originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft, 8310 originalStyles = { 8311 position: elementStyle.position, 8312 top: elementStyle.top, 8313 left: elementStyle.left, 8314 WebkitUserSelect: elementStyle.WebkitUserSelect 8315 }; 8316 8317 dom.setStyles({ 8318 position: "absolute", 8319 top: "-99999px", 8320 left: "-99999px", 8321 // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother 8322 WebkitUserSelect: "none" 8323 }).on(element); 8324 8325 element.focus(); 8326 8327 dom.setStyles(originalStyles).on(element); 8328 8329 if (win.scrollTo) { 8330 // Some browser extensions unset this method to prevent annoyances 8331 // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100 8332 // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1 8333 win.scrollTo(originalScrollLeft, originalScrollTop); 8334 } 8335 } 8336 }; 8337 8338 8339 wysihtml5.views.Composer.prototype.style = function() { 8340 var that = this, 8341 originalActiveElement = doc.querySelector(":focus"), 8342 textareaElement = this.textarea.element, 8343 hasPlaceholder = textareaElement.hasAttribute("placeholder"), 8344 originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"); 8345 this.focusStylesHost = this.focusStylesHost || HOST_TEMPLATE.cloneNode(false); 8346 this.blurStylesHost = this.blurStylesHost || HOST_TEMPLATE.cloneNode(false); 8347 8348 // Remove placeholder before copying (as the placeholder has an affect on the computed style) 8349 if (hasPlaceholder) { 8350 textareaElement.removeAttribute("placeholder"); 8351 } 8352 8353 if (textareaElement === originalActiveElement) { 8354 textareaElement.blur(); 8355 } 8356 8357 // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) --------- 8358 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe).andTo(this.blurStylesHost); 8359 8360 // --------- editor styles --------- 8361 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost); 8362 8363 // --------- apply standard rules --------- 8364 dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument); 8365 8366 // --------- :focus styles --------- 8367 focusWithoutScrolling(textareaElement); 8368 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost); 8369 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost); 8370 8371 // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus 8372 // this is needed for when the change_view event is fired where the iframe is hidden and then 8373 // the blur event fires and re-displays it 8374 var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]); 8375 8376 // --------- restore focus --------- 8377 if (originalActiveElement) { 8378 originalActiveElement.focus(); 8379 } else { 8380 textareaElement.blur(); 8381 } 8382 8383 // --------- restore placeholder --------- 8384 if (hasPlaceholder) { 8385 textareaElement.setAttribute("placeholder", originalPlaceholder); 8386 } 8387 8388 // When copying styles, we only get the computed style which is never returned in percent unit 8389 // Therefore we've to recalculate style onresize 8390 if (!wysihtml5.browser.hasCurrentStyleProperty()) { 8391 var winObserver = dom.observe(win, "resize", function() { 8392 // Remove event listener if composer doesn't exist anymore 8393 if (!dom.contains(document.documentElement, that.iframe)) { 8394 winObserver.stop(); 8395 return; 8396 } 8397 var originalTextareaDisplayStyle = dom.getStyle("display").from(textareaElement), 8398 originalComposerDisplayStyle = dom.getStyle("display").from(that.iframe); 8399 textareaElement.style.display = "";
8400 that.iframe.style.display = "none"; 8401 dom.copyStyles(RESIZE_STYLE) 8402 .from(textareaElement) 8403 .to(that.iframe) 8404 .andTo(that.focusStylesHost) 8405 .andTo(that.blurStylesHost); 8406 that.iframe.style.display = originalComposerDisplayStyle; 8407 textareaElement.style.display = originalTextareaDisplayStyle; 8408 }); 8409 } 8410 8411 // --------- Sync focus/blur styles --------- 8412 this.parent.observe("focus:composer", function() { 8413 dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.iframe); 8414 dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element); 8415 }); 8416 8417 this.parent.observe("blur:composer", function() { 8418 dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe); 8419 dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element); 8420 }); 8421 8422 return this; 8423 }; 8424})(wysihtml5);/** 8425 * Taking care of events 8426 * - Simulating 'change' event on contentEditable element 8427 * - Handling drag & drop logic 8428 * - Catch paste events 8429 * - Dispatch proprietary newword:composer event 8430 * - Keyboard shortcuts 8431 */ 8432(function(wysihtml5) { 8433 var dom = wysihtml5.dom, 8434 browser = wysihtml5.browser, 8435 /** 8436 * Map keyCodes to query commands 8437 */ 8438 shortcuts = { 8439 "66": "bold", // B 8440 "73": "italic", // I 8441 "85": "underline" // U 8442 }; 8443 8444 wysihtml5.views.Composer.prototype.observe = function() { 8445 var that = this, 8446 state = this.getValue(), 8447 iframe = this.sandbox.getIframe(), 8448 element = this.element, 8449 focusBlurElement = browser.supportsEventsInIframeCorrectly() ? element : this.sandbox.getWindow(), 8450 // Firefox < 3.5 doesn't support the drop event, instead it supports a so called "dragdrop" event which behaves almost the same 8451 pasteEvents = browser.supportsEvent("drop") ? ["drop", "paste"] : ["dragdrop", "paste"]; 8452 8453 // --------- destroy:composer event --------- 8454 dom.observe(iframe, "DOMNodeRemoved", function() { 8455 clearInterval(domNodeRemovedInterval); 8456 that.parent.fire("destroy:composer"); 8457 }); 8458 8459 // DOMNodeRemoved event is not supported in IE 8 8460 var domNodeRemovedInterval = setInterval(function() { 8461 if (!dom.contains(document.documentElement, iframe)) { 8462 clearInterval(domNodeRemovedInterval); 8463 that.parent.fire("destroy:composer"); 8464 } 8465 }, 250); 8466 8467 8468 // --------- Focus & blur logic --------- 8469 dom.observe(focusBlurElement, "focus", function() { 8470 that.parent.fire("focus").fire("focus:composer"); 8471 8472 // Delay storing of state until all focus handler are fired 8473 // especially the one which resets the placeholder 8474 setTimeout(function() { state = that.getValue(); }, 0); 8475 }); 8476 8477 dom.observe(focusBlurElement, "blur", function() { 8478 if (state !== that.getValue()) { 8479 that.parent.fire("change").fire("change:composer"); 8480 } 8481 that.parent.fire("blur").fire("blur:composer"); 8482 }); 8483 8484 if (wysihtml5.browser.isIos()) { 8485 // When on iPad/iPhone/IPod after clicking outside of editor, the editor loses focus 8486 // but the UI still acts as if the editor has focus (blinking caret and onscreen keyboard visible) 8487 // We prevent that by focusing a temporary input element which immediately loses focus 8488 dom.observe(element, "blur", function() { 8489 var input = element.ownerDocument.createElement("input"), 8490 originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop, 8491 originalScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; 8492 try { 8493 that.selection.insertNode(input); 8494 } catch(e) { 8495 element.appendChild(input); 8496 } 8497 input.focus(); 8498 input.parentNode.removeChild(input); 8499 8500 window.scrollTo(originalScrollLeft, originalScrollTop); 8501 }); 8502 } 8503 8504 // --------- Drag & Drop logic --------- 8505 dom.observe(element, "dragenter", function() { 8506 that.parent.fire("unset_placeholder"); 8507 });
8508 8509 if (browser.firesOnDropOnlyWhenOnDragOverIsCancelled()) { 8510 dom.observe(element, ["dragover", "dragenter"], function(event) { 8511 event.preventDefault(); 8512 }); 8513 } 8514 8515 dom.observe(element, pasteEvents, function(event) { 8516 var dataTransfer = event.dataTransfer, 8517 data; 8518 8519 if (dataTransfer && browser.supportsDataTransfer()) { 8520 data = dataTransfer.getData("text/html") || dataTransfer.getData("text/plain"); 8521 } 8522 if (data) { 8523 element.focus(); 8524 that.commands.exec("insertHTML", data); 8525 that.parent.fire("paste").fire("paste:composer"); 8526 event.stopPropagation(); 8527 event.preventDefault(); 8528 } else { 8529 setTimeout(function() { 8530 that.parent.fire("paste").fire("paste:composer"); 8531 }, 0); 8532 } 8533 }); 8534 8535 // --------- neword event --------- 8536 dom.observe(element, "keyup", function(event) { 8537 var keyCode = event.keyCode; 8538 if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) { 8539 that.parent.fire("newword:composer"); 8540 } 8541 }); 8542 8543 this.parent.observe("paste:composer", function() { 8544 setTimeout(function() { that.parent.fire("newword:composer"); }, 0); 8545 }); 8546 8547 // --------- Make sure that images are selected when clicking on them --------- 8548 if (!browser.canSelectImagesInContentEditable()) { 8549 dom.observe(element, "mousedown", function(event) { 8550 var target = event.target; 8551 if (target.nodeName === "IMG") { 8552 that.selection.selectNode(target); 8553 event.preventDefault(); 8554 } 8555 }); 8556 } 8557 8558 // --------- Shortcut logic --------- 8559 dom.observe(element, "keydown", function(event) { 8560 var keyCode = event.keyCode,
8561 command = shortcuts[keyCode]; 8562 if ((event.ctrlKey || event.metaKey) && !event.altKey && command) { 8563 that.commands.exec(command); 8564 event.preventDefault(); 8565 } 8566 }); 8567 8568 // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor --------- 8569 dom.observe(element, "keydown", function(event) { 8570 var target = that.selection.getSelectedNode(true), 8571 keyCode = event.keyCode, 8572 parent; 8573 if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete 8574 parent = target.parentNode; 8575 // delete the <img> 8576 parent.removeChild(target); 8577 // and it's parent <a> too if it hasn't got any other child nodes 8578 if (parent.nodeName === "A" && !parent.firstChild) { 8579 parent.parentNode.removeChild(parent); 8580 } 8581 8582 setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0); 8583 event.preventDefault(); 8584 } 8585 }); 8586 8587 // --------- Show url in tooltip when hovering links or images --------- 8588 var titlePrefixes = { 8589 IMG: "Image: ", 8590 A: "Link: " 8591 }; 8592 8593 dom.observe(element, "mouseover", function(event) { 8594 var target = event.target, 8595 nodeName = target.nodeName, 8596 title; 8597 if (nodeName !== "A" && nodeName !== "IMG") { 8598 return; 8599 } 8600 var hasTitle = target.hasAttribute("title"); 8601 if(!hasTitle){ 8602 title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src")); 8603 target.setAttribute("title", title); 8604 } 8605 }); 8606 }; 8607})(wysihtml5);/** 8608 * Class that takes care that the value of the composer and the textarea is always in sync 8609 */ 8610(function(wysihtml5) { 8611 var INTERVAL = 400; 8612 8613 wysihtml5.views.Synchronizer = Base.extend( 8614 /** @scope wysihtml5.views.Synchronizer.prototype */ { 8615 8616 constructor: function(editor, textarea, composer) { 8617 this.editor = editor; 8618 this.textarea = textarea; 8619 this.composer = composer; 8620 8621 this._observe(); 8622 }, 8623 8624 /** 8625 * Sync html from composer to textarea 8626 * Takes care of placeholders 8627 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea 8628 */ 8629 fromComposerToTextarea: function(shouldParseHtml) { 8630 this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue()).trim(), shouldParseHtml); 8631 }, 8632 8633 /** 8634 * Sync value of textarea to composer 8635 * Takes care of placeholders 8636 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer 8637 */ 8638 fromTextareaToComposer: function(shouldParseHtml) { 8639 var textareaValue = this.textarea.getValue(); 8640 if (textareaValue) { 8641 this.composer.setValue(textareaValue, shouldParseHtml); 8642 } else { 8643 this.composer.clear(); 8644 this.editor.fire("set_placeholder"); 8645 } 8646 }, 8647 8648 /** 8649 * Invoke syncing based on view state 8650 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea 8651 */ 8652 sync: function(shouldParseHtml) { 8653 if (this.editor.currentView.name === "textarea") { 8654 this.fromTextareaToComposer(shouldParseHtml); 8655 } else { 8656 this.fromComposerToTextarea(shouldParseHtml); 8657 } 8658 }, 8659 8660 /** 8661 * Initializes interval-based syncing 8662 * also makes sure that on-submit the composer's content is synced with the textarea 8663 * immediately when the form gets submitted 8664 */ 8665 _observe: function() { 8666 var interval, 8667 that = this, 8668 form = this.textarea.element.form, 8669 startInterval = function() { 8670 interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL); 8671 }, 8672 stopInterval = function() { 8673 clearInterval(interval); 8674 interval = null; 8675 }; 8676 8677 startInterval(); 8678 8679 if (form) { 8680 // If the textarea is in a form make sure that after onreset and onsubmit the composer 8681 // has the correct state 8682 wysihtml5.dom.observe(form, "submit", function() { 8683 that.sync(true); 8684 }); 8685 wysihtml5.dom.observe(form, "reset", function() { 8686 setTimeout(function() { that.fromTextareaToComposer(); }, 0); 8687 }); 8688 } 8689 8690 this.editor.observe("change_view", function(view) { 8691 if (view === "composer" && !interval) { 8692 that.fromTextareaToComposer(true); 8693 startInterval(); 8694 } else if (view === "textarea") { 8695 that.fromComposerToTextarea(true); 8696 stopInterval(); 8697 } 8698 }); 8699 8700 this.editor.observe("destroy:composer", stopInterval); 8701 } 8702 }); 8703})(wysihtml5); 8704wysihtml5.views.Textarea = wysihtml5.views.View.extend( 8705 /** @scope wysihtml5.views.Textarea.prototype */ { 8706 name: "textarea", 8707 8708 constructor: function(parent, textareaElement, config) { 8709 this.base(parent, textareaElement, config); 8710 8711 this._observe(); 8712 }, 8713 8714 clear: function() { 8715 this.element.value = ""; 8716 }, 8717 8718 getValue: function(parse) { 8719 var value = this.isEmpty() ? "" : this.element.value; 8720 if (parse) { 8721 value = this.parent.parse(value); 8722 } 8723 return value; 8724 }, 8725 8726 setValue: function(html, parse) { 8727 if (parse) { 8728 html = this.parent.parse(html); 8729 } 8730 this.element.value = html; 8731 }, 8732 8733 hasPlaceholderSet: function() { 8734 var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element), 8735 placeholderText = this.element.getAttribute("placeholder") || null, 8736 value = this.element.value, 8737 isEmpty = !value; 8738 return (supportsPlaceholder && isEmpty) || (value === placeholderText); 8739 }, 8740 8741 isEmpty: function() { 8742 return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet(); 8743 }, 8744 8745 _observe: function() { 8746 var element = this.element, 8747 parent = this.parent, 8748 eventMapping = { 8749 focusin: "focus", 8750 focusout: "blur" 8751 }, 8752 /** 8753 * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events 8754 * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai 8755 */ 8756 events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"]; 8757 8758 parent.observe("beforeload", function() { 8759 wysihtml5.dom.observe(element, events, function(event) { 8760 var eventName = eventMapping[event.type] || event.type; 8761 parent.fire(eventName).fire(eventName + ":textarea"); 8762 }); 8763 8764 wysihtml5.dom.observe(element, ["paste", "drop"], function() { 8765 setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0); 8766 }); 8767 });
8768 } 8769});/** 8770 * Toolbar Dialog 8771 * 8772 * @param {Element} link The toolbar link which causes the dialog to show up 8773 * @param {Element} container The dialog container 8774 * 8775 * @example 8776 * <!-- Toolbar link --> 8777 * <a data-wysihtml5-command="insertImage">insert an image</a> 8778 * 8779 * <!-- Dialog --> 8780 * <div data-wysihtml5-dialog="insertImage" style="display: none;"> 8781 * <label> 8782 * URL: <input data-wysihtml5-dialog-field="src" value="http://"> 8783 * </label> 8784 * <label> 8785 * Alternative text: <input data-wysihtml5-dialog-field="alt" value=""> 8786 * </label> 8787 * </div> 8788 * 8789 * <script> 8790 * var dialog = new wysihtml5.toolbar.Dialog( 8791 * document.querySelector("[data-wysihtml5-command='insertImage']"), 8792 * document.querySelector("[data-wysihtml5-dialog='insertImage']") 8793 * ); 8794 * dialog.observe("save", function(attributes) { 8795 * // do something 8796 * }); 8797 * </script> 8798 */ 8799(function(wysihtml5) { 8800 var dom = wysihtml5.dom, 8801 CLASS_NAME_OPENED = "wysihtml5-command-dialog-opened", 8802 SELECTOR_FORM_ELEMENTS = "input, select, textarea", 8803 SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]", 8804 ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field"; 8805 8806 8807 wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend( 8808 /** @scope wysihtml5.toolbar.Dialog.prototype */ { 8809 constructor: function(link, container) { 8810 this.link = link; 8811 this.container = container; 8812 }, 8813 8814 _observe: function() { 8815 if (this._observed) { 8816 return; 8817 } 8818 8819 var that = this, 8820 callbackWrapper = function(event) { 8821 var attributes = that._serialize(); 8822 if (attributes == that.elementToChange) { 8823 that.fire("edit", attributes); 8824 } else { 8825 that.fire("save", attributes); 8826 } 8827 that.hide(); 8828 event.preventDefault(); 8829 event.stopPropagation(); 8830 }; 8831 8832 dom.observe(that.link, "click", function(event) { 8833 if (dom.hasClass(that.link, CLASS_NAME_OPENED)) { 8834 setTimeout(function() { that.hide(); }, 0); 8835 } 8836 }); 8837 8838 dom.observe(this.container, "keydown", function(event) { 8839 var keyCode = event.keyCode; 8840 if (keyCode === wysihtml5.ENTER_KEY) { 8841 callbackWrapper(event); 8842 } 8843 if (keyCode === wysihtml5.ESCAPE_KEY) { 8844 that.hide(); 8845 } 8846 }); 8847 8848 dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper); 8849 8850 dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) { 8851 that.fire("cancel"); 8852 that.hide(); 8853 event.preventDefault(); 8854 event.stopPropagation(); 8855 }); 8856 8857 var formElements = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS), 8858 i = 0, 8859 length = formElements.length, 8860 _clearInterval = function() { clearInterval(that.interval); }; 8861 for (; i<length; i++) { 8862 dom.observe(formElements[i], "change", _clearInterval); 8863 } 8864 8865 this._observed = true; 8866 }, 8867 8868 /** 8869 * Grabs all fields in the dialog and puts them in key=>value style in an object which 8870 * then gets returned 8871 */ 8872 _serialize: function() { 8873 var data = this.elementToChange || {}, 8874 fields = this.container.querySelectorAll(SELECTOR_FIELDS), 8875 length = fields.length, 8876 i = 0; 8877 for (; i<length; i++) { 8878 data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value; 8879 } 8880 return data; 8881 }, 8882 8883 /** 8884 * Takes the attributes of the "elementToChange" 8885 * and inserts them in their corresponding dialog input fields 8886 * 8887 * Assume the "elementToChange" looks like this: 8888 * <a href="http://www.google.com" target="_blank">foo</a> 8889 * 8890 * and we have the following dialog: 8891 * <input type="text" data-wysihtml5-dialog-field="href" value=""> 8892 * <input type="text" data-wysihtml5-dialog-field="target" value=""> 8893 * 8894 * after calling _interpolate() the dialog will look like this 8895 * <input type="text" data-wysihtml5-dialog-field="href" value="http://www.google.com"> 8896 * <input type="text" data-wysihtml5-dialog-field="target" value="_blank"> 8897 * 8898 * Basically it adopted the attribute values into the corresponding input fields 8899 * 8900 */ 8901 _interpolate: function(avoidHiddenFields) { 8902 var field, 8903 fieldName, 8904 newValue, 8905 focusedElement = document.querySelector(":focus"), 8906 fields = this.container.querySelectorAll(SELECTOR_FIELDS), 8907 length = fields.length, 8908 i = 0; 8909 for (; i<length; i++) { 8910 field = fields[i]; 8911 8912 // Never change elements where the user is currently typing in 8913 if (field === focusedElement) { 8914 continue; 8915 } 8916 8917 // Don't update hidden fields 8918 // See https://github.com/xing/wysihtml5/pull/14 8919 if (avoidHiddenFields && field.type === "hidden") { 8920 continue; 8921 } 8922 8923 fieldName = field.getAttribute(ATTRIBUTE_FIELDS); 8924 newValue = this.elementToChange ? (this.elementToChange[fieldName] || "") : field.defaultValue; 8925 field.value = newValue; 8926 } 8927 }, 8928 8929 /** 8930 * Show the dialog element 8931 */ 8932 show: function(elementToChange) { 8933 var that = this, 8934 firstField = this.container.querySelector(SELECTOR_FORM_ELEMENTS); 8935 this.elementToChange = elementToChange; 8936 this._observe(); 8937 this._interpolate(); 8938 if (elementToChange) { 8939 this.interval = setInterval(function() { that._interpolate(true); }, 500); 8940 } 8941 dom.addClass(this.link, CLASS_NAME_OPENED); 8942 this.container.style.display = ""; 8943 this.fire("show"); 8944 if (firstField && !elementToChange) { 8945 try { 8946 firstField.focus(); 8947 } catch(e) {} 8948 } 8949 }, 8950 8951 /** 8952 * Hide the dialog element 8953 */ 8954 hide: function() { 8955 clearInterval(this.interval); 8956 this.elementToChange = null; 8957 dom.removeClass(this.link, CLASS_NAME_OPENED); 8958 this.container.style.display = "none"; 8959 this.fire("hide"); 8960 } 8961 }); 8962})(wysihtml5); 8963/** 8964 * Converts speech-to-text and inserts this into the editor 8965 * As of now (2011/03/25) this only is supported in Chrome >= 11 8966 * 8967 * Note that it sends the recorded audio to the google speech recognition api: 8968 * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec 8969 * 8970 * Current HTML5 draft can be found here 8971 * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html 8972 * 8973 * "Accessing Google Speech API Chrome 11" 8974 * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/ 8975 */ 8976(function(wysihtml5) { 8977 var dom = wysihtml5.dom; 8978 8979 var linkStyles = { 8980 position: "relative" 8981 }; 8982 8983 var wrapperStyles = { 8984 left: 0, 8985 margin: 0, 8986 opacity: 0, 8987 overflow: "hidden", 8988 padding: 0, 8989 position: "absolute",
8990 top: 0, 8991 zIndex: 1 8992 }; 8993 8994 var inputStyles = { 8995 cursor: "inherit", 8996 fontSize: "50px", 8997 height: "50px", 8998 marginTop: "-25px", 8999 outline: 0, 9000 padding: 0, 9001 position: "absolute", 9002 right: "-4px", 9003 top: "50%" 9004 }; 9005 9006 var inputAttributes = { 9007 "x-webkit-speech": "", 9008 "speech": "" 9009 }; 9010 9011 wysihtml5.toolbar.Speech = function(parent, link) { 9012 var input = document.createElement("input"); 9013 if (!wysihtml5.browser.supportsSpeechApiOn(input)) { 9014 link.style.display = "none"; 9015 return; 9016 } 9017 9018 var wrapper = document.createElement("div"); 9019 9020 wysihtml5.lang.object(wrapperStyles).merge({ 9021 width: link.offsetWidth + "px", 9022 height: link.offsetHeight + "px" 9023 }); 9024 9025 dom.insert(input).into(wrapper); 9026 dom.insert(wrapper).into(link); 9027 9028 dom.setStyles(inputStyles).on(input); 9029 dom.setAttributes(inputAttributes).on(input) 9030 9031 dom.setStyles(wrapperStyles).on(wrapper); 9032 dom.setStyles(linkStyles).on(link); 9033 9034 var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange"; 9035 dom.observe(input, eventName, function() { 9036 parent.execCommand("insertText", input.value); 9037 input.value = ""; 9038 }); 9039 9040 dom.observe(input, "click", function(event) { 9041 if (dom.hasClass(link, "wysihtml5-command-disabled")) { 9042 event.preventDefault(); 9043 } 9044 9045 event.stopPropagation(); 9046 }); 9047 }; 9048})(wysihtml5);/** 9049 * Toolbar 9050 * 9051 * @param {Object} parent Reference to instance of Editor instance 9052 * @param {Element} container Reference to the toolbar container element 9053 * 9054 * @example 9055 * <div id="toolbar"> 9056 * <a data-wysihtml5-command="createLink">insert link</a> 9057 * <a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">insert h1</a> 9058 * </div> 9059 * 9060 * <script> 9061 * var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar")); 9062 * </script> 9063 */ 9064(function(wysihtml5) { 9065 var CLASS_NAME_COMMAND_DISABLED = "wysihtml5-command-disabled", 9066 CLASS_NAME_COMMANDS_DISABLED = "wysihtml5-commands-disabled", 9067 CLASS_NAME_COMMAND_ACTIVE = "wysihtml5-command-active", 9068 CLASS_NAME_ACTION_ACTIVE = "wysihtml5-action-active", 9069 dom = wysihtml5.dom; 9070 9071 wysihtml5.toolbar.Toolbar = Base.extend( 9072 /** @scope wysihtml5.toolbar.Toolbar.prototype */ { 9073 constructor: function(editor, container) { 9074 this.editor = editor; 9075 this.container = typeof(container) === "string" ? document.getElementById(container) : container; 9076 this.composer = editor.composer; 9077 9078 this._getLinks("command"); 9079 this._getLinks("action"); 9080 9081 this._observe(); 9082 this.show(); 9083 9084 var speechInputLinks = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"), 9085 length = speechInputLinks.length, 9086 i = 0; 9087 for (; i<length; i++) { 9088 new wysihtml5.toolbar.Speech(this, speechInputLinks[i]); 9089 } 9090 }, 9091 9092 _getLinks: function(type) { 9093 var links = this[type + "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("[data-wysihtml5-" + type + "]")).get(), 9094 length = links.length, 9095 i = 0, 9096 mapping = this[type + "Mapping"] = {}, 9097 link, 9098 group, 9099 name, 9100 value, 9101 dialog; 9102 for (; i<length; i++) { 9103 link = links[i]; 9104 name = link.getAttribute("data-wysihtml5-" + type); 9105 value = link.getAttribute("data-wysihtml5-" + type + "-value"); 9106 group = this.container.querySelector("[data-wysihtml5-" + type + "-group='" + name + "']"); 9107 dialog = this._getDialog(link, name); 9108 9109 mapping[name + ":" + value] = { 9110 link: link, 9111 group: group, 9112 name: name, 9113 value: value, 9114 dialog: dialog, 9115 state: false 9116 }; 9117 } 9118 }, 9119 9120 _getDialog: function(link, command) { 9121 var that = this, 9122 dialogElement = this.container.querySelector("[data-wysihtml5-dialog='" + command + "']"), 9123 dialog, 9124 caretBookmark; 9125 9126 if (dialogElement) { 9127 dialog = new wysihtml5.toolbar.Dialog(link, dialogElement); 9128 9129 dialog.observe("show", function() { 9130 caretBookmark = that.composer.selection.getBookmark(); 9131
9132 that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); 9133 }); 9134 9135 dialog.observe("save", function(attributes) { 9136 if (caretBookmark) { 9137 that.composer.selection.setBookmark(caretBookmark); 9138 } 9139 that._execCommand(command, attributes); 9140 9141 that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); 9142 }); 9143 9144 dialog.observe("cancel", function() { 9145 that.editor.focus(false); 9146 that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); 9147 }); 9148 } 9149 return dialog; 9150 }, 9151 9152 /** 9153 * @example 9154 * var toolbar = new wysihtml5.Toolbar(); 9155 * // Insert a <blockquote> element or wrap current selection in <blockquote> 9156 * toolbar.execCommand("formatBlock", "blockquote"); 9157 */ 9158 execCommand: function(command, commandValue) { 9159 if (this.commandsDisabled) { 9160 return; 9161 } 9162 9163 var commandObj = this.commandMapping[command + ":" + commandValue]; 9164 9165 // Show dialog when available 9166 if (commandObj && commandObj.dialog && !commandObj.state) { 9167 commandObj.dialog.show(); 9168 } else { 9169 this._execCommand(command, commandValue); 9170 } 9171 }, 9172 9173 _execCommand: function(command, commandValue) { 9174 // Make sure that composer is focussed (false => don't move caret to the end) 9175 this.editor.focus(false); 9176 9177 this.composer.commands.exec(command, commandValue); 9178 this._updateLinkStates(); 9179 }, 9180 9181 execAction: function(action) { 9182 var editor = this.editor; 9183 switch(action) { 9184 case "change_view": 9185 if (editor.currentView === editor.textarea) { 9186 editor.fire("change_view", "composer"); 9187 } else { 9188 editor.fire("change_view", "textarea"); 9189 } 9190 break; 9191 } 9192 }, 9193 9194 _observe: function() { 9195 var that = this, 9196 editor = this.editor, 9197 container = this.container, 9198 links = this.commandLinks.concat(this.actionLinks), 9199 length = links.length, 9200 i = 0; 9201 9202 for (; i<length; i++) { 9203 // 'javascript:;' and unselectable=on Needed for IE, but done in all browsers to make sure that all get the same css applied 9204 // (you know, a:link { ... } doesn't match anchors with missing href attribute) 9205 dom.setAttributes({ 9206 href: "javascript:;", 9207 unselectable: "on" 9208 }).on(links[i]); 9209 } 9210 9211 // Needed for opera 9212 dom.delegate(container, "[data-wysihtml5-command]", "mousedown", function(event) { event.preventDefault(); }); 9213 9214 dom.delegate(container, "[data-wysihtml5-command]", "click", function(event) { 9215 var link = this, 9216 command = link.getAttribute("data-wysihtml5-command"), 9217 commandValue = link.getAttribute("data-wysihtml5-command-value"); 9218 that.execCommand(command, commandValue); 9219 event.preventDefault(); 9220 }); 9221 9222 dom.delegate(container, "[data-wysihtml5-action]", "click", function(event) { 9223 var action = this.getAttribute("data-wysihtml5-action"); 9224 that.execAction(action); 9225 event.preventDefault(); 9226 }); 9227 9228 editor.observe("focus:composer", function() { 9229 that.bookmark = null; 9230 clearInterval(that.interval); 9231 that.interval = setInterval(function() { that._updateLinkStates(); }, 500); 9232 }); 9233 9234 editor.observe("blur:composer", function() { 9235 clearInterval(that.interval); 9236 }); 9237 9238 editor.observe("destroy:composer", function() { 9239 clearInterval(that.interval); 9240 }); 9241 9242 editor.observe("change_view", function(currentView) { 9243 // Set timeout needed in order to let the blur event fire first 9244 setTimeout(function() { 9245 that.commandsDisabled = (currentView !== "composer"); 9246 that._updateLinkStates(); 9247 if (that.commandsDisabled) { 9248 dom.addClass(container, CLASS_NAME_COMMANDS_DISABLED); 9249 } else { 9250 dom.removeClass(container, CLASS_NAME_COMMANDS_DISABLED); 9251 } 9252 }, 0); 9253 }); 9254 }, 9255 9256 _updateLinkStates: function() { 9257 var element = this.composer.element, 9258 commandMapping = this.commandMapping, 9259 actionMapping = this.actionMapping, 9260 i, 9261 state, 9262 action, 9263 command; 9264 // every millisecond counts... this is executed quite often 9265 for (i in commandMapping) { 9266 command = commandMapping[i]; 9267 if (this.commandsDisabled) { 9268 state = false; 9269 dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE); 9270 if (command.group) { 9271 dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE); 9272 } 9273 if (command.dialog) { 9274 command.dialog.hide(); 9275 } 9276 } else { 9277 state = this.composer.commands.state(command.name, command.value); 9278 if (wysihtml5.lang.object(state).isArray()) { 9279 // Grab first and only object/element in state array, otherwise convert state into boolean 9280 // to avoid showing a dialog for multiple selected elements which may have different attributes 9281 // eg. when two links with different href are selected, the state will be an array consisting of both link elements 9282 // but the dialog interface can only update one 9283 state = state.length === 1 ? state[0] : true; 9284 } 9285 dom.removeClass(command.link, CLASS_NAME_COMMAND_DISABLED); 9286 if (command.group) { 9287 dom.removeClass(command.group, CLASS_NAME_COMMAND_DISABLED); 9288 } 9289 } 9290 9291 if (command.state === state) { 9292 continue; 9293 } 9294 9295 command.state = state; 9296 if (state) { 9297 dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE); 9298 if (command.group) { 9299 dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE); 9300 } 9301 if (command.dialog) { 9302 if (typeof(state) === "object") { 9303 command.dialog.show(state); 9304 } else { 9305 command.dialog.hide(); 9306 } 9307 } 9308 } else { 9309 dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE); 9310 if (command.group) { 9311 dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE); 9312 } 9313 if (command.dialog) { 9314 command.dialog.hide(); 9315 } 9316 } 9317 } 9318 9319 for (i in actionMapping) { 9320 action = actionMapping[i]; 9321 9322 if (action.name === "change_view") { 9323 action.state = this.editor.currentView === this.editor.textarea; 9324 if (action.state) { 9325 dom.addClass(action.link, CLASS_NAME_ACTION_ACTIVE); 9326 } else { 9327 dom.removeClass(action.link, CLASS_NAME_ACTION_ACTIVE); 9328 } 9329 } 9330 } 9331 }, 9332 9333 show: function() { 9334 this.container.style.display = ""; 9335 }, 9336 9337 hide: function() { 9338 this.container.style.display = "none"; 9339 } 9340 }); 9341 9342})(wysihtml5); 9343/** 9344 * WYSIHTML5 Editor 9345 * 9346 * @param {Element} textareaElement Reference to the textarea which should be turned into a rich text interface 9347 * @param {Object} [config] See defaultConfig object below for explanation of each individual config option 9348 * 9349 * @events 9350 * load 9351 * beforeload (for internal use only) 9352 * focus 9353 * focus:composer 9354 * focus:textarea 9355 * blur 9356 * blur:composer 9357 * blur:textarea 9358 * change 9359 * change:composer 9360 * change:textarea 9361 * paste 9362 * paste:composer 9363 * paste:textarea 9364 * newword:composer 9365 * destroy:composer 9366 * undo:composer 9367 * redo:composer
9368 * beforecommand:composer 9369 * aftercommand:composer 9370 * change_view 9371 */ 9372(function(wysihtml5) { 9373 var undef; 9374 9375 var defaultConfig = { 9376 // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body 9377 name: undef, 9378 // Whether the editor should look like the textarea (by adopting styles) 9379 style: true, 9380 // Id of the toolbar element, pass falsey value if you don't want any toolbar logic 9381 toolbar: undef, 9382 // Whether urls, entered by the user should automatically become clickable-links 9383 autoLink: true, 9384 // Object which includes parser rules to apply when html gets inserted via copy & paste 9385 // See parser_rules/*.js for examples 9386 parserRules: { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} }, 9387 // Parser method to use when the user inserts content via copy & paste 9388 parser: wysihtml5.dom.parse, 9389 // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option 9390 composerClassName: "wysihtml5-editor", 9391 // Class name to add to the body when the wysihtml5 editor is supported 9392 bodyClassName: "wysihtml5-supported",
9393 // Array (or single string) of stylesheet urls to be loaded in the editor's iframe 9394 stylesheets: [], 9395 // Placeholder text to use, defaults to the placeholder attribute on the textarea element 9396 placeholderText: undef, 9397 // Whether the composer should allow the user to manually resize images, tables etc. 9398 allowObjectResizing: true, 9399 // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5) 9400 supportTouchDevices: true 9401 }; 9402 9403 wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend( 9404 /** @scope wysihtml5.Editor.prototype */ { 9405 constructor: function(textareaElement, config) { 9406 this.textareaElement = typeof(textareaElement) === "string" ? document.getElementById(textareaElement) : textareaElement; 9407 this.config = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get(); 9408 this.textarea = new wysihtml5.views.Textarea(this, this.textareaElement, this.config); 9409 this.currentView = this.textarea; 9410 this._isCompatible = wysihtml5.browser.supported(); 9411 9412 // Sort out unsupported/unwanted browsers here 9413 if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) { 9414 var that = this; 9415 setTimeout(function() { that.fire("beforeload").fire("load"); }, 0); 9416 return; 9417 } 9418 9419 // Add class name to body, to indicate that the editor is supported 9420 wysihtml5.dom.addClass(document.body, this.config.bodyClassName); 9421 9422 this.composer = new wysihtml5.views.Composer(this, this.textareaElement, this.config); 9423 this.currentView = this.composer; 9424 9425 if (typeof(this.config.parser) === "function") { 9426 this._initParser(); 9427 } 9428 9429 this.observe("beforeload", function() { 9430 this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer); 9431 if (this.config.toolbar) { 9432 this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar); 9433 } 9434 }); 9435 9436 try { 9437 console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5"); 9438 } catch(e) {} 9439 }, 9440 9441 isCompatible: function() { 9442 return this._isCompatible; 9443 }, 9444 9445 clear: function() { 9446 this.currentView.clear(); 9447 return this; 9448 }, 9449 9450 getValue: function(parse) { 9451 return this.currentView.getValue(parse); 9452 }, 9453 9454 setValue: function(html, parse) { 9455 if (!html) { 9456 return this.clear(); 9457 } 9458 this.currentView.setValue(html, parse); 9459 return this; 9460 }, 9461 9462 focus: function(setToEnd) { 9463 this.currentView.focus(setToEnd); 9464 return this; 9465 }, 9466 9467 /** 9468 * Deactivate editor (make it readonly) 9469 */ 9470 disable: function() { 9471 this.currentView.disable(); 9472 return this; 9473 }, 9474 9475 /** 9476 * Activate editor 9477 */ 9478 enable: function() { 9479 this.currentView.enable(); 9480 return this; 9481 }, 9482 9483 isEmpty: function() { 9484 return this.currentView.isEmpty(); 9485 }, 9486 9487 hasPlaceholderSet: function() { 9488 return this.currentView.hasPlaceholderSet(); 9489 }, 9490 9491 parse: function(htmlOrElement) { 9492 var returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), true); 9493 if (typeof(htmlOrElement) === "object") { 9494 wysihtml5.quirks.redraw(htmlOrElement); 9495 } 9496 return returnValue; 9497 }, 9498 9499 /** 9500 * Prepare html parser logic 9501 * - Observes for paste and drop 9502 */ 9503 _initParser: function() { 9504 this.observe("paste:composer", function() { 9505 var keepScrollPosition = true, 9506 that = this; 9507 that.composer.selection.executeAndRestore(function() { 9508 wysihtml5.quirks.cleanPastedHTML(that.composer.element); 9509 that.parse(that.composer.element); 9510 }, keepScrollPosition); 9511 }); 9512 9513 this.observe("paste:textarea", function() { 9514 var value = this.textarea.getValue(), 9515 newValue; 9516 newValue = this.parse(value); 9517 this.textarea.setValue(newValue); 9518 }); 9519 } 9520 }); 9521})(wysihtml5);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.