github.com/insionng/yougam@v0.0.0-20170714101924-2bc18d833463/public/root/js/wysihtml5-0.3.0.js (about) 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 */ 11 var 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 */ 44 window['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 })(); 329 rangy.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) : ""; 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; 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 && (isNonTextPartiallySelected(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", "focusNode"] && 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; 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(); 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 3252 var Base = function() { 3253 // dummy 3254 }; 3255 3256 Base.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 3300 Base.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 3352 Base = 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 3386 * Detect browser support for specific features 3387 */ 3388 wysihtml5.browser = (function() { 3389 var userAgent = navigator.userAgent, 3390 testElement = document.createElement("div"), 3391 // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect 3392 isIE = userAgent.indexOf("MSIE") !== -1 && userAgent.indexOf("Opera") === -1, 3393 isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1, 3394 isWebKit = userAgent.indexOf("AppleWebKit/") !== -1, 3395 isChrome = userAgent.indexOf("Chrome/") !== -1, 3396 isOpera = userAgent.indexOf("Opera/") !== -1; 3397 3398 function iosVersion(userAgent) { 3399 return ((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [, 0])[1]; 3400 } 3401 3402 return { 3403 // Static variable needed, publicly accessible, to be able override it in unit tests 3404 USER_AGENT: userAgent, 3405 3406 /** 3407 * Exclude browsers that are not capable of displaying and handling 3408 * contentEditable as desired: 3409 * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable 3410 * - IE < 8 create invalid markup and crash randomly from time to time 3411 * 3412 * @return {Boolean} 3413 */ 3414 supported: function() { 3415 var userAgent = this.USER_AGENT.toLowerCase(), 3416 // Essential for making html elements editable 3417 hasContentEditableSupport = "contentEditable" in testElement, 3418 // Following methods are needed in order to interact with the contentEditable area 3419 hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState, 3420 // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+ 3421 hasQuerySelectorSupport = document.querySelector && document.querySelectorAll, 3422 // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05) 3423 isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1; 3424 3425 return hasContentEditableSupport 3426 && hasEditingApiSupport 3427 && hasQuerySelectorSupport 3428 && !isIncompatibleMobileBrowser; 3429 }, 3430 3431 isTouchDevice: function() { 3432 return this.supportsEvent("touchmove"); 3433 }, 3434 3435 isIos: function() { 3436 var userAgent = this.USER_AGENT.toLowerCase(); 3437 return userAgent.indexOf("webkit") !== -1 && userAgent.indexOf("mobile") !== -1; 3438 }, 3439 3440 /** 3441 * Whether the browser supports sandboxed iframes 3442 * Currently only IE 6+ offers such feature <iframe security="restricted"> 3443 * 3444 * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx 3445 * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx 3446 * 3447 * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage) 3448 */ 3449 supportsSandboxedIframes: function() { 3450 return isIE; 3451 }, 3452 3453 /** 3454 * IE6+7 throw a mixed content warning when the src of an iframe 3455 * is empty/unset or about:blank 3456 * window.querySelector is implemented as of IE8 3457 */ 3458 throwsMixedContentWarningWhenIframeSrcIsEmpty: function() { 3459 return !("querySelector" in document); 3460 }, 3461 3462 /** 3463 * Whether the caret is correctly displayed in contentEditable elements 3464 * Firefox sometimes shows a huge caret in the beginning after focusing 3465 */ 3466 displaysCaretInEmptyContentEditableCorrectly: function() { 3467 return !isGecko; 3468 }, 3469 3470 /** 3471 * Opera and IE are the only browsers who offer the css value 3472 * in the original unit, thx to the currentStyle object 3473 * All other browsers provide the computed style in px via window.getComputedStyle 3474 */ 3475 hasCurrentStyleProperty: function() { 3476 return "currentStyle" in testElement; 3477 }, 3478 3479 /** 3480 * Whether the browser inserts a <br> when pressing enter in a contentEditable element 3481 */ 3482 insertsLineBreaksOnReturn: function() { 3483 return isGecko; 3484 }, 3485 3486 supportsPlaceholderAttributeOn: function(element) { 3487 return "placeholder" in element; 3488 }, 3489 3490 supportsEvent: function(eventName) { 3491 return "on" + eventName in testElement || (function() { 3492 testElement.setAttribute("on" + eventName, "return;"); 3493 return typeof(testElement["on" + eventName]) === "function"; 3494 })(); 3495 }, 3496 3497 /** 3498 * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe 3499 */ 3500 supportsEventsInIframeCorrectly: function() { 3501 return !isOpera; 3502 }, 3503 3504 /** 3505 * Chrome & Safari only fire the ondrop/ondragend/... events when the ondragover event is cancelled 3506 * with event.preventDefault 3507 * Firefox 3.6 fires those events anyway, but the mozilla doc says that the dragover/dragenter event needs 3508 * to be cancelled 3509 */ 3510 firesOnDropOnlyWhenOnDragOverIsCancelled: function() { 3511 return isWebKit || isGecko; 3512 }, 3513 3514 /** 3515 * Whether the browser supports the event.dataTransfer property in a proper way 3516 */ 3517 supportsDataTransfer: function() { 3518 try { 3519 // Firefox doesn't support dataTransfer in a safe way, it doesn't strip script code in the html payload (like Chrome does) 3520 return isWebKit && (window.Clipboard || window.DataTransfer).prototype.getData; 3521 } catch(e) { 3522 return false; 3523 } 3524 }, 3525 3526 /** 3527 * Everything below IE9 doesn't know how to treat HTML5 tags 3528 * 3529 * @param {Object} context The document object on which to check HTML5 support 3530 * 3531 * @example 3532 * wysihtml5.browser.supportsHTML5Tags(document); 3533 */ 3534 supportsHTML5Tags: function(context) { 3535 var element = context.createElement("div"), 3536 html5 = "<article>foo</article>"; 3537 element.innerHTML = html5; 3538 return element.innerHTML.toLowerCase() === html5; 3539 }, 3540 3541 /** 3542 * Checks whether a document supports a certain queryCommand 3543 * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree 3544 * in oder to report correct results 3545 * 3546 * @param {Object} doc Document object on which to check for a query command 3547 * @param {String} command The query command to check for 3548 * @return {Boolean} 3549 * 3550 * @example 3551 * wysihtml5.browser.supportsCommand(document, "bold"); 3552 */ 3553 supportsCommand: (function() { 3554 // Following commands are supported but contain bugs in some browsers 3555 var buggyCommands = { 3556 // formatBlock fails with some tags (eg. <blockquote>) 3557 "formatBlock": isIE, 3558 // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets 3559 // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>) 3560 // IE and Opera act a bit different here as they convert the entire content of the current block element into a list 3561 "insertUnorderedList": isIE || isOpera || isWebKit, 3562 "insertOrderedList": isIE || isOpera || isWebKit 3563 }; 3564 3565 // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands 3566 var supported = { 3567 "insertHTML": isGecko 3568 }; 3569 3570 return function(doc, command) { 3571 var isBuggy = buggyCommands[command]; 3572 if (!isBuggy) { 3573 // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled 3574 try { 3575 return doc.queryCommandSupported(command); 3576 } catch(e1) {} 3577 3578 try { 3579 return doc.queryCommandEnabled(command); 3580 } catch(e2) { 3581 return !!supported[command]; 3582 } 3583 } 3584 return false; 3585 }; 3586 })(), 3587 3588 /** 3589 * IE: URLs starting with: 3590 * www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://, 3591 * nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url: 3592 * will automatically be auto-linked when either the user inserts them via copy&paste or presses the 3593 * space bar when the caret is directly after such an url. 3594 * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll 3595 * (related blog post on msdn 3596 * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx). 3597 */ 3598 doesAutoLinkingInContentEditable: function() { 3599 return isIE; 3600 }, 3601 3602 /** 3603 * As stated above, IE auto links urls typed into contentEditable elements 3604 * Since IE9 it's possible to prevent this behavior 3605 */ 3606 canDisableAutoLinking: function() { 3607 return this.supportsCommand(document, "AutoUrlDetect"); 3608 }, 3609 3610 /** 3611 * IE leaves an empty paragraph in the contentEditable element after clearing it 3612 * Chrome/Safari sometimes an empty <div> 3613 */ 3614 clearsContentEditableCorrectly: function() { 3615 return isGecko || isOpera || isWebKit; 3616 }, 3617 3618 /** 3619 * IE gives wrong results for getAttribute 3620 */ 3621 supportsGetAttributeCorrectly: function() { 3622 var td = document.createElement("td"); 3623 return td.getAttribute("rowspan") != "1"; 3624 }, 3625 3626 /** 3627 * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them. 3628 * Chrome and Safari both don't support this 3629 */ 3630 canSelectImagesInContentEditable: function() { 3631 return isGecko || isIE || isOpera; 3632 }, 3633 3634 /** 3635 * When the caret is in an empty list (<ul><li>|</li></ul>) which is the first child in an contentEditable container 3636 * pressing backspace doesn't remove the entire list as done in other browsers 3637 */ 3638 clearsListsInContentEditableCorrectly: function() { 3639 return isGecko || isIE || isWebKit; 3640 }, 3641 3642 /** 3643 * All browsers except Safari and Chrome automatically scroll the range/caret position into view 3644 */ 3645 autoScrollsToCaret: function() { 3646 return !isWebKit; 3647 }, 3648 3649 /** 3650 * Check whether the browser automatically closes tags that don't need to be opened 3651 */ 3652 autoClosesUnclosedTags: function() { 3653 var clonedTestElement = testElement.cloneNode(false), 3654 returnValue, 3655 innerHTML; 3656 3657 clonedTestElement.innerHTML = "<p><div></div>"; 3658 innerHTML = clonedTestElement.innerHTML.toLowerCase(); 3659 returnValue = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>"; 3660 3661 // Cache result by overwriting current function 3662 this.autoClosesUnclosedTags = function() { return returnValue; }; 3663 3664 return returnValue; 3665 }, 3666 3667 /** 3668 * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists 3669 */ 3670 supportsNativeGetElementsByClassName: function() { 3671 return String(document.getElementsByClassName).indexOf("[native code]") !== -1; 3672 }, 3673 3674 /** 3675 * As of now (19.04.2011) only supported by Firefox 4 and Chrome 3676 * See https://developer.mozilla.org/en/DOM/Selection/modify 3677 */ 3678 supportsSelectionModify: function() { 3679 return "getSelection" in window && "modify" in window.getSelection(); 3680 }, 3681 3682 /** 3683 * Whether the browser supports the classList object for fast className manipulation 3684 * See https://developer.mozilla.org/en/DOM/element.classList 3685 */ 3686 supportsClassList: function() { 3687 return "classList" in testElement; 3688 }, 3689 3690 /** 3691 * Opera needs a white space after a <br> in order to position the caret correctly 3692 */ 3693 needsSpaceAfterLineBreak: function() { 3694 return isOpera; 3695 }, 3696 3697 /** 3698 * Whether the browser supports the speech api on the given element 3699 * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/ 3700 * 3701 * @example 3702 * var input = document.createElement("input"); 3703 * if (wysihtml5.browser.supportsSpeechApiOn(input)) { 3704 * // ... 3705 * } 3706 */ 3707 supportsSpeechApiOn: function(input) { 3708 var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [, 0]; 3709 return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input); 3710 }, 3711 3712 /** 3713 * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest 3714 * See https://connect.microsoft.com/ie/feedback/details/650112 3715 * or try the POC http://tifftiff.de/ie9_crash/ 3716 */ 3717 crashesWhenDefineProperty: function(property) { 3718 return isIE && (property === "XMLHttpRequest" || property === "XDomainRequest"); 3719 }, 3720 3721 /** 3722 * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element 3723 */ 3724 doesAsyncFocus: function() { 3725 return isIE; 3726 }, 3727 3728 /** 3729 * 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 3730 */ 3731 hasProblemsSettingCaretAfterImg: function() { 3732 return isIE; 3733 }, 3734 3735 hasUndoInContextMenu: function() { 3736 return isGecko || isChrome || isOpera; 3737 } 3738 }; 3739 })();wysihtml5.lang.array = function(arr) { 3740 return { 3741 /** 3742 * Check whether a given object exists in an array 3743 * 3744 * @example 3745 * wysihtml5.lang.array([1, 2]).contains(1); 3746 * // => true 3747 */ 3748 contains: function(needle) { 3749 if (arr.indexOf) { 3750 return arr.indexOf(needle) !== -1; 3751 } else { 3752 for (var i=0, length=arr.length; i<length; i++) { 3753 if (arr[i] === needle) { return true; } 3754 } 3755 return false; 3756 } 3757 }, 3758 3759 /** 3760 * Substract one array from another 3761 * 3762 * @example 3763 * wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]); 3764 * // => [1, 2] 3765 */ 3766 without: function(arrayToSubstract) { 3767 arrayToSubstract = wysihtml5.lang.array(arrayToSubstract); 3768 var newArr = [], 3769 i = 0, 3770 length = arr.length; 3771 for (; i<length; i++) { 3772 if (!arrayToSubstract.contains(arr[i])) { 3773 newArr.push(arr[i]); 3774 } 3775 } 3776 return newArr; 3777 }, 3778 3779 /** 3780 * Return a clean native array 3781 * 3782 * Following will convert a Live NodeList to a proper Array 3783 * @example 3784 * var childNodes = wysihtml5.lang.array(document.body.childNodes).get(); 3785 */ 3786 get: function() { 3787 var i = 0, 3788 length = arr.length, 3789 newArray = []; 3790 for (; i<length; i++) { 3791 newArray.push(arr[i]); 3792 } 3793 return newArray; 3794 } 3795 }; 3796 };wysihtml5.lang.Dispatcher = Base.extend( 3797 /** @scope wysihtml5.lang.Dialog.prototype */ { 3798 observe: function(eventName, handler) { 3799 this.events = this.events || {}; 3800 this.events[eventName] = this.events[eventName] || []; 3801 this.events[eventName].push(handler); 3802 return this; 3803 }, 3804 3805 on: function() { 3806 return this.observe.apply(this, wysihtml5.lang.array(arguments).get()); 3807 }, 3808 3809 fire: function(eventName, payload) { 3810 this.events = this.events || {}; 3811 var handlers = this.events[eventName] || [], 3812 i = 0; 3813 for (; i<handlers.length; i++) { 3814 handlers[i].call(this, payload); 3815 } 3816 return this; 3817 }, 3818 3819 stopObserving: function(eventName, handler) { 3820 this.events = this.events || {}; 3821 var i = 0, 3822 handlers, 3823 newHandlers; 3824 if (eventName) { 3825 handlers = this.events[eventName] || [], 3826 newHandlers = []; 3827 for (; i<handlers.length; i++) { 3828 if (handlers[i] !== handler && handler) { 3829 newHandlers.push(handlers[i]); 3830 } 3831 } 3832 this.events[eventName] = newHandlers; 3833 } else { 3834 // Clean up all events 3835 this.events = {}; 3836 } 3837 return this; 3838 } 3839 });wysihtml5.lang.object = function(obj) { 3840 return { 3841 /** 3842 * @example 3843 * wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get(); 3844 * // => { foo: 1, bar: 2, baz: 3 } 3845 */ 3846 merge: function(otherObj) { 3847 for (var i in otherObj) { 3848 obj[i] = otherObj[i]; 3849 } 3850 return this; 3851 }, 3852 3853 get: function() { 3854 return obj; 3855 }, 3856 3857 /** 3858 * @example 3859 * wysihtml5.lang.object({ foo: 1 }).clone(); 3860 * // => { foo: 1 } 3861 */ 3862 clone: function() { 3863 var newObj = {}, 3864 i; 3865 for (i in obj) { 3866 newObj[i] = obj[i]; 3867 } 3868 return newObj; 3869 }, 3870 3871 /** 3872 * @example 3873 * wysihtml5.lang.object([]).isArray(); 3874 * // => true 3875 */ 3876 isArray: function() { 3877 return Object.prototype.toString.call(obj) === "[object Array]"; 3878 } 3879 }; 3880 };(function() { 3881 var WHITE_SPACE_START = /^\s+/, 3882 WHITE_SPACE_END = /\s+$/; 3883 wysihtml5.lang.string = function(str) { 3884 str = String(str); 3885 return { 3886 /** 3887 * @example 3888 * wysihtml5.lang.string(" foo ").trim(); 3889 * // => "foo" 3890 */ 3891 trim: function() { 3892 return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, ""); 3893 }, 3894 3895 /** 3896 * @example 3897 * wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" }); 3898 * // => "Hello Christopher" 3899 */ 3900 interpolate: function(vars) { 3901 for (var i in vars) { 3902 str = this.replace("#{" + i + "}").by(vars[i]); 3903 } 3904 return str; 3905 }, 3906 3907 /** 3908 * @example 3909 * wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans"); 3910 * // => "Hello Hans" 3911 */ 3912 replace: function(search) { 3913 return { 3914 by: function(replace) { 3915 return str.split(search).join(replace); 3916 } 3917 } 3918 } 3919 }; 3920 }; 3921 })();/** 3922 * Find urls in descendant text nodes of an element and auto-links them 3923 * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/ 3924 * 3925 * @param {Element} element Container element in which to search for urls 3926 * 3927 * @example 3928 * <div id="text-container">Please click here: www.google.com</div> 3929 * <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script> 3930 */ 3931 (function(wysihtml5) { 3932 var /** 3933 * Don't auto-link urls that are contained in the following elements: 3934 */ 3935 IGNORE_URLS_IN = wysihtml5.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]), 3936 /** 3937 * revision 1: 3938 * /(\S+\.{1}[^\s\,\.\!]+)/g 3939 * 3940 * revision 2: 3941 * /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim 3942 * 3943 * put this in the beginning if you don't wan't to match within a word 3944 * (^|[\>\(\{\[\s\>]) 3945 */ 3946 URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi, 3947 TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i, 3948 MAX_DISPLAY_LENGTH = 100, 3949 BRACKETS = { ")": "(", "]": "[", "}": "{" }; 3950 3951 function autoLink(element) { 3952 if (_hasParentThatShouldBeIgnored(element)) { 3953 return element; 3954 } 3955 3956 if (element === element.ownerDocument.documentElement) { 3957 element = element.ownerDocument.body; 3958 } 3959 3960 return _parseNode(element); 3961 } 3962 3963 /** 3964 * This is basically a rebuild of 3965 * the rails auto_link_urls text helper 3966 */ 3967 function _convertUrlsToLinks(str) { 3968 return str.replace(URL_REG_EXP, function(match, url) { 3969 var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "", 3970 opening = BRACKETS[punctuation]; 3971 url = url.replace(TRAILING_CHAR_REG_EXP, ""); 3972 3973 if (url.split(opening).length > url.split(punctuation).length) { 3974 url = url + punctuation; 3975 punctuation = ""; 3976 } 3977 var realUrl = url, 3978 displayUrl = url; 3979 if (url.length > MAX_DISPLAY_LENGTH) { 3980 displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "..."; 3981 } 3982 // Add http prefix if necessary 3983 if (realUrl.substr(0, 4) === "www.") { 3984 realUrl = "http://" + realUrl; 3985 } 3986 3987 return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation; 3988 }); 3989 } 3990 3991 /** 3992 * Creates or (if already cached) returns a temp element 3993 * for the given document object 3994 */ 3995 function _getTempElement(context) { 3996 var tempElement = context._wysihtml5_tempElement; 3997 if (!tempElement) { 3998 tempElement = context._wysihtml5_tempElement = context.createElement("div"); 3999 } 4000 return tempElement; 4001 } 4002 4003 /** 4004 * Replaces the original text nodes with the newly auto-linked dom tree 4005 */ 4006 function _wrapMatchesInNode(textNode) { 4007 var parentNode = textNode.parentNode, 4008 tempElement = _getTempElement(parentNode.ownerDocument); 4009 4010 // We need to insert an empty/temporary <span /> to fix IE quirks 4011 // Elsewise IE would strip white space in the beginning 4012 tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(textNode.data); 4013 tempElement.removeChild(tempElement.firstChild); 4014 4015 while (tempElement.firstChild) { 4016 // inserts tempElement.firstChild before textNode 4017 parentNode.insertBefore(tempElement.firstChild, textNode); 4018 } 4019 parentNode.removeChild(textNode); 4020 } 4021 4022 function _hasParentThatShouldBeIgnored(node) { 4023 var nodeName; 4024 while (node.parentNode) { 4025 node = node.parentNode; 4026 nodeName = node.nodeName; 4027 if (IGNORE_URLS_IN.contains(nodeName)) { 4028 return true; 4029 } else if (nodeName === "body") { 4030 return false; 4031 } 4032 } 4033 return false; 4034 } 4035 4036 function _parseNode(element) { 4037 if (IGNORE_URLS_IN.contains(element.nodeName)) { 4038 return; 4039 } 4040 4041 if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) { 4042 _wrapMatchesInNode(element); 4043 return; 4044 } 4045 4046 var childNodes = wysihtml5.lang.array(element.childNodes).get(), 4047 childNodesLength = childNodes.length, 4048 i = 0; 4049 4050 for (; i<childNodesLength; i++) { 4051 _parseNode(childNodes[i]); 4052 } 4053 4054 return element; 4055 } 4056 4057 wysihtml5.dom.autoLink = autoLink; 4058 4059 // Reveal url reg exp to the outside 4060 wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP; 4061 })(wysihtml5);(function(wysihtml5) { 4062 var supportsClassList = wysihtml5.browser.supportsClassList(), 4063 api = wysihtml5.dom; 4064 4065 api.addClass = function(element, className) { 4066 if (supportsClassList) { 4067 return element.classList.add(className); 4068 } 4069 if (api.hasClass(element, className)) { 4070 return; 4071 } 4072 element.className += " " + className; 4073 }; 4074 4075 api.removeClass = function(element, className) { 4076 if (supportsClassList) { 4077 return element.classList.remove(className); 4078 } 4079 4080 element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " "); 4081 }; 4082 4083 api.hasClass = function(element, className) { 4084 if (supportsClassList) { 4085 return element.classList.contains(className); 4086 } 4087 4088 var elementClassName = element.className; 4089 return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); 4090 }; 4091 })(wysihtml5); 4092 wysihtml5.dom.contains = (function() { 4093 var documentElement = document.documentElement; 4094 if (documentElement.contains) { 4095 return function(container, element) { 4096 if (element.nodeType !== wysihtml5.ELEMENT_NODE) { 4097 element = element.parentNode; 4098 } 4099 return container !== element && container.contains(element); 4100 }; 4101 } else if (documentElement.compareDocumentPosition) { 4102 return function(container, element) { 4103 // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition 4104 return !!(container.compareDocumentPosition(element) & 16); 4105 }; 4106 } 4107 })();/** 4108 * Converts an HTML fragment/element into a unordered/ordered list 4109 * 4110 * @param {Element} element The element which should be turned into a list 4111 * @param {String} listType The list type in which to convert the tree (either "ul" or "ol") 4112 * @return {Element} The created list 4113 * 4114 * @example 4115 * <!-- Assume the following dom: --> 4116 * <span id="pseudo-list"> 4117 * eminem<br> 4118 * dr. dre 4119 * <div>50 Cent</div> 4120 * </span> 4121 * 4122 * <script> 4123 * wysihtml5.dom.convertToList(document.getElementById("pseudo-list"), "ul"); 4124 * </script> 4125 * 4126 * <!-- Will result in: --> 4127 * <ul> 4128 * <li>eminem</li> 4129 * <li>dr. dre</li> 4130 * <li>50 Cent</li> 4131 * </ul> 4132 */ 4133 wysihtml5.dom.convertToList = (function() { 4134 function _createListItem(doc, list) { 4135 var listItem = doc.createElement("li"); 4136 list.appendChild(listItem); 4137 return listItem; 4138 } 4139 4140 function _createList(doc, type) { 4141 return doc.createElement(type); 4142 } 4143 4144 function convertToList(element, listType) { 4145 if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") { 4146 // Already a list 4147 return element; 4148 } 4149 4150 var doc = element.ownerDocument, 4151 list = _createList(doc, listType), 4152 lineBreaks = element.querySelectorAll("br"), 4153 lineBreaksLength = lineBreaks.length, 4154 childNodes, 4155 childNodesLength, 4156 childNode, 4157 lineBreak, 4158 parentNode, 4159 isBlockElement, 4160 isLineBreak, 4161 currentListItem, 4162 i; 4163 4164 // First find <br> at the end of inline elements and move them behind them 4165 for (i=0; i<lineBreaksLength; i++) { 4166 lineBreak = lineBreaks[i]; 4167 while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) { 4168 if (wysihtml5.dom.getStyle("display").from(parentNode) === "block") { 4169 parentNode.removeChild(lineBreak); 4170 break; 4171 } 4172 wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode); 4173 } 4174 } 4175 4176 childNodes = wysihtml5.lang.array(element.childNodes).get(); 4177 childNodesLength = childNodes.length; 4178 4179 for (i=0; i<childNodesLength; i++) { 4180 currentListItem = currentListItem || _createListItem(doc, list); 4181 childNode = childNodes[i]; 4182 isBlockElement = wysihtml5.dom.getStyle("display").from(childNode) === "block"; 4183 isLineBreak = childNode.nodeName === "BR"; 4184 4185 if (isBlockElement) { 4186 // Append blockElement to current <li> if empty, otherwise create a new one 4187 currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem; 4188 currentListItem.appendChild(childNode); 4189 currentListItem = null; 4190 continue; 4191 } 4192 4193 if (isLineBreak) { 4194 // Only create a new list item in the next iteration when the current one has already content 4195 currentListItem = currentListItem.firstChild ? null : currentListItem; 4196 continue; 4197 } 4198 4199 currentListItem.appendChild(childNode); 4200 } 4201 4202 element.parentNode.replaceChild(list, element); 4203 return list; 4204 } 4205 4206 return convertToList; 4207 })();/** 4208 * Copy a set of attributes from one element to another 4209 * 4210 * @param {Array} attributesToCopy List of attributes which should be copied 4211 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to 4212 * copy the attributes from., this again returns an object which provides a method named "to" which can be invoked 4213 * with the element where to copy the attributes to (see example) 4214 * 4215 * @example 4216 * var textarea = document.querySelector("textarea"), 4217 * div = document.querySelector("div[contenteditable=true]"), 4218 * anotherDiv = document.querySelector("div.preview"); 4219 * wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv); 4220 * 4221 */ 4222 wysihtml5.dom.copyAttributes = function(attributesToCopy) { 4223 return { 4224 from: function(elementToCopyFrom) { 4225 return { 4226 to: function(elementToCopyTo) { 4227 var attribute, 4228 i = 0, 4229 length = attributesToCopy.length; 4230 for (; i<length; i++) { 4231 attribute = attributesToCopy[i]; 4232 if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") { 4233 elementToCopyTo[attribute] = elementToCopyFrom[attribute]; 4234 } 4235 } 4236 return { andTo: arguments.callee }; 4237 } 4238 }; 4239 } 4240 }; 4241 };/** 4242 * Copy a set of styles from one element to another 4243 * Please note that this only works properly across browsers when the element from which to copy the styles 4244 * is in the dom 4245 * 4246 * Interesting article on how to copy styles 4247 * 4248 * @param {Array} stylesToCopy List of styles which should be copied 4249 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to 4250 * copy the styles from., this again returns an object which provides a method named "to" which can be invoked 4251 * with the element where to copy the styles to (see example) 4252 * 4253 * @example 4254 * var textarea = document.querySelector("textarea"), 4255 * div = document.querySelector("div[contenteditable=true]"), 4256 * anotherDiv = document.querySelector("div.preview"); 4257 * wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv); 4258 * 4259 */ 4260 (function(dom) { 4261 4262 /** 4263 * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set 4264 * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then 4265 * its computed css width will be 198px 4266 */ 4267 var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"]; 4268 4269 var shouldIgnoreBoxSizingBorderBox = function(element) { 4270 if (hasBoxSizingBorderBox(element)) { 4271 return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth; 4272 } 4273 return false; 4274 }; 4275 4276 var hasBoxSizingBorderBox = function(element) { 4277 var i = 0, 4278 length = BOX_SIZING_PROPERTIES.length; 4279 for (; i<length; i++) { 4280 if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") { 4281 return BOX_SIZING_PROPERTIES[i]; 4282 } 4283 } 4284 }; 4285 4286 dom.copyStyles = function(stylesToCopy) { 4287 return { 4288 from: function(element) { 4289 if (shouldIgnoreBoxSizingBorderBox(element)) { 4290 stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES); 4291 } 4292 4293 var cssText = "", 4294 length = stylesToCopy.length, 4295 i = 0, 4296 property; 4297 for (; i<length; i++) { 4298 property = stylesToCopy[i]; 4299 cssText += property + ":" + dom.getStyle(property).from(element) + ";"; 4300 } 4301 4302 return { 4303 to: function(element) { 4304 dom.setStyles(cssText).on(element); 4305 return { andTo: arguments.callee }; 4306 } 4307 }; 4308 } 4309 }; 4310 }; 4311 })(wysihtml5.dom);/** 4312 * Event Delegation 4313 * 4314 * @example 4315 * wysihtml5.dom.delegate(document.body, "a", "click", function() { 4316 * // foo 4317 * }); 4318 */ 4319 (function(wysihtml5) { 4320 4321 wysihtml5.dom.delegate = function(container, selector, eventName, handler) { 4322 return wysihtml5.dom.observe(container, eventName, function(event) { 4323 var target = event.target, 4324 match = wysihtml5.lang.array(container.querySelectorAll(selector)); 4325 4326 while (target && target !== container) { 4327 if (match.contains(target)) { 4328 handler.call(target, event); 4329 break; 4330 } 4331 target = target.parentNode; 4332 } 4333 }); 4334 }; 4335 4336 })(wysihtml5);/** 4337 * Returns the given html wrapped in a div element 4338 * 4339 * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly 4340 * when inserted via innerHTML 4341 * 4342 * @param {String} html The html which should be wrapped in a dom element 4343 * @param {Obejct} [context] Document object of the context the html belongs to 4344 * 4345 * @example 4346 * wysihtml5.dom.getAsDom("<article>foo</article>"); 4347 */ 4348 wysihtml5.dom.getAsDom = (function() { 4349 4350 var _innerHTMLShiv = function(html, context) { 4351 var tempElement = context.createElement("div"); 4352 tempElement.style.display = "none"; 4353 context.body.appendChild(tempElement); 4354 // IE throws an exception when trying to insert <frameset></frameset> via innerHTML 4355 try { tempElement.innerHTML = html; } catch(e) {} 4356 context.body.removeChild(tempElement); 4357 return tempElement; 4358 }; 4359 4360 /** 4361 * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element 4362 */ 4363 var _ensureHTML5Compatibility = function(context) { 4364 if (context._wysihtml5_supportsHTML5Tags) { 4365 return; 4366 } 4367 for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) { 4368 context.createElement(HTML5_ELEMENTS[i]); 4369 } 4370 context._wysihtml5_supportsHTML5Tags = true; 4371 }; 4372 4373 4374 /** 4375 * List of html5 tags 4376 * taken from http://simon.html5.org/html5-elements 4377 */ 4378 var HTML5_ELEMENTS = [ 4379 "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption", 4380 "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress", 4381 "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr" 4382 ]; 4383 4384 return function(html, context) { 4385 context = context || document; 4386 var tempElement; 4387 if (typeof(html) === "object" && html.nodeType) { 4388 tempElement = context.createElement("div"); 4389 tempElement.appendChild(html); 4390 } else if (wysihtml5.browser.supportsHTML5Tags(context)) { 4391 tempElement = context.createElement("div"); 4392 tempElement.innerHTML = html; 4393 } else { 4394 _ensureHTML5Compatibility(context); 4395 tempElement = _innerHTMLShiv(html, context); 4396 } 4397 return tempElement; 4398 }; 4399 })();/** 4400 * Walks the dom tree from the given node up until it finds a match 4401 * Designed for optimal performance. 4402 * 4403 * @param {Element} node The from which to check the parent nodes 4404 * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp) 4405 * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50) 4406 * @return {null|Element} Returns the first element that matched the desiredNodeName(s) 4407 * @example 4408 * var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] }); 4409 * // ... or ... 4410 * var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" }); 4411 * // ... or ... 4412 * var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g }); 4413 */ 4414 wysihtml5.dom.getParentElement = (function() { 4415 4416 function _isSameNodeName(nodeName, desiredNodeNames) { 4417 if (!desiredNodeNames || !desiredNodeNames.length) { 4418 return true; 4419 } 4420 4421 if (typeof(desiredNodeNames) === "string") { 4422 return nodeName === desiredNodeNames; 4423 } else { 4424 return wysihtml5.lang.array(desiredNodeNames).contains(nodeName); 4425 } 4426 } 4427 4428 function _isElement(node) { 4429 return node.nodeType === wysihtml5.ELEMENT_NODE; 4430 } 4431 4432 function _hasClassName(element, className, classRegExp) { 4433 var classNames = (element.className || "").match(classRegExp) || []; 4434 if (!className) { 4435 return !!classNames.length; 4436 } 4437 return classNames[classNames.length - 1] === className; 4438 } 4439 4440 function _getParentElementWithNodeName(node, nodeName, levels) { 4441 while (levels-- && node && node.nodeName !== "BODY") { 4442 if (_isSameNodeName(node.nodeName, nodeName)) { 4443 return node; 4444 } 4445 node = node.parentNode; 4446 } 4447 return null; 4448 } 4449 4450 function _getParentElementWithNodeNameAndClassName(node, nodeName, className, classRegExp, levels) { 4451 while (levels-- && node && node.nodeName !== "BODY") { 4452 if (_isElement(node) && 4453 _isSameNodeName(node.nodeName, nodeName) && 4454 _hasClassName(node, className, classRegExp)) { 4455 return node; 4456 } 4457 node = node.parentNode; 4458 } 4459 return null; 4460 } 4461 4462 return function(node, matchingSet, levels) { 4463 levels = levels || 50; // Go max 50 nodes upwards from current node 4464 if (matchingSet.className || matchingSet.classRegExp) { 4465 return _getParentElementWithNodeNameAndClassName( 4466 node, matchingSet.nodeName, matchingSet.className, matchingSet.classRegExp, levels 4467 ); 4468 } else { 4469 return _getParentElementWithNodeName( 4470 node, matchingSet.nodeName, levels 4471 ); 4472 } 4473 }; 4474 })(); 4475 /** 4476 * Get element's style for a specific css property 4477 * 4478 * @param {Element} element The element on which to retrieve the style 4479 * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...) 4480 * 4481 * @example 4482 * wysihtml5.dom.getStyle("display").from(document.body); 4483 * // => "block" 4484 */ 4485 wysihtml5.dom.getStyle = (function() { 4486 var stylePropertyMapping = { 4487 "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat" 4488 }, 4489 REG_EXP_CAMELIZE = /\-[a-z]/g; 4490 4491 function camelize(str) { 4492 return str.replace(REG_EXP_CAMELIZE, function(match) { 4493 return match.charAt(1).toUpperCase(); 4494 }); 4495 } 4496 4497 return function(property) { 4498 return { 4499 from: function(element) { 4500 if (element.nodeType !== wysihtml5.ELEMENT_NODE) { 4501 return; 4502 } 4503 4504 var doc = element.ownerDocument, 4505 camelizedProperty = stylePropertyMapping[property] || camelize(property), 4506 style = element.style, 4507 currentStyle = element.currentStyle, 4508 styleValue = style[camelizedProperty]; 4509 if (styleValue) { 4510 return styleValue; 4511 } 4512 4513 // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant 4514 // window.getComputedStyle, since it returns css property values in their original unit: 4515 // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle 4516 // gives you the original "50%". 4517 // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio 4518 if (currentStyle) { 4519 try { 4520 return currentStyle[camelizedProperty]; 4521 } catch(e) { 4522 //ie will occasionally fail for unknown reasons. swallowing exception 4523 } 4524 } 4525 4526 var win = doc.defaultView || doc.parentWindow, 4527 needsOverflowReset = (property === "height" || property === "width") && element.nodeName === "TEXTAREA", 4528 originalOverflow, 4529 returnValue; 4530 4531 if (win.getComputedStyle) { 4532 // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars 4533 // therfore we remove and restore the scrollbar and calculate the value in between 4534 if (needsOverflowReset) { 4535 originalOverflow = style.overflow; 4536 style.overflow = "hidden"; 4537 } 4538 returnValue = win.getComputedStyle(element, null).getPropertyValue(property); 4539 if (needsOverflowReset) { 4540 style.overflow = originalOverflow || ""; 4541 } 4542 return returnValue; 4543 } 4544 } 4545 }; 4546 }; 4547 })();/** 4548 * High performant way to check whether an element with a specific tag name is in the given document 4549 * Optimized for being heavily executed 4550 * Unleashes the power of live node lists 4551 * 4552 * @param {Object} doc The document object of the context where to check 4553 * @param {String} tagName Upper cased tag name 4554 * @example 4555 * wysihtml5.dom.hasElementWithTagName(document, "IMG"); 4556 */ 4557 wysihtml5.dom.hasElementWithTagName = (function() { 4558 var LIVE_CACHE = {}, 4559 DOCUMENT_IDENTIFIER = 1; 4560 4561 function _getDocumentIdentifier(doc) { 4562 return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); 4563 } 4564 4565 return function(doc, tagName) { 4566 var key = _getDocumentIdentifier(doc) + ":" + tagName, 4567 cacheEntry = LIVE_CACHE[key]; 4568 if (!cacheEntry) { 4569 cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName); 4570 } 4571 4572 return cacheEntry.length > 0; 4573 }; 4574 })();/** 4575 * High performant way to check whether an element with a specific class name is in the given document 4576 * Optimized for being heavily executed 4577 * Unleashes the power of live node lists 4578 * 4579 * @param {Object} doc The document object of the context where to check 4580 * @param {String} tagName Upper cased tag name 4581 * @example 4582 * wysihtml5.dom.hasElementWithClassName(document, "foobar"); 4583 */ 4584 (function(wysihtml5) { 4585 var LIVE_CACHE = {}, 4586 DOCUMENT_IDENTIFIER = 1; 4587 4588 function _getDocumentIdentifier(doc) { 4589 return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); 4590 } 4591 4592 wysihtml5.dom.hasElementWithClassName = function(doc, className) { 4593 // getElementsByClassName is not supported by IE<9 4594 // but is sometimes mocked via library code (which then doesn't return live node lists) 4595 if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) { 4596 return !!doc.querySelector("." + className); 4597 } 4598 4599 var key = _getDocumentIdentifier(doc) + ":" + className, 4600 cacheEntry = LIVE_CACHE[key]; 4601 if (!cacheEntry) { 4602 cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className); 4603 } 4604 4605 return cacheEntry.length > 0; 4606 }; 4607 })(wysihtml5); 4608 wysihtml5.dom.insert = function(elementToInsert) { 4609 return { 4610 after: function(element) { 4611 element.parentNode.insertBefore(elementToInsert, element.nextSibling); 4612 }, 4613 4614 before: function(element) { 4615 element.parentNode.insertBefore(elementToInsert, element); 4616 }, 4617 4618 into: function(element) { 4619 element.appendChild(elementToInsert); 4620 } 4621 }; 4622 };wysihtml5.dom.insertCSS = function(rules) { 4623 rules = rules.join("\n"); 4624 4625 return { 4626 into: function(doc) { 4627 var head = doc.head || doc.getElementsByTagName("head")[0], 4628 styleElement = doc.createElement("style"); 4629 4630 styleElement.type = "text/css"; 4631 4632 if (styleElement.styleSheet) { 4633 styleElement.styleSheet.cssText = rules; 4634 } else { 4635 styleElement.appendChild(doc.createTextNode(rules)); 4636 } 4637 4638 if (head) { 4639 head.appendChild(styleElement); 4640 } 4641 } 4642 }; 4643 };/** 4644 * Method to set dom events 4645 * 4646 * @example 4647 * wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... }); 4648 */ 4649 wysihtml5.dom.observe = function(element, eventNames, handler) { 4650 eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames; 4651 4652 var handlerWrapper, 4653 eventName, 4654 i = 0, 4655 length = eventNames.length; 4656 4657 for (; i<length; i++) { 4658 eventName = eventNames[i]; 4659 if (element.addEventListener) { 4660 element.addEventListener(eventName, handler, false); 4661 } else { 4662 handlerWrapper = function(event) { 4663 if (!("target" in event)) { 4664 event.target = event.srcElement; 4665 } 4666 event.preventDefault = event.preventDefault || function() { 4667 this.returnValue = false; 4668 }; 4669 event.stopPropagation = event.stopPropagation || function() { 4670 this.cancelBubble = true; 4671 }; 4672 handler.call(element, event); 4673 }; 4674 element.attachEvent("on" + eventName, handlerWrapper); 4675 } 4676 } 4677 4678 return { 4679 stop: function() { 4680 var eventName, 4681 i = 0, 4682 length = eventNames.length; 4683 for (; i<length; i++) { 4684 eventName = eventNames[i]; 4685 if (element.removeEventListener) { 4686 element.removeEventListener(eventName, handler, false); 4687 } else { 4688 element.detachEvent("on" + eventName, handlerWrapper); 4689 } 4690 } 4691 } 4692 }; 4693 }; 4694 /** 4695 * HTML Sanitizer 4696 * Rewrites the HTML based on given rules 4697 * 4698 * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized 4699 * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will 4700 * be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the 4701 * desired substitution. 4702 * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing 4703 * 4704 * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element. 4705 * 4706 * @example 4707 * var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>'; 4708 * wysihtml5.dom.parse(userHTML, { 4709 * tags { 4710 * p: "div", // Rename p tags to div tags 4711 * font: "span" // Rename font tags to span tags 4712 * div: true, // Keep them, also possible (same result when passing: "div" or true) 4713 * script: undefined // Remove script elements 4714 * } 4715 * }); 4716 * // => <div><div><span>foo bar</span></div></div> 4717 * 4718 * var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>'; 4719 * wysihtml5.dom.parse(userHTML); 4720 * // => '<span><span><span><span>I'm a table!</span></span></span></span>' 4721 * 4722 * var userHTML = '<div>foobar<br>foobar</div>'; 4723 * wysihtml5.dom.parse(userHTML, { 4724 * tags: { 4725 * div: undefined, 4726 * br: true 4727 * } 4728 * }); 4729 * // => '' 4730 * 4731 * var userHTML = '<div class="red">foo</div><div class="pink">bar</div>'; 4732 * wysihtml5.dom.parse(userHTML, { 4733 * classes: { 4734 * red: 1, 4735 * green: 1 4736 * }, 4737 * tags: { 4738 * div: { 4739 * rename_tag: "p" 4740 * } 4741 * } 4742 * }); 4743 * // => '<p class="red">foo</p><p>bar</p>' 4744 */ 4745 wysihtml5.dom.parse = (function() { 4746 4747 /** 4748 * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML 4749 * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the 4750 * node isn't closed 4751 * 4752 * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML. 4753 */ 4754 var NODE_TYPE_MAPPING = { 4755 "1": _handleElement, 4756 "3": _handleText 4757 }, 4758 // Rename unknown tags to this 4759 DEFAULT_NODE_NAME = "span", 4760 WHITE_SPACE_REG_EXP = /\s+/, 4761 defaultRules = { tags: {}, classes: {} }, 4762 currentRules = {}; 4763 4764 /** 4765 * Iterates over all childs of the element, recreates them, appends them into a document fragment 4766 * which later replaces the entire body content 4767 */ 4768 function parse(elementOrHtml, rules, context, cleanUp) { 4769 wysihtml5.lang.object(currentRules).merge(defaultRules).merge(rules).get(); 4770 4771 context = context || elementOrHtml.ownerDocument || document; 4772 var fragment = context.createDocumentFragment(), 4773 isString = typeof(elementOrHtml) === "string", 4774 element, 4775 newNode, 4776 firstChild; 4777 4778 if (isString) { 4779 element = wysihtml5.dom.getAsDom(elementOrHtml, context); 4780 } else { 4781 element = elementOrHtml; 4782 } 4783 4784 while (element.firstChild) { 4785 firstChild = element.firstChild; 4786 element.removeChild(firstChild); 4787 newNode = _convert(firstChild, cleanUp); 4788 if (newNode) { 4789 fragment.appendChild(newNode); 4790 } 4791 } 4792 4793 // Clear element contents 4794 element.innerHTML = ""; 4795 4796 // Insert new DOM tree 4797 element.appendChild(fragment); 4798 4799 return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element; 4800 } 4801 4802 function _convert(oldNode, cleanUp) { 4803 var oldNodeType = oldNode.nodeType, 4804 oldChilds = oldNode.childNodes, 4805 oldChildsLength = oldChilds.length, 4806 newNode, 4807 method = NODE_TYPE_MAPPING[oldNodeType], 4808 i = 0; 4809 4810 newNode = method && method(oldNode); 4811 4812 if (!newNode) { 4813 return null; 4814 } 4815 4816 for (i=0; i<oldChildsLength; i++) { 4817 newChild = _convert(oldChilds[i], cleanUp); 4818 if (newChild) { 4819 newNode.appendChild(newChild); 4820 } 4821 } 4822 4823 // Cleanup senseless <span> elements 4824 if (cleanUp && 4825 newNode.childNodes.length <= 1 && 4826 newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME && 4827 !newNode.attributes.length) { 4828 return newNode.firstChild; 4829 } 4830 4831 return newNode; 4832 } 4833 4834 function _handleElement(oldNode) { 4835 var rule, 4836 newNode, 4837 endTag, 4838 tagRules = currentRules.tags, 4839 nodeName = oldNode.nodeName.toLowerCase(), 4840 scopeName = oldNode.scopeName; 4841 4842 /** 4843 * We already parsed that element 4844 * ignore it! (yes, this sometimes happens in IE8 when the html is invalid) 4845 */ 4846 if (oldNode._wysihtml5) { 4847 return null; 4848 } 4849 oldNode._wysihtml5 = 1; 4850 4851 if (oldNode.className === "wysihtml5-temp") { 4852 return null; 4853 } 4854 4855 /** 4856 * IE is the only browser who doesn't include the namespace in the 4857 * nodeName, that's why we have to prepend it by ourselves 4858 * scopeName is a proprietary IE feature 4859 * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx 4860 */ 4861 if (scopeName && scopeName != "HTML") { 4862 nodeName = scopeName + ":" + nodeName; 4863 } 4864 4865 /** 4866 * Repair node 4867 * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags 4868 * 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 4869 */ 4870 if ("outerHTML" in oldNode) { 4871 if (!wysihtml5.browser.autoClosesUnclosedTags() && 4872 oldNode.nodeName === "P" && 4873 oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") { 4874 nodeName = "div"; 4875 } 4876 } 4877 4878 if (nodeName in tagRules) { 4879 rule = tagRules[nodeName]; 4880 if (!rule || rule.remove) { 4881 return null; 4882 } 4883 4884 rule = typeof(rule) === "string" ? { rename_tag: rule } : rule; 4885 } else if (oldNode.firstChild) { 4886 rule = { rename_tag: DEFAULT_NODE_NAME }; 4887 } else { 4888 // Remove empty unknown elements 4889 return null; 4890 } 4891 4892 newNode = oldNode.ownerDocument.createElement(rule.rename_tag || nodeName); 4893 _handleAttributes(oldNode, newNode, rule); 4894 4895 oldNode = null; 4896 return newNode; 4897 } 4898 4899 function _handleAttributes(oldNode, newNode, rule) { 4900 var attributes = {}, // fresh new set of attributes to set on newNode 4901 setClass = rule.set_class, // classes to set 4902 addClass = rule.add_class, // add classes based on existing attributes 4903 setAttributes = rule.set_attributes, // attributes to set on the current node 4904 checkAttributes = rule.check_attributes, // check/convert values of attributes 4905 allowedClasses = currentRules.classes, 4906 i = 0, 4907 classes = [], 4908 newClasses = [], 4909 newUniqueClasses = [], 4910 oldClasses = [], 4911 classesLength, 4912 newClassesLength, 4913 currentClass, 4914 newClass, 4915 attributeName, 4916 newAttributeValue, 4917 method; 4918 4919 if (setAttributes) { 4920 attributes = wysihtml5.lang.object(setAttributes).clone(); 4921 } 4922 4923 if (checkAttributes) { 4924 for (attributeName in checkAttributes) { 4925 method = attributeCheckMethods[checkAttributes[attributeName]]; 4926 if (!method) { 4927 continue; 4928 } 4929 newAttributeValue = method(_getAttribute(oldNode, attributeName)); 4930 if (typeof(newAttributeValue) === "string") { 4931 attributes[attributeName] = newAttributeValue; 4932 } 4933 } 4934 } 4935 4936 if (setClass) { 4937 classes.push(setClass); 4938 } 4939 4940 if (addClass) { 4941 for (attributeName in addClass) { 4942 method = addClassMethods[addClass[attributeName]]; 4943 if (!method) { 4944 continue; 4945 } 4946 newClass = method(_getAttribute(oldNode, attributeName)); 4947 if (typeof(newClass) === "string") { 4948 classes.push(newClass); 4949 } 4950 } 4951 } 4952 4953 // make sure that wysihtml5 temp class doesn't get stripped out 4954 allowedClasses["_wysihtml5-temp-placeholder"] = 1; 4955 4956 // add old classes last 4957 oldClasses = oldNode.getAttribute("class"); 4958 if (oldClasses) { 4959 classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP)); 4960 } 4961 classesLength = classes.length; 4962 for (; i<classesLength; i++) { 4963 currentClass = classes[i]; 4964 if (allowedClasses[currentClass]) { 4965 newClasses.push(currentClass); 4966 } 4967 } 4968 4969 // remove duplicate entries and preserve class specificity 4970 newClassesLength = newClasses.length; 4971 while (newClassesLength--) { 4972 currentClass = newClasses[newClassesLength]; 4973 if (!wysihtml5.lang.array(newUniqueClasses).contains(currentClass)) { 4974 newUniqueClasses.unshift(currentClass); 4975 } 4976 } 4977 4978 if (newUniqueClasses.length) { 4979 attributes["class"] = newUniqueClasses.join(" "); 4980 } 4981 4982 // set attributes on newNode 4983 for (attributeName in attributes) { 4984 // Setting attributes can cause a js error in IE under certain circumstances 4985 // eg. on a <img> under https when it's new attribute value is non-https 4986 // TODO: Investigate this further and check for smarter handling 4987 try { 4988 newNode.setAttribute(attributeName, attributes[attributeName]); 4989 } catch(e) {} 4990 } 4991 4992 // IE8 sometimes loses the width/height attributes when those are set before the "src" 4993 // so we make sure to set them again 4994 if (attributes.src) { 4995 if (typeof(attributes.width) !== "undefined") { 4996 newNode.setAttribute("width", attributes.width); 4997 } 4998 if (typeof(attributes.height) !== "undefined") { 4999 newNode.setAttribute("height", attributes.height); 5000 } 5001 } 5002 } 5003 5004 /** 5005 * IE gives wrong results for hasAttribute/getAttribute, for example: 5006 * var td = document.createElement("td"); 5007 * td.getAttribute("rowspan"); // => "1" in IE 5008 * 5009 * Therefore we have to check the element's outerHTML for the attribute 5010 */ 5011 var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly(); 5012 function _getAttribute(node, attributeName) { 5013 attributeName = attributeName.toLowerCase(); 5014 var nodeName = node.nodeName; 5015 if (nodeName == "IMG" && attributeName == "src" && _isLoadedImage(node) === true) { 5016 // Get 'src' attribute value via object property since this will always contain the 5017 // full absolute url (http://...) 5018 // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host 5019 // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url) 5020 return node.src; 5021 } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) { 5022 // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML 5023 var outerHTML = node.outerHTML.toLowerCase(), 5024 // TODO: This might not work for attributes without value: <input disabled> 5025 hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1; 5026 5027 return hasAttribute ? node.getAttribute(attributeName) : null; 5028 } else{ 5029 return node.getAttribute(attributeName); 5030 } 5031 } 5032 5033 /** 5034 * Check whether the given node is a proper loaded image 5035 * FIXME: Returns undefined when unknown (Chrome, Safari) 5036 */ 5037 function _isLoadedImage(node) { 5038 try { 5039 return node.complete && !node.mozMatchesSelector(":-moz-broken"); 5040 } catch(e) { 5041 if (node.complete && node.readyState === "complete") { 5042 return true; 5043 } 5044 } 5045 } 5046 5047 function _handleText(oldNode) { 5048 return oldNode.ownerDocument.createTextNode(oldNode.data); 5049 } 5050 5051 5052 // ------------ attribute checks ------------ \\ 5053 var attributeCheckMethods = { 5054 url: (function() { 5055 var REG_EXP = /^https?:\/\//i; 5056 return function(attributeValue) { 5057 if (!attributeValue || !attributeValue.match(REG_EXP)) { 5058 return null; 5059 } 5060 return attributeValue.replace(REG_EXP, function(match) { 5061 return match.toLowerCase(); 5062 }); 5063 }; 5064 })(), 5065 5066 alt: (function() { 5067 var REG_EXP = /[^ a-z0-9_\-]/gi; 5068 return function(attributeValue) { 5069 if (!attributeValue) { 5070 return ""; 5071 } 5072 return attributeValue.replace(REG_EXP, ""); 5073 }; 5074 })(), 5075 5076 numbers: (function() { 5077 var REG_EXP = /\D/g; 5078 return function(attributeValue) { 5079 attributeValue = (attributeValue || "").replace(REG_EXP, ""); 5080 return attributeValue || null; 5081 }; 5082 })() 5083 }; 5084 5085 // ------------ class converter (converts an html attribute to a class name) ------------ \\ 5086 var addClassMethods = { 5087 align_img: (function() { 5088 var mapping = { 5089 left: "wysiwyg-float-left", 5090 right: "wysiwyg-float-right" 5091 }; 5092 return function(attributeValue) { 5093 return mapping[String(attributeValue).toLowerCase()]; 5094 }; 5095 })(), 5096 5097 align_text: (function() { 5098 var mapping = { 5099 left: "wysiwyg-text-align-left", 5100 right: "wysiwyg-text-align-right", 5101 center: "wysiwyg-text-align-center", 5102 justify: "wysiwyg-text-align-justify" 5103 }; 5104 return function(attributeValue) { 5105 return mapping[String(attributeValue).toLowerCase()]; 5106 }; 5107 })(), 5108 5109 clear_br: (function() { 5110 var mapping = { 5111 left: "wysiwyg-clear-left", 5112 right: "wysiwyg-clear-right", 5113 both: "wysiwyg-clear-both", 5114 all: "wysiwyg-clear-both" 5115 }; 5116 return function(attributeValue) { 5117 return mapping[String(attributeValue).toLowerCase()]; 5118 }; 5119 })(), 5120 5121 size_font: (function() { 5122 var mapping = { 5123 "1": "wysiwyg-font-size-xx-small", 5124 "2": "wysiwyg-font-size-small", 5125 "3": "wysiwyg-font-size-medium", 5126 "4": "wysiwyg-font-size-large", 5127 "5": "wysiwyg-font-size-x-large", 5128 "6": "wysiwyg-font-size-xx-large", 5129 "7": "wysiwyg-font-size-xx-large", 5130 "-": "wysiwyg-font-size-smaller", 5131 "+": "wysiwyg-font-size-larger" 5132 }; 5133 return function(attributeValue) { 5134 return mapping[String(attributeValue).charAt(0)]; 5135 }; 5136 })() 5137 }; 5138 5139 return parse; 5140 })();/** 5141 * Checks for empty text node childs and removes them 5142 * 5143 * @param {Element} node The element in which to cleanup 5144 * @example 5145 * wysihtml5.dom.removeEmptyTextNodes(element); 5146 */ 5147 wysihtml5.dom.removeEmptyTextNodes = function(node) { 5148 var childNode, 5149 childNodes = wysihtml5.lang.array(node.childNodes).get(), 5150 childNodesLength = childNodes.length, 5151 i = 0; 5152 for (; i<childNodesLength; i++) { 5153 childNode = childNodes[i]; 5154 if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") { 5155 childNode.parentNode.removeChild(childNode); 5156 } 5157 } 5158 }; 5159 /** 5160 * Renames an element (eg. a <div> to a <p>) and keeps its childs 5161 * 5162 * @param {Element} element The list element which should be renamed 5163 * @param {Element} newNodeName The desired tag name 5164 * 5165 * @example 5166 * <!-- Assume the following dom: --> 5167 * <ul id="list"> 5168 * <li>eminem</li> 5169 * <li>dr. dre</li> 5170 * <li>50 Cent</li> 5171 * </ul> 5172 * 5173 * <script> 5174 * wysihtml5.dom.renameElement(document.getElementById("list"), "ol"); 5175 * </script> 5176 * 5177 * <!-- Will result in: --> 5178 * <ol> 5179 * <li>eminem</li> 5180 * <li>dr. dre</li> 5181 * <li>50 Cent</li> 5182 * </ol> 5183 */ 5184 wysihtml5.dom.renameElement = function(element, newNodeName) { 5185 var newElement = element.ownerDocument.createElement(newNodeName), 5186 firstChild; 5187 while (firstChild = element.firstChild) { 5188 newElement.appendChild(firstChild); 5189 } 5190 wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement); 5191 element.parentNode.replaceChild(newElement, element); 5192 return newElement; 5193 };/** 5194 * Takes an element, removes it and replaces it with it's childs 5195 * 5196 * @param {Object} node The node which to replace with it's child nodes 5197 * @example 5198 * <div id="foo"> 5199 * <span>hello</span> 5200 * </div> 5201 * <script> 5202 * // Remove #foo and replace with it's children 5203 * wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo")); 5204 * </script> 5205 */ 5206 wysihtml5.dom.replaceWithChildNodes = function(node) { 5207 if (!node.parentNode) { 5208 return; 5209 } 5210 5211 if (!node.firstChild) { 5212 node.parentNode.removeChild(node); 5213 return; 5214 } 5215 5216 var fragment = node.ownerDocument.createDocumentFragment(); 5217 while (node.firstChild) { 5218 fragment.appendChild(node.firstChild); 5219 } 5220 node.parentNode.replaceChild(fragment, node); 5221 node = fragment = null; 5222 }; 5223 /** 5224 * Unwraps an unordered/ordered list 5225 * 5226 * @param {Element} element The list element which should be unwrapped 5227 * 5228 * @example 5229 * <!-- Assume the following dom: --> 5230 * <ul id="list"> 5231 * <li>eminem</li> 5232 * <li>dr. dre</li> 5233 * <li>50 Cent</li> 5234 * </ul> 5235 * 5236 * <script> 5237 * wysihtml5.dom.resolveList(document.getElementById("list")); 5238 * </script> 5239 * 5240 * <!-- Will result in: --> 5241 * eminem<br> 5242 * dr. dre<br> 5243 * 50 Cent<br> 5244 */ 5245 (function(dom) { 5246 function _isBlockElement(node) { 5247 return dom.getStyle("display").from(node) === "block"; 5248 } 5249 5250 function _isLineBreak(node) { 5251 return node.nodeName === "BR"; 5252 } 5253 5254 function _appendLineBreak(element) { 5255 var lineBreak = element.ownerDocument.createElement("br"); 5256 element.appendChild(lineBreak); 5257 } 5258 5259 function resolveList(list) { 5260 if (list.nodeName !== "MENU" && list.nodeName !== "UL" && list.nodeName !== "OL") { 5261 return; 5262 } 5263 5264 var doc = list.ownerDocument, 5265 fragment = doc.createDocumentFragment(), 5266 previousSibling = list.previousElementSibling || list.previousSibling, 5267 firstChild, 5268 lastChild, 5269 isLastChild, 5270 shouldAppendLineBreak, 5271 listItem; 5272 5273 if (previousSibling && !_isBlockElement(previousSibling)) { 5274 _appendLineBreak(fragment); 5275 } 5276 5277 while (listItem = list.firstChild) { 5278 lastChild = listItem.lastChild; 5279 while (firstChild = listItem.firstChild) { 5280 isLastChild = firstChild === lastChild; 5281 // This needs to be done before appending it to the fragment, as it otherwise will loose style information 5282 shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild); 5283 fragment.appendChild(firstChild); 5284 if (shouldAppendLineBreak) { 5285 _appendLineBreak(fragment); 5286 } 5287 } 5288 5289 listItem.parentNode.removeChild(listItem); 5290 } 5291 list.parentNode.replaceChild(fragment, list); 5292 } 5293 5294 dom.resolveList = resolveList; 5295 })(wysihtml5.dom);/** 5296 * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way 5297 * 5298 * Browser Compatibility: 5299 * - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted" 5300 * - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...) 5301 * 5302 * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons: 5303 * - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'") 5304 * - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...) 5305 * - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire 5306 * - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe 5307 * can do anything as if the sandbox attribute wasn't set 5308 * 5309 * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready 5310 * @param {Object} [config] Optional parameters 5311 * 5312 * @example 5313 * new wysihtml5.dom.Sandbox(function(sandbox) { 5314 * sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">'; 5315 * }); 5316 */ 5317 (function(wysihtml5) { 5318 var /** 5319 * Default configuration 5320 */ 5321 doc = document, 5322 /** 5323 * Properties to unset/protect on the window object 5324 */ 5325 windowProperties = [ 5326 "parent", "top", "opener", "frameElement", "frames", 5327 "localStorage", "globalStorage", "sessionStorage", "indexedDB" 5328 ], 5329 /** 5330 * Properties on the window object which are set to an empty function 5331 */ 5332 windowProperties2 = [ 5333 "open", "close", "openDialog", "showModalDialog", 5334 "alert", "confirm", "prompt", 5335 "openDatabase", "postMessage", 5336 "XMLHttpRequest", "XDomainRequest" 5337 ], 5338 /** 5339 * Properties to unset/protect on the document object 5340 */ 5341 documentProperties = [ 5342 "referrer", 5343 "write", "open", "close" 5344 ]; 5345 5346 wysihtml5.dom.Sandbox = Base.extend( 5347 /** @scope wysihtml5.dom.Sandbox.prototype */ { 5348 5349 constructor: function(readyCallback, config) { 5350 this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION; 5351 this.config = wysihtml5.lang.object({}).merge(config).get(); 5352 this.iframe = this._createIframe(); 5353 }, 5354 5355 insertInto: function(element) { 5356 if (typeof(element) === "string") { 5357 element = doc.getElementById(element); 5358 } 5359 5360 element.appendChild(this.iframe); 5361 }, 5362 5363 getIframe: function() { 5364 return this.iframe; 5365 }, 5366 5367 getWindow: function() { 5368 this._readyError(); 5369 }, 5370 5371 getDocument: function() { 5372 this._readyError(); 5373 }, 5374 5375 destroy: function() { 5376 var iframe = this.getIframe(); 5377 iframe.parentNode.removeChild(iframe); 5378 }, 5379 5380 _readyError: function() { 5381 throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet"); 5382 }, 5383 5384 /** 5385 * Creates the sandbox iframe 5386 * 5387 * Some important notes: 5388 * - We can't use HTML5 sandbox for now: 5389 * setting it causes that the iframe's dom can't be accessed from the outside 5390 * Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom 5391 * But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired. 5392 * In order to make this happen we need to set the "allow-scripts" flag. 5393 * A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all. 5394 * - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document) 5395 * - IE needs to have the security="restricted" attribute set before the iframe is 5396 * inserted into the dom tree 5397 * - Believe it or not but in IE "security" in document.createElement("iframe") is false, even 5398 * though it supports it 5399 * - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore 5400 * - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely 5401 * on the onreadystatechange event 5402 */ 5403 _createIframe: function() { 5404 var that = this, 5405 iframe = doc.createElement("iframe"); 5406 iframe.className = "wysihtml5-sandbox"; 5407 wysihtml5.dom.setAttributes({ 5408 "security": "restricted", 5409 "allowtransparency": "true", 5410 "frameborder": 0, 5411 "width": 0, 5412 "height": 0, 5413 "marginwidth": 0, 5414 "marginheight": 0 5415 }).on(iframe); 5416 5417 // Setting the src like this prevents ssl warnings in IE6 5418 if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) { 5419 iframe.src = "javascript:'<html></html>'"; 5420 } 5421 5422 iframe.onload = function() { 5423 iframe.onreadystatechange = iframe.onload = null; 5424 that._onLoadIframe(iframe); 5425 }; 5426 5427 iframe.onreadystatechange = function() { 5428 if (/loaded|complete/.test(iframe.readyState)) { 5429 iframe.onreadystatechange = iframe.onload = null; 5430 that._onLoadIframe(iframe); 5431 } 5432 }; 5433 5434 return iframe; 5435 }, 5436 5437 /** 5438 * Callback for when the iframe has finished loading 5439 */ 5440 _onLoadIframe: function(iframe) { 5441 // don't resume when the iframe got unloaded (eg. by removing it from the dom) 5442 if (!wysihtml5.dom.contains(doc.documentElement, iframe)) { 5443 return; 5444 } 5445 5446 var that = this, 5447 iframeWindow = iframe.contentWindow, 5448 iframeDocument = iframe.contentWindow.document, 5449 charset = doc.characterSet || doc.charset || "utf-8", 5450 sandboxHtml = this._getHtml({ 5451 charset: charset, 5452 stylesheets: this.config.stylesheets 5453 }); 5454 5455 // Create the basic dom tree including proper DOCTYPE and charset 5456 iframeDocument.open("text/html", "replace"); 5457 iframeDocument.write(sandboxHtml); 5458 iframeDocument.close(); 5459 5460 this.getWindow = function() { return iframe.contentWindow; }; 5461 this.getDocument = function() { return iframe.contentWindow.document; }; 5462 5463 // Catch js errors and pass them to the parent's onerror event 5464 // addEventListener("error") doesn't work properly in some browsers 5465 // TODO: apparently this doesn't work in IE9! 5466 iframeWindow.onerror = function(errorMessage, fileName, lineNumber) { 5467 throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber); 5468 }; 5469 5470 if (!wysihtml5.browser.supportsSandboxedIframes()) { 5471 // Unset a bunch of sensitive variables 5472 // Please note: This isn't hack safe! 5473 // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information 5474 // IE is secure though, which is the most important thing, since IE is the only browser, who 5475 // takes over scripts & styles into contentEditable elements when copied from external websites 5476 // or applications (Microsoft Word, ...) 5477 var i, length; 5478 for (i=0, length=windowProperties.length; i<length; i++) { 5479 this._unset(iframeWindow, windowProperties[i]); 5480 } 5481 for (i=0, length=windowProperties2.length; i<length; i++) { 5482 this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION); 5483 } 5484 for (i=0, length=documentProperties.length; i<length; i++) { 5485 this._unset(iframeDocument, documentProperties[i]); 5486 } 5487 // This doesn't work in Safari 5 5488 // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit 5489 this._unset(iframeDocument, "cookie", "", true); 5490 } 5491 5492 this.loaded = true; 5493 5494 // Trigger the callback 5495 setTimeout(function() { that.callback(that); }, 0); 5496 }, 5497 5498 _getHtml: function(templateVars) { 5499 var stylesheets = templateVars.stylesheets, 5500 html = "", 5501 i = 0, 5502 length; 5503 stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets; 5504 if (stylesheets) { 5505 length = stylesheets.length; 5506 for (; i<length; i++) { 5507 html += '<link rel="stylesheet" href="' + stylesheets[i] + '">'; 5508 } 5509 } 5510 templateVars.stylesheets = html; 5511 5512 return wysihtml5.lang.string( 5513 '<!DOCTYPE html><html><head>' 5514 + '<meta charset="#{charset}">#{stylesheets}</head>' 5515 + '<body></body></html>' 5516 ).interpolate(templateVars); 5517 }, 5518 5519 /** 5520 * Method to unset/override existing variables 5521 * @example 5522 * // Make cookie unreadable and unwritable 5523 * this._unset(document, "cookie", "", true); 5524 */ 5525 _unset: function(object, property, value, setter) { 5526 try { object[property] = value; } catch(e) {} 5527 5528 try { object.__defineGetter__(property, function() { return value; }); } catch(e) {} 5529 if (setter) { 5530 try { object.__defineSetter__(property, function() {}); } catch(e) {} 5531 } 5532 5533 if (!wysihtml5.browser.crashesWhenDefineProperty(property)) { 5534 try { 5535 var config = { 5536 get: function() { return value; } 5537 }; 5538 if (setter) { 5539 config.set = function() {}; 5540 } 5541 Object.defineProperty(object, property, config); 5542 } catch(e) {} 5543 } 5544 } 5545 }); 5546 })(wysihtml5); 5547 (function() { 5548 var mapping = { 5549 "className": "class" 5550 }; 5551 wysihtml5.dom.setAttributes = function(attributes) { 5552 return { 5553 on: function(element) { 5554 for (var i in attributes) { 5555 element.setAttribute(mapping[i] || i, attributes[i]); 5556 } 5557 } 5558 } 5559 }; 5560 })();wysihtml5.dom.setStyles = function(styles) { 5561 return { 5562 on: function(element) { 5563 var style = element.style; 5564 if (typeof(styles) === "string") { 5565 style.cssText += ";" + styles; 5566 return; 5567 } 5568 for (var i in styles) { 5569 if (i === "float") { 5570 style.cssFloat = styles[i]; 5571 style.styleFloat = styles[i]; 5572 } else { 5573 style[i] = styles[i]; 5574 } 5575 } 5576 } 5577 }; 5578 };/** 5579 * Simulate HTML5 placeholder attribute 5580 * 5581 * Needed since 5582 * - div[contentEditable] elements don't support it 5583 * - older browsers (such as IE8 and Firefox 3.6) don't support it at all 5584 * 5585 * @param {Object} parent Instance of main wysihtml5.Editor class 5586 * @param {Element} view Instance of wysihtml5.views.* class 5587 * @param {String} placeholderText 5588 * 5589 * @example 5590 * wysihtml.dom.simulatePlaceholder(this, composer, "Foobar"); 5591 */ 5592 (function(dom) { 5593 dom.simulatePlaceholder = function(editor, view, placeholderText) { 5594 var CLASS_NAME = "placeholder", 5595 unset = function() { 5596 if (view.hasPlaceholderSet()) { 5597 view.clear(); 5598 } 5599 dom.removeClass(view.element, CLASS_NAME); 5600 }, 5601 set = function() { 5602 if (view.isEmpty()) { 5603 view.setValue(placeholderText); 5604 dom.addClass(view.element, CLASS_NAME); 5605 } 5606 }; 5607 5608 editor 5609 .observe("set_placeholder", set) 5610 .observe("unset_placeholder", unset) 5611 .observe("focus:composer", unset) 5612 .observe("paste:composer", unset) 5613 .observe("blur:composer", set); 5614 5615 set(); 5616 }; 5617 })(wysihtml5.dom); 5618 (function(dom) { 5619 var documentElement = document.documentElement; 5620 if ("textContent" in documentElement) { 5621 dom.setTextContent = function(element, text) { 5622 element.textContent = text; 5623 }; 5624 5625 dom.getTextContent = function(element) { 5626 return element.textContent; 5627 }; 5628 } else if ("innerText" in documentElement) { 5629 dom.setTextContent = function(element, text) { 5630 element.innerText = text; 5631 }; 5632 5633 dom.getTextContent = function(element) { 5634 return element.innerText; 5635 }; 5636 } else { 5637 dom.setTextContent = function(element, text) { 5638 element.nodeValue = text; 5639 }; 5640 5641 dom.getTextContent = function(element) { 5642 return element.nodeValue; 5643 }; 5644 } 5645 })(wysihtml5.dom); 5646 5647 /** 5648 * Fix most common html formatting misbehaviors of browsers implementation when inserting 5649 * content via copy & paste contentEditable 5650 * 5651 * @author Christopher Blum 5652 */ 5653 wysihtml5.quirks.cleanPastedHTML = (function() { 5654 // TODO: We probably need more rules here 5655 var defaultRules = { 5656 // When pasting underlined links <a> into a contentEditable, IE thinks, it has to insert <u> to keep the styling 5657 "a u": wysihtml5.dom.replaceWithChildNodes 5658 }; 5659 5660 function cleanPastedHTML(elementOrHtml, rules, context) { 5661 rules = rules || defaultRules; 5662 context = context || elementOrHtml.ownerDocument || document; 5663 5664 var element, 5665 isString = typeof(elementOrHtml) === "string", 5666 method, 5667 matches, 5668 matchesLength, 5669 i, 5670 j = 0; 5671 if (isString) { 5672 element = wysihtml5.dom.getAsDom(elementOrHtml, context); 5673 } else { 5674 element = elementOrHtml; 5675 } 5676 5677 for (i in rules) { 5678 matches = element.querySelectorAll(i); 5679 method = rules[i]; 5680 matchesLength = matches.length; 5681 for (; j<matchesLength; j++) { 5682 method(matches[j]); 5683 } 5684 } 5685 5686 matches = elementOrHtml = rules = null; 5687 5688 return isString ? element.innerHTML : element; 5689 } 5690 5691 return cleanPastedHTML; 5692 })();/** 5693 * IE and Opera leave an empty paragraph in the contentEditable element after clearing it 5694 * 5695 * @param {Object} contentEditableElement The contentEditable element to observe for clearing events 5696 * @exaple 5697 * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); 5698 */ 5699 (function(wysihtml5) { 5700 var dom = wysihtml5.dom; 5701 5702 wysihtml5.quirks.ensureProperClearing = (function() { 5703 var clearIfNecessary = function(event) { 5704 var element = this; 5705 setTimeout(function() { 5706 var innerHTML = element.innerHTML.toLowerCase(); 5707 if (innerHTML == "<p> </p>" || 5708 innerHTML == "<p> </p><p> </p>") { 5709 element.innerHTML = ""; 5710 } 5711 }, 0); 5712 }; 5713 5714 return function(composer) { 5715 dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary); 5716 }; 5717 })(); 5718 5719 5720 5721 /** 5722 * 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 5723 * 5724 * @param {Object} contentEditableElement The contentEditable element to observe for clearing events 5725 * @exaple 5726 * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); 5727 */ 5728 wysihtml5.quirks.ensureProperClearingOfLists = (function() { 5729 var ELEMENTS_THAT_CONTAIN_LI = ["OL", "UL", "MENU"]; 5730 5731 var clearIfNecessary = function(element, contentEditableElement) { 5732 if (!contentEditableElement.firstChild || !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI).contains(contentEditableElement.firstChild.nodeName)) { 5733 return; 5734 } 5735 5736 var list = dom.getParentElement(element, { nodeName: ELEMENTS_THAT_CONTAIN_LI }); 5737 if (!list) { 5738 return; 5739 } 5740 5741 var listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild; 5742 if (!listIsFirstChildOfContentEditable) { 5743 return; 5744 } 5745 5746 var hasOnlyOneListItem = list.childNodes.length <= 1; 5747 if (!hasOnlyOneListItem) { 5748 return; 5749 } 5750 5751 var onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === "" : true; 5752 if (!onlyListItemIsEmpty) { 5753 return; 5754 } 5755 5756 list.parentNode.removeChild(list); 5757 }; 5758 5759 return function(composer) { 5760 dom.observe(composer.element, "keydown", function(event) { 5761 if (event.keyCode !== wysihtml5.BACKSPACE_KEY) { 5762 return; 5763 } 5764 5765 var element = composer.selection.getSelectedNode(); 5766 clearIfNecessary(element, composer.element); 5767 }); 5768 }; 5769 })(); 5770 5771 })(wysihtml5); 5772 // See https://bugzilla.mozilla.org/show_bug.cgi?id=664398 5773 // 5774 // In Firefox this: 5775 // var d = document.createElement("div"); 5776 // d.innerHTML ='<a href="~"></a>'; 5777 // d.innerHTML; 5778 // will result in: 5779 // <a href="%7E"></a> 5780 // which is wrong 5781 (function(wysihtml5) { 5782 var TILDE_ESCAPED = "%7E"; 5783 wysihtml5.quirks.getCorrectInnerHTML = function(element) { 5784 var innerHTML = element.innerHTML; 5785 if (innerHTML.indexOf(TILDE_ESCAPED) === -1) { 5786 return innerHTML; 5787 } 5788 5789 var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"), 5790 url, 5791 urlToSearch, 5792 length, 5793 i; 5794 for (i=0, length=elementsWithTilde.length; i<length; i++) { 5795 url = elementsWithTilde[i].href || elementsWithTilde[i].src; 5796 urlToSearch = wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED); 5797 innerHTML = wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url); 5798 } 5799 return innerHTML; 5800 }; 5801 })(wysihtml5);/** 5802 * Some browsers don't insert line breaks when hitting return in a contentEditable element 5803 * - Opera & IE insert new <p> on return 5804 * - Chrome & Safari insert new <div> on return 5805 * - Firefox inserts <br> on return (yippie!) 5806 * 5807 * @param {Element} element 5808 * 5809 * @example 5810 * wysihtml5.quirks.insertLineBreakOnReturn(element); 5811 */ 5812 (function(wysihtml5) { 5813 var dom = wysihtml5.dom, 5814 USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"], 5815 LIST_TAGS = ["UL", "OL", "MENU"]; 5816 5817 wysihtml5.quirks.insertLineBreakOnReturn = function(composer) { 5818 function unwrap(selectedNode) { 5819 var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2); 5820 if (!parentElement) { 5821 return; 5822 } 5823 5824 var invisibleSpace = document.createTextNode(wysihtml5.INVISIBLE_SPACE); 5825 dom.insert(invisibleSpace).before(parentElement); 5826 dom.replaceWithChildNodes(parentElement); 5827 composer.selection.selectNode(invisibleSpace); 5828 } 5829 5830 function keyDown(event) { 5831 var keyCode = event.keyCode; 5832 if (event.shiftKey || (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) { 5833 return; 5834 } 5835 5836 var element = event.target, 5837 selectedNode = composer.selection.getSelectedNode(), 5838 blockElement = dom.getParentElement(selectedNode, { nodeName: USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS }, 4); 5839 if (blockElement) { 5840 // Some browsers create <p> elements after leaving a list 5841 // check after keydown of backspace and return whether a <p> got inserted and unwrap it 5842 if (blockElement.nodeName === "LI" && (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) { 5843 setTimeout(function() { 5844 var selectedNode = composer.selection.getSelectedNode(), 5845 list, 5846 div; 5847 if (!selectedNode) { 5848 return; 5849 } 5850 5851 list = dom.getParentElement(selectedNode, { 5852 nodeName: LIST_TAGS 5853 }, 2); 5854 5855 if (list) { 5856 return; 5857 } 5858 5859 unwrap(selectedNode); 5860 }, 0); 5861 } else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === wysihtml5.ENTER_KEY) { 5862 setTimeout(function() { 5863 unwrap(composer.selection.getSelectedNode()); 5864 }, 0); 5865 } 5866 return; 5867 } 5868 5869 if (keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) { 5870 composer.commands.exec("insertLineBreak"); 5871 event.preventDefault(); 5872 } 5873 } 5874 5875 // keypress doesn't fire when you hit backspace 5876 dom.observe(composer.element.ownerDocument, "keydown", keyDown); 5877 }; 5878 })(wysihtml5);/** 5879 * Force rerendering of a given element 5880 * Needed to fix display misbehaviors of IE 5881 * 5882 * @param {Element} element The element object which needs to be rerendered 5883 * @example 5884 * wysihtml5.quirks.redraw(document.body); 5885 */ 5886 (function(wysihtml5) { 5887 var CLASS_NAME = "wysihtml5-quirks-redraw"; 5888 5889 wysihtml5.quirks.redraw = function(element) { 5890 wysihtml5.dom.addClass(element, CLASS_NAME); 5891 wysihtml5.dom.removeClass(element, CLASS_NAME); 5892 5893 // Following hack is needed for firefox to make sure that image resize handles are properly removed 5894 try { 5895 var doc = element.ownerDocument; 5896 doc.execCommand("italic", false, null); 5897 doc.execCommand("italic", false, null); 5898 } catch(e) {} 5899 }; 5900 })(wysihtml5);/** 5901 * Selection API 5902 * 5903 * @example 5904 * var selection = new wysihtml5.Selection(editor); 5905 */ 5906 (function(wysihtml5) { 5907 var dom = wysihtml5.dom; 5908 5909 function _getCumulativeOffsetTop(element) { 5910 var top = 0; 5911 if (element.parentNode) { 5912 do { 5913 top += element.offsetTop || 0; 5914 element = element.offsetParent; 5915 } while (element); 5916 } 5917 return top; 5918 } 5919 5920 wysihtml5.Selection = Base.extend( 5921 /** @scope wysihtml5.Selection.prototype */ { 5922 constructor: function(editor) { 5923 // Make sure that our external range library is initialized 5924 window.rangy.init(); 5925 5926 this.editor = editor; 5927 this.composer = editor.composer; 5928 this.doc = this.composer.doc; 5929 }, 5930 5931 /** 5932 * Get the current selection as a bookmark to be able to later restore it 5933 * 5934 * @return {Object} An object that represents the current selection 5935 */ 5936 getBookmark: function() { 5937 var range = this.getRange(); 5938 return range && range.cloneRange(); 5939 }, 5940 5941 /** 5942 * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark 5943 * 5944 * @param {Object} bookmark An object that represents the current selection 5945 */ 5946 setBookmark: function(bookmark) { 5947 if (!bookmark) { 5948 return; 5949 } 5950 5951 this.setSelection(bookmark); 5952 }, 5953 5954 /** 5955 * Set the caret in front of the given node 5956 * 5957 * @param {Object} node The element or text node where to position the caret in front of 5958 * @example 5959 * selection.setBefore(myElement); 5960 */ 5961 setBefore: function(node) { 5962 var range = rangy.createRange(this.doc); 5963 range.setStartBefore(node); 5964 range.setEndBefore(node); 5965 return this.setSelection(range); 5966 }, 5967 5968 /** 5969 * Set the caret after the given node 5970 * 5971 * @param {Object} node The element or text node where to position the caret in front of 5972 * @example 5973 * selection.setBefore(myElement); 5974 */ 5975 setAfter: function(node) { 5976 var range = rangy.createRange(this.doc); 5977 range.setStartAfter(node); 5978 range.setEndAfter(node); 5979 return this.setSelection(range); 5980 }, 5981 5982 /** 5983 * Ability to select/mark nodes 5984 * 5985 * @param {Element} node The node/element to select 5986 * @example 5987 * selection.selectNode(document.getElementById("my-image")); 5988 */ 5989 selectNode: function(node) { 5990 var range = rangy.createRange(this.doc), 5991 isElement = node.nodeType === wysihtml5.ELEMENT_NODE, 5992 canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"), 5993 content = isElement ? node.innerHTML : node.data, 5994 isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE), 5995 displayStyle = dom.getStyle("display").from(node), 5996 isBlockElement = (displayStyle === "block" || displayStyle === "list-item"); 5997 5998 if (isEmpty && isElement && canHaveHTML) { 5999 // Make sure that caret is visible in node by inserting a zero width no breaking space 6000 try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} 6001 } 6002 6003 if (canHaveHTML) { 6004 range.selectNodeContents(node); 6005 } else { 6006 range.selectNode(node); 6007 } 6008 6009 if (canHaveHTML && isEmpty && isElement) { 6010 range.collapse(isBlockElement); 6011 } else if (canHaveHTML && isEmpty) { 6012 range.setStartAfter(node); 6013 range.setEndAfter(node); 6014 } 6015 6016 this.setSelection(range); 6017 }, 6018 6019 /** 6020 * Get the node which contains the selection 6021 * 6022 * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange" 6023 * @return {Object} The node that contains the caret 6024 * @example 6025 * var nodeThatContainsCaret = selection.getSelectedNode(); 6026 */ 6027 getSelectedNode: function(controlRange) { 6028 var selection, 6029 range; 6030 6031 if (controlRange && this.doc.selection && this.doc.selection.type === "Control") { 6032 range = this.doc.selection.createRange(); 6033 if (range && range.length) { 6034 return range.item(0); 6035 } 6036 } 6037 6038 selection = this.getSelection(this.doc); 6039 if (selection.focusNode === selection.anchorNode) { 6040 return selection.focusNode; 6041 } else { 6042 range = this.getRange(this.doc); 6043 return range ? range.commonAncestorContainer : this.doc.body; 6044 } 6045 }, 6046 6047 executeAndRestore: function(method, restoreScrollPosition) { 6048 var body = this.doc.body, 6049 oldScrollTop = restoreScrollPosition && body.scrollTop, 6050 oldScrollLeft = restoreScrollPosition && body.scrollLeft, 6051 className = "_wysihtml5-temp-placeholder", 6052 placeholderHTML = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>', 6053 range = this.getRange(this.doc), 6054 newRange; 6055 6056 // Nothing selected, execute and say goodbye 6057 if (!range) { 6058 method(body, body); 6059 return; 6060 } 6061 6062 var node = range.createContextualFragment(placeholderHTML); 6063 range.insertNode(node); 6064 6065 // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder 6066 try { 6067 method(range.startContainer, range.endContainer); 6068 } catch(e3) { 6069 setTimeout(function() { throw e3; }, 0); 6070 } 6071 6072 caretPlaceholder = this.doc.querySelector("." + className); 6073 if (caretPlaceholder) { 6074 newRange = rangy.createRange(this.doc); 6075 newRange.selectNode(caretPlaceholder); 6076 newRange.deleteContents(); 6077 this.setSelection(newRange); 6078 } else { 6079 // fallback for when all hell breaks loose 6080 body.focus(); 6081 } 6082 6083 if (restoreScrollPosition) { 6084 body.scrollTop = oldScrollTop; 6085 body.scrollLeft = oldScrollLeft; 6086 } 6087 6088 // Remove it again, just to make sure that the placeholder is definitely out of the dom tree 6089 try { 6090 caretPlaceholder.parentNode.removeChild(caretPlaceholder); 6091 } catch(e4) {} 6092 }, 6093 6094 /** 6095 * Different approach of preserving the selection (doesn't modify the dom) 6096 * Takes all text nodes in the selection and saves the selection position in the first and last one 6097 */ 6098 executeAndRestoreSimple: function(method) { 6099 var range = this.getRange(), 6100 body = this.doc.body, 6101 newRange, 6102 firstNode, 6103 lastNode, 6104 textNodes, 6105 rangeBackup; 6106 6107 // Nothing selected, execute and say goodbye 6108 if (!range) { 6109 method(body, body); 6110 return; 6111 } 6112 6113 textNodes = range.getNodes([3]); 6114 firstNode = textNodes[0] || range.startContainer; 6115 lastNode = textNodes[textNodes.length - 1] || range.endContainer; 6116 6117 rangeBackup = { 6118 collapsed: range.collapsed, 6119 startContainer: firstNode, 6120 startOffset: firstNode === range.startContainer ? range.startOffset : 0, 6121 endContainer: lastNode, 6122 endOffset: lastNode === range.endContainer ? range.endOffset : lastNode.length 6123 }; 6124 6125 try { 6126 method(range.startContainer, range.endContainer); 6127 } catch(e) { 6128 setTimeout(function() { throw e; }, 0); 6129 } 6130 6131 newRange = rangy.createRange(this.doc); 6132 try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {} 6133 try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {} 6134 try { this.setSelection(newRange); } catch(e3) {} 6135 }, 6136 6137 /** 6138 * Insert html at the caret position and move the cursor after the inserted html 6139 * 6140 * @param {String} html HTML string to insert 6141 * @example 6142 * selection.insertHTML("<p>foobar</p>"); 6143 */ 6144 insertHTML: function(html) { 6145 var range = rangy.createRange(this.doc), 6146 node = range.createContextualFragment(html), 6147 lastChild = node.lastChild; 6148 this.insertNode(node); 6149 if (lastChild) { 6150 this.setAfter(lastChild); 6151 } 6152 }, 6153 6154 /** 6155 * Insert a node at the caret position and move the cursor behind it 6156 * 6157 * @param {Object} node HTML string to insert 6158 * @example 6159 * selection.insertNode(document.createTextNode("foobar")); 6160 */ 6161 insertNode: function(node) { 6162 var range = this.getRange(); 6163 if (range) { 6164 range.insertNode(node); 6165 } 6166 }, 6167 6168 /** 6169 * Wraps current selection with the given node 6170 * 6171 * @param {Object} node The node to surround the selected elements with 6172 */ 6173 surround: function(node) { 6174 var range = this.getRange(); 6175 if (!range) { 6176 return; 6177 } 6178 6179 try { 6180 // This only works when the range boundaries are not overlapping other elements 6181 range.surroundContents(node); 6182 this.selectNode(node); 6183 } catch(e) { 6184 // fallback 6185 node.appendChild(range.extractContents()); 6186 range.insertNode(node); 6187 } 6188 }, 6189 6190 /** 6191 * Scroll the current caret position into the view 6192 * FIXME: This is a bit hacky, there might be a smarter way of doing this 6193 * 6194 * @example 6195 * selection.scrollIntoView(); 6196 */ 6197 scrollIntoView: function() { 6198 var doc = this.doc, 6199 hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight, 6200 tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() { 6201 var element = doc.createElement("span"); 6202 // The element needs content in order to be able to calculate it's position properly 6203 element.innerHTML = wysihtml5.INVISIBLE_SPACE; 6204 return element; 6205 })(), 6206 offsetTop; 6207 6208 if (hasScrollBars) { 6209 this.insertNode(tempElement); 6210 offsetTop = _getCumulativeOffsetTop(tempElement); 6211 tempElement.parentNode.removeChild(tempElement); 6212 if (offsetTop > doc.body.scrollTop) { 6213 doc.body.scrollTop = offsetTop; 6214 } 6215 } 6216 }, 6217 6218 /** 6219 * Select line where the caret is in 6220 */ 6221 selectLine: function() { 6222 if (wysihtml5.browser.supportsSelectionModify()) { 6223 this._selectLine_W3C(); 6224 } else if (this.doc.selection) { 6225 this._selectLine_MSIE(); 6226 } 6227 }, 6228 6229 /** 6230 * See https://developer.mozilla.org/en/DOM/Selection/modify 6231 */ 6232 _selectLine_W3C: function() { 6233 var win = this.doc.defaultView, 6234 selection = win.getSelection(); 6235 selection.modify("extend", "left", "lineboundary"); 6236 selection.modify("extend", "right", "lineboundary"); 6237 }, 6238 6239 _selectLine_MSIE: function() { 6240 var range = this.doc.selection.createRange(), 6241 rangeTop = range.boundingTop, 6242 rangeHeight = range.boundingHeight, 6243 scrollWidth = this.doc.body.scrollWidth, 6244 rangeBottom, 6245 rangeEnd, 6246 measureNode, 6247 i, 6248 j; 6249 6250 if (!range.moveToPoint) { 6251 return; 6252 } 6253 6254 if (rangeTop === 0) { 6255 // Don't know why, but when the selection ends at the end of a line 6256 // range.boundingTop is 0 6257 measureNode = this.doc.createElement("span"); 6258 this.insertNode(measureNode); 6259 rangeTop = measureNode.offsetTop; 6260 measureNode.parentNode.removeChild(measureNode); 6261 } 6262 6263 rangeTop += 1; 6264 6265 for (i=-10; i<scrollWidth; i+=2) { 6266 try { 6267 range.moveToPoint(i, rangeTop); 6268 break; 6269 } catch(e1) {} 6270 } 6271 6272 // Investigate the following in order to handle multi line selections 6273 // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0); 6274 rangeBottom = rangeTop; 6275 rangeEnd = this.doc.selection.createRange(); 6276 for (j=scrollWidth; j>=0; j--) { 6277 try { 6278 rangeEnd.moveToPoint(j, rangeBottom); 6279 break; 6280 } catch(e2) {} 6281 } 6282 6283 range.setEndPoint("EndToEnd", rangeEnd); 6284 range.select(); 6285 }, 6286 6287 getText: function() { 6288 var selection = this.getSelection(); 6289 return selection ? selection.toString() : ""; 6290 }, 6291 6292 getNodes: function(nodeType, filter) { 6293 var range = this.getRange(); 6294 if (range) { 6295 return range.getNodes([nodeType], filter); 6296 } else { 6297 return []; 6298 } 6299 }, 6300 6301 getRange: function() { 6302 var selection = this.getSelection(); 6303 return selection && selection.rangeCount && selection.getRangeAt(0); 6304 }, 6305 6306 getSelection: function() { 6307 return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow); 6308 }, 6309 6310 setSelection: function(range) { 6311 var win = this.doc.defaultView || this.doc.parentWindow, 6312 selection = rangy.getSelection(win); 6313 return selection.setSingleRange(range); 6314 } 6315 }); 6316 6317 })(wysihtml5); 6318 /** 6319 * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license. 6320 * http://code.google.com/p/rangy/ 6321 * 6322 * changed in order to be able ... 6323 * - to use custom tags 6324 * - to detect and replace similar css classes via reg exp 6325 */ 6326 (function(wysihtml5, rangy) { 6327 var defaultTagName = "span"; 6328 6329 var REG_EXP_WHITE_SPACE = /\s+/g; 6330 6331 function hasClass(el, cssClass, regExp) { 6332 if (!el.className) { 6333 return false; 6334 } 6335 6336 var matchingClassNames = el.className.match(regExp) || []; 6337 return matchingClassNames[matchingClassNames.length - 1] === cssClass; 6338 } 6339 6340 function addClass(el, cssClass, regExp) { 6341 if (el.className) { 6342 removeClass(el, regExp); 6343 el.className += " " + cssClass; 6344 } else { 6345 el.className = cssClass; 6346 } 6347 } 6348 6349 function removeClass(el, regExp) { 6350 if (el.className) { 6351 el.className = el.className.replace(regExp, ""); 6352 } 6353 } 6354 6355 function hasSameClasses(el1, el2) { 6356 return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " "); 6357 } 6358 6359 function replaceWithOwnChildren(el) { 6360 var parent = el.parentNode; 6361 while (el.firstChild) { 6362 parent.insertBefore(el.firstChild, el); 6363 } 6364 parent.removeChild(el); 6365 } 6366 6367 function elementsHaveSameNonClassAttributes(el1, el2) { 6368 if (el1.attributes.length != el2.attributes.length) { 6369 return false; 6370 } 6371 for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) { 6372 attr1 = el1.attributes[i]; 6373 name = attr1.name; 6374 if (name != "class") { 6375 attr2 = el2.attributes.getNamedItem(name); 6376 if (attr1.specified != attr2.specified) { 6377 return false; 6378 } 6379 if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) { 6380 return false; 6381 } 6382 } 6383 } 6384 return true; 6385 } 6386 6387 function isSplitPoint(node, offset) { 6388 if (rangy.dom.isCharacterDataNode(node)) { 6389 if (offset == 0) { 6390 return !!node.previousSibling; 6391 } else if (offset == node.length) { 6392 return !!node.nextSibling; 6393 } else { 6394 return true; 6395 } 6396 } 6397 6398 return offset > 0 && offset < node.childNodes.length; 6399 } 6400 6401 function splitNodeAt(node, descendantNode, descendantOffset) { 6402 var newNode; 6403 if (rangy.dom.isCharacterDataNode(descendantNode)) { 6404 if (descendantOffset == 0) { 6405 descendantOffset = rangy.dom.getNodeIndex(descendantNode); 6406 descendantNode = descendantNode.parentNode; 6407 } else if (descendantOffset == descendantNode.length) { 6408 descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1; 6409 descendantNode = descendantNode.parentNode; 6410 } else { 6411 newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset); 6412 } 6413 } 6414 if (!newNode) { 6415 newNode = descendantNode.cloneNode(false); 6416 if (newNode.id) { 6417 newNode.removeAttribute("id"); 6418 } 6419 var child; 6420 while ((child = descendantNode.childNodes[descendantOffset])) { 6421 newNode.appendChild(child); 6422 } 6423 rangy.dom.insertAfter(newNode, descendantNode); 6424 } 6425 return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode)); 6426 } 6427 6428 function Merge(firstNode) { 6429 this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE); 6430 this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; 6431 this.textNodes = [this.firstTextNode]; 6432 } 6433 6434 Merge.prototype = { 6435 doMerge: function() { 6436 var textBits = [], textNode, parent, text; 6437 for (var i = 0, len = this.textNodes.length; i < len; ++i) { 6438 textNode = this.textNodes[i]; 6439 parent = textNode.parentNode; 6440 textBits[i] = textNode.data; 6441 if (i) { 6442 parent.removeChild(textNode); 6443 if (!parent.hasChildNodes()) { 6444 parent.parentNode.removeChild(parent); 6445 } 6446 } 6447 } 6448 this.firstTextNode.data = text = textBits.join(""); 6449 return text; 6450 }, 6451 6452 getLength: function() { 6453 var i = this.textNodes.length, len = 0; 6454 while (i--) { 6455 len += this.textNodes[i].length; 6456 } 6457 return len; 6458 }, 6459 6460 toString: function() { 6461 var textBits = []; 6462 for (var i = 0, len = this.textNodes.length; i < len; ++i) { 6463 textBits[i] = "'" + this.textNodes[i].data + "'"; 6464 } 6465 return "[Merge(" + textBits.join(",") + ")]"; 6466 } 6467 }; 6468 6469 function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) { 6470 this.tagNames = tagNames || [defaultTagName]; 6471 this.cssClass = cssClass || ""; 6472 this.similarClassRegExp = similarClassRegExp; 6473 this.normalize = normalize; 6474 this.applyToAnyTagName = false; 6475 } 6476 6477 HTMLApplier.prototype = { 6478 getAncestorWithClass: function(node) { 6479 var cssClassMatch; 6480 while (node) { 6481 cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true; 6482 if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) { 6483 return node; 6484 } 6485 node = node.parentNode; 6486 } 6487 return false; 6488 }, 6489 6490 // Normalizes nodes after applying a CSS class to a Range. 6491 postApply: function(textNodes, range) { 6492 var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; 6493 6494 var merges = [], currentMerge; 6495 6496 var rangeStartNode = firstNode, rangeEndNode = lastNode; 6497 var rangeStartOffset = 0, rangeEndOffset = lastNode.length; 6498 6499 var textNode, precedingTextNode; 6500 6501 for (var i = 0, len = textNodes.length; i < len; ++i) { 6502 textNode = textNodes[i]; 6503 precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false); 6504 if (precedingTextNode) { 6505 if (!currentMerge) { 6506 currentMerge = new Merge(precedingTextNode); 6507 merges.push(currentMerge); 6508 } 6509 currentMerge.textNodes.push(textNode); 6510 if (textNode === firstNode) { 6511 rangeStartNode = currentMerge.firstTextNode; 6512 rangeStartOffset = rangeStartNode.length; 6513 } 6514 if (textNode === lastNode) { 6515 rangeEndNode = currentMerge.firstTextNode; 6516 rangeEndOffset = currentMerge.getLength(); 6517 } 6518 } else { 6519 currentMerge = null; 6520 } 6521 } 6522 6523 // Test whether the first node after the range needs merging 6524 var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true); 6525 if (nextTextNode) { 6526 if (!currentMerge) { 6527 currentMerge = new Merge(lastNode); 6528 merges.push(currentMerge); 6529 } 6530 currentMerge.textNodes.push(nextTextNode); 6531 } 6532 6533 // Do the merges 6534 if (merges.length) { 6535 for (i = 0, len = merges.length; i < len; ++i) { 6536 merges[i].doMerge(); 6537 } 6538 // Set the range boundaries 6539 range.setStart(rangeStartNode, rangeStartOffset); 6540 range.setEnd(rangeEndNode, rangeEndOffset); 6541 } 6542 }, 6543 6544 getAdjacentMergeableTextNode: function(node, forward) { 6545 var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE); 6546 var el = isTextNode ? node.parentNode : node; 6547 var adjacentNode; 6548 var propName = forward ? "nextSibling" : "previousSibling"; 6549 if (isTextNode) { 6550 // Can merge if the node's previous/next sibling is a text node 6551 adjacentNode = node[propName]; 6552 if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) { 6553 return adjacentNode; 6554 } 6555 } else { 6556 // Compare element with its sibling 6557 adjacentNode = el[propName]; 6558 if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) { 6559 return adjacentNode[forward ? "firstChild" : "lastChild"]; 6560 } 6561 } 6562 return null; 6563 }, 6564 6565 areElementsMergeable: function(el1, el2) { 6566 return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase()) 6567 && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase()) 6568 && hasSameClasses(el1, el2) 6569 && elementsHaveSameNonClassAttributes(el1, el2); 6570 }, 6571 6572 createContainer: function(doc) { 6573 var el = doc.createElement(this.tagNames[0]); 6574 if (this.cssClass) { 6575 el.className = this.cssClass; 6576 } 6577 return el; 6578 }, 6579 6580 applyToTextNode: function(textNode) { 6581 var parent = textNode.parentNode; 6582 if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) { 6583 if (this.cssClass) { 6584 addClass(parent, this.cssClass, this.similarClassRegExp); 6585 } 6586 } else { 6587 var el = this.createContainer(rangy.dom.getDocument(textNode)); 6588 textNode.parentNode.insertBefore(el, textNode); 6589 el.appendChild(textNode); 6590 } 6591 }, 6592 6593 isRemovable: function(el) { 6594 return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass; 6595 }, 6596 6597 undoToTextNode: function(textNode, range, ancestorWithClass) { 6598 if (!range.containsNode(ancestorWithClass)) { 6599 // Split out the portion of the ancestor from which we can remove the CSS class 6600 var ancestorRange = range.cloneRange(); 6601 ancestorRange.selectNode(ancestorWithClass); 6602 6603 if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) { 6604 splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset); 6605 range.setEndAfter(ancestorWithClass); 6606 } 6607 if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) { 6608 ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset); 6609 } 6610 } 6611 6612 if (this.similarClassRegExp) { 6613 removeClass(ancestorWithClass, this.similarClassRegExp); 6614 } 6615 if (this.isRemovable(ancestorWithClass)) { 6616 replaceWithOwnChildren(ancestorWithClass); 6617 } 6618 }, 6619 6620 applyToRange: function(range) { 6621 var textNodes = range.getNodes([wysihtml5.TEXT_NODE]); 6622 if (!textNodes.length) { 6623 try { 6624 var node = this.createContainer(range.endContainer.ownerDocument); 6625 range.surroundContents(node); 6626 this.selectNode(range, node); 6627 return; 6628 } catch(e) {} 6629 } 6630 6631 range.splitBoundaries(); 6632 textNodes = range.getNodes([wysihtml5.TEXT_NODE]); 6633 6634 if (textNodes.length) { 6635 var textNode; 6636 6637 for (var i = 0, len = textNodes.length; i < len; ++i) { 6638 textNode = textNodes[i]; 6639 if (!this.getAncestorWithClass(textNode)) { 6640 this.applyToTextNode(textNode); 6641 } 6642 } 6643 6644 range.setStart(textNodes[0], 0); 6645 textNode = textNodes[textNodes.length - 1]; 6646 range.setEnd(textNode, textNode.length); 6647 6648 if (this.normalize) { 6649 this.postApply(textNodes, range); 6650 } 6651 } 6652 }, 6653 6654 undoToRange: function(range) { 6655 var textNodes = range.getNodes([wysihtml5.TEXT_NODE]), textNode, ancestorWithClass; 6656 if (textNodes.length) { 6657 range.splitBoundaries(); 6658 textNodes = range.getNodes([wysihtml5.TEXT_NODE]); 6659 } else { 6660 var doc = range.endContainer.ownerDocument, 6661 node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE); 6662 range.insertNode(node); 6663 range.selectNode(node); 6664 textNodes = [node]; 6665 } 6666 6667 for (var i = 0, len = textNodes.length; i < len; ++i) { 6668 textNode = textNodes[i]; 6669 ancestorWithClass = this.getAncestorWithClass(textNode); 6670 if (ancestorWithClass) { 6671 this.undoToTextNode(textNode, range, ancestorWithClass); 6672 } 6673 } 6674 6675 if (len == 1) { 6676 this.selectNode(range, textNodes[0]); 6677 } else { 6678 range.setStart(textNodes[0], 0); 6679 textNode = textNodes[textNodes.length - 1]; 6680 range.setEnd(textNode, textNode.length); 6681 6682 if (this.normalize) { 6683 this.postApply(textNodes, range); 6684 } 6685 } 6686 }, 6687 6688 selectNode: function(range, node) { 6689 var isElement = node.nodeType === wysihtml5.ELEMENT_NODE, 6690 canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true, 6691 content = isElement ? node.innerHTML : node.data, 6692 isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE); 6693 6694 if (isEmpty && isElement && canHaveHTML) { 6695 // Make sure that caret is visible in node by inserting a zero width no breaking space 6696 try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} 6697 } 6698 range.selectNodeContents(node); 6699 if (isEmpty && isElement) { 6700 range.collapse(false); 6701 } else if (isEmpty) { 6702 range.setStartAfter(node); 6703 range.setEndAfter(node); 6704 } 6705 }, 6706 6707 getTextSelectedByRange: function(textNode, range) { 6708 var textRange = range.cloneRange(); 6709 textRange.selectNodeContents(textNode); 6710 6711 var intersectionRange = textRange.intersection(range); 6712 var text = intersectionRange ? intersectionRange.toString() : ""; 6713 textRange.detach(); 6714 6715 return text; 6716 }, 6717 6718 isAppliedToRange: function(range) { 6719 var ancestors = [], 6720 ancestor, 6721 textNodes = range.getNodes([wysihtml5.TEXT_NODE]); 6722 if (!textNodes.length) { 6723 ancestor = this.getAncestorWithClass(range.startContainer); 6724 return ancestor ? [ancestor] : false; 6725 } 6726 6727 for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) { 6728 selectedText = this.getTextSelectedByRange(textNodes[i], range); 6729 ancestor = this.getAncestorWithClass(textNodes[i]); 6730 if (selectedText != "" && !ancestor) { 6731 return false; 6732 } else { 6733 ancestors.push(ancestor); 6734 } 6735 } 6736 return ancestors; 6737 }, 6738 6739 toggleRange: function(range) { 6740 if (this.isAppliedToRange(range)) { 6741 this.undoToRange(range); 6742 } else { 6743 this.applyToRange(range); 6744 } 6745 } 6746 }; 6747 6748 wysihtml5.selection.HTMLApplier = HTMLApplier; 6749 6750 })(wysihtml5, rangy);/** 6751 * Rich Text Query/Formatting Commands 6752 * 6753 * @example 6754 * var commands = new wysihtml5.Commands(editor); 6755 */ 6756 wysihtml5.Commands = Base.extend( 6757 /** @scope wysihtml5.Commands.prototype */ { 6758 constructor: function(editor) { 6759 this.editor = editor; 6760 this.composer = editor.composer; 6761 this.doc = this.composer.doc; 6762 }, 6763 6764 /** 6765 * Check whether the browser supports the given command 6766 * 6767 * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") 6768 * @example 6769 * commands.supports("createLink"); 6770 */ 6771 support: function(command) { 6772 return wysihtml5.browser.supportsCommand(this.doc, command); 6773 }, 6774 6775 /** 6776 * Check whether the browser supports the given command 6777 * 6778 * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList") 6779 * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...) 6780 * @example 6781 * commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg"); 6782 */ 6783 exec: function(command, value) { 6784 var obj = wysihtml5.commands[command], 6785 args = wysihtml5.lang.array(arguments).get(), 6786 method = obj && obj.exec, 6787 result = null; 6788 6789 this.editor.fire("beforecommand:composer"); 6790 6791 if (method) { 6792 args.unshift(this.composer); 6793 result = method.apply(obj, args); 6794 } else { 6795 try { 6796 // try/catch for buggy firefox 6797 result = this.doc.execCommand(command, false, value); 6798 } catch(e) {} 6799 } 6800 6801 this.editor.fire("aftercommand:composer"); 6802 return result; 6803 }, 6804 6805 /** 6806 * Check whether the current command is active 6807 * If the caret is within a bold text, then calling this with command "bold" should return true 6808 * 6809 * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") 6810 * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src) 6811 * @return {Boolean} Whether the command is active 6812 * @example 6813 * var isCurrentSelectionBold = commands.state("bold"); 6814 */ 6815 state: function(command, commandValue) { 6816 var obj = wysihtml5.commands[command], 6817 args = wysihtml5.lang.array(arguments).get(), 6818 method = obj && obj.state; 6819 if (method) { 6820 args.unshift(this.composer); 6821 return method.apply(obj, args); 6822 } else { 6823 try { 6824 // try/catch for buggy firefox 6825 return this.doc.queryCommandState(command); 6826 } catch(e) { 6827 return false; 6828 } 6829 } 6830 }, 6831 6832 /** 6833 * Get the current command's value 6834 * 6835 * @param {String} command The command string which to check (eg. "formatBlock") 6836 * @return {String} The command value 6837 * @example 6838 * var currentBlockElement = commands.value("formatBlock"); 6839 */ 6840 value: function(command) { 6841 var obj = wysihtml5.commands[command], 6842 method = obj && obj.value; 6843 if (method) { 6844 return method.call(obj, this.composer, command); 6845 } else { 6846 try { 6847 // try/catch for buggy firefox 6848 return this.doc.queryCommandValue(command); 6849 } catch(e) { 6850 return null; 6851 } 6852 } 6853 } 6854 }); 6855 (function(wysihtml5) { 6856 var undef; 6857 6858 wysihtml5.commands.bold = { 6859 exec: function(composer, command) { 6860 return wysihtml5.commands.formatInline.exec(composer, command, "b"); 6861 }, 6862 6863 state: function(composer, command, color) { 6864 // element.ownerDocument.queryCommandState("bold") results: 6865 // firefox: only <b> 6866 // chrome: <b>, <strong>, <h1>, <h2>, ... 6867 // ie: <b>, <strong> 6868 // opera: <b>, <strong> 6869 return wysihtml5.commands.formatInline.state(composer, command, "b"); 6870 }, 6871 6872 value: function() { 6873 return undef; 6874 } 6875 }; 6876 })(wysihtml5); 6877 6878 (function(wysihtml5) { 6879 var undef, 6880 NODE_NAME = "A", 6881 dom = wysihtml5.dom; 6882 6883 function _removeFormat(composer, anchors) { 6884 var length = anchors.length, 6885 i = 0, 6886 anchor, 6887 codeElement, 6888 textContent; 6889 for (; i<length; i++) { 6890 anchor = anchors[i]; 6891 codeElement = dom.getParentElement(anchor, { nodeName: "code" }); 6892 textContent = dom.getTextContent(anchor); 6893 6894 // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking 6895 // else replace <a> with its childNodes 6896 if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) { 6897 // <code> element is used to prevent later auto-linking of the content 6898 codeElement = dom.renameElement(anchor, "code"); 6899 } else { 6900 dom.replaceWithChildNodes(anchor); 6901 } 6902 } 6903 } 6904 6905 function _format(composer, attributes) { 6906 var doc = composer.doc, 6907 tempClass = "_wysihtml5-temp-" + (+new Date()), 6908 tempClassRegExp = /non-matching-class/g, 6909 i = 0, 6910 length, 6911 anchors, 6912 anchor, 6913 hasElementChild, 6914 isEmpty, 6915 elementToSetCaretAfter, 6916 textContent, 6917 whiteSpace, 6918 j; 6919 wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp); 6920 anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass); 6921 length = anchors.length; 6922 for (; i<length; i++) { 6923 anchor = anchors[i]; 6924 anchor.removeAttribute("class"); 6925 for (j in attributes) { 6926 anchor.setAttribute(j, attributes[j]); 6927 } 6928 } 6929 6930 elementToSetCaretAfter = anchor; 6931 if (length === 1) { 6932 textContent = dom.getTextContent(anchor); 6933 hasElementChild = !!anchor.querySelector("*"); 6934 isEmpty = textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE; 6935 if (!hasElementChild && isEmpty) { 6936 dom.setTextContent(anchor, attributes.text || anchor.href); 6937 whiteSpace = doc.createTextNode(" "); 6938 composer.selection.setAfter(anchor); 6939 composer.selection.insertNode(whiteSpace); 6940 elementToSetCaretAfter = whiteSpace; 6941 } 6942 } 6943 composer.selection.setAfter(elementToSetCaretAfter); 6944 } 6945 6946 wysihtml5.commands.createLink = { 6947 /** 6948 * TODO: Use HTMLApplier or formatInline here 6949 * 6950 * Turns selection into a link 6951 * If selection is already a link, it removes the link and wraps it with a <code> element 6952 * The <code> element is needed to avoid auto linking 6953 * 6954 * @example 6955 * // either ... 6956 * wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de"); 6957 * // ... or ... 6958 * wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" }); 6959 */ 6960 exec: function(composer, command, value) { 6961 var anchors = this.state(composer, command); 6962 if (anchors) { 6963 // Selection contains links 6964 composer.selection.executeAndRestore(function() { 6965 _removeFormat(composer, anchors); 6966 }); 6967 } else { 6968 // Create links 6969 value = typeof(value) === "object" ? value : { href: value }; 6970 _format(composer, value); 6971 } 6972 }, 6973 6974 state: function(composer, command) { 6975 return wysihtml5.commands.formatInline.state(composer, command, "A"); 6976 }, 6977 6978 value: function() { 6979 return undef; 6980 } 6981 }; 6982 })(wysihtml5);/** 6983 * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags 6984 * which we don't want 6985 * Instead we set a css class 6986 */ 6987 (function(wysihtml5) { 6988 var undef, 6989 REG_EXP = /wysiwyg-font-size-[a-z\-]+/g; 6990 6991 wysihtml5.commands.fontSize = { 6992 exec: function(composer, command, size) { 6993 return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP); 6994 }, 6995 6996 state: function(composer, command, size) { 6997 return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP); 6998 }, 6999 7000 value: function() { 7001 return undef; 7002 } 7003 }; 7004 })(wysihtml5); 7005 /** 7006 * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags 7007 * which we don't want 7008 * Instead we set a css class 7009 */ 7010 (function(wysihtml5) { 7011 var undef, 7012 REG_EXP = /wysiwyg-color-[a-z]+/g; 7013 7014 wysihtml5.commands.foreColor = { 7015 exec: function(composer, command, color) { 7016 return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-color-" + color, REG_EXP); 7017 }, 7018 7019 state: function(composer, command, color) { 7020 return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP); 7021 }, 7022 7023 value: function() { 7024 return undef; 7025 } 7026 }; 7027 })(wysihtml5);(function(wysihtml5) { 7028 var undef, 7029 dom = wysihtml5.dom, 7030 DEFAULT_NODE_NAME = "DIV", 7031 // Following elements are grouped 7032 // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4 7033 // instead of creating a H4 within a H1 which would result in semantically invalid html 7034 BLOCK_ELEMENTS_GROUP = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", DEFAULT_NODE_NAME]; 7035 7036 /** 7037 * Remove similiar classes (based on classRegExp) 7038 * and add the desired class name 7039 */ 7040 function _addClass(element, className, classRegExp) { 7041 if (element.className) { 7042 _removeClass(element, classRegExp); 7043 element.className += " " + className; 7044 } else { 7045 element.className = className; 7046 } 7047 } 7048 7049 function _removeClass(element, classRegExp) { 7050 element.className = element.className.replace(classRegExp, ""); 7051 } 7052 7053 /** 7054 * Check whether given node is a text node and whether it's empty 7055 */ 7056 function _isBlankTextNode(node) { 7057 return node.nodeType === wysihtml5.TEXT_NODE && !wysihtml5.lang.string(node.data).trim(); 7058 } 7059 7060 /** 7061 * Returns previous sibling node that is not a blank text node 7062 */ 7063 function _getPreviousSiblingThatIsNotBlank(node) { 7064 var previousSibling = node.previousSibling; 7065 while (previousSibling && _isBlankTextNode(previousSibling)) { 7066 previousSibling = previousSibling.previousSibling; 7067 } 7068 return previousSibling; 7069 } 7070 7071 /** 7072 * Returns next sibling node that is not a blank text node 7073 */ 7074 function _getNextSiblingThatIsNotBlank(node) { 7075 var nextSibling = node.nextSibling; 7076 while (nextSibling && _isBlankTextNode(nextSibling)) { 7077 nextSibling = nextSibling.nextSibling; 7078 } 7079 return nextSibling; 7080 } 7081 7082 /** 7083 * Adds line breaks before and after the given node if the previous and next siblings 7084 * aren't already causing a visual line break (block element or <br>) 7085 */ 7086 function _addLineBreakBeforeAndAfter(node) { 7087 var doc = node.ownerDocument, 7088 nextSibling = _getNextSiblingThatIsNotBlank(node), 7089 previousSibling = _getPreviousSiblingThatIsNotBlank(node); 7090 7091 if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) { 7092 node.parentNode.insertBefore(doc.createElement("br"), nextSibling); 7093 } 7094 if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) { 7095 node.parentNode.insertBefore(doc.createElement("br"), node); 7096 } 7097 } 7098 7099 /** 7100 * Removes line breaks before and after the given node 7101 */ 7102 function _removeLineBreakBeforeAndAfter(node) { 7103 var nextSibling = _getNextSiblingThatIsNotBlank(node), 7104 previousSibling = _getPreviousSiblingThatIsNotBlank(node); 7105 7106 if (nextSibling && _isLineBreak(nextSibling)) { 7107 nextSibling.parentNode.removeChild(nextSibling); 7108 } 7109 if (previousSibling && _isLineBreak(previousSibling)) { 7110 previousSibling.parentNode.removeChild(previousSibling); 7111 } 7112 } 7113 7114 function _removeLastChildIfLineBreak(node) { 7115 var lastChild = node.lastChild; 7116 if (lastChild && _isLineBreak(lastChild)) { 7117 lastChild.parentNode.removeChild(lastChild); 7118 } 7119 } 7120 7121 function _isLineBreak(node) { 7122 7123 return node.nodeName === "BR"; 7124 } 7125 7126 /** 7127 * Checks whether the elment causes a visual line break 7128 * (<br> or block elements) 7129 */ 7130 function _isLineBreakOrBlockElement(element) { 7131 if (_isLineBreak(element)) { 7132 return true; 7133 } 7134 7135 if (dom.getStyle("display").from(element) === "block") { 7136 return true; 7137 } 7138 7139 return false; 7140 } 7141 7142 /** 7143 * Execute native query command 7144 * and if necessary modify the inserted node's className 7145 */ 7146 function _execCommand(doc, command, nodeName, className) { 7147 if (className) { 7148 var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) { 7149 var target = event.target, 7150 displayStyle; 7151 if (target.nodeType !== wysihtml5.ELEMENT_NODE) { 7152 return; 7153 } 7154 displayStyle = dom.getStyle("display").from(target); 7155 if (displayStyle.substr(0, 6) !== "inline") { 7156 // Make sure that only block elements receive the given class 7157 target.className += " " + className; 7158 } 7159 }); 7160 } 7161 doc.execCommand(command, false, nodeName); 7162 if (eventListener) { 7163 eventListener.stop(); 7164 } 7165 } 7166 7167 function _selectLineAndWrap(composer, element) { 7168 composer.selection.selectLine(); 7169 composer.selection.surround(element); 7170 _removeLineBreakBeforeAndAfter(element); 7171 _removeLastChildIfLineBreak(element); 7172 composer.selection.selectNode(element); 7173 } 7174 7175 function _hasClasses(element) { 7176 return !!wysihtml5.lang.string(element.className).trim(); 7177 } 7178 7179 wysihtml5.commands.formatBlock = { 7180 exec: function(composer, command, nodeName, className, classRegExp) { 7181 var doc = composer.doc, 7182 blockElement = this.state(composer, command, nodeName, className, classRegExp), 7183 selectedNode; 7184 7185 nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName; 7186 7187 if (blockElement) { 7188 composer.selection.executeAndRestoreSimple(function() { 7189 if (classRegExp) { 7190 _removeClass(blockElement, classRegExp); 7191 } 7192 var hasClasses = _hasClasses(blockElement); 7193 if (!hasClasses && blockElement.nodeName === (nodeName || DEFAULT_NODE_NAME)) { 7194 // Insert a line break afterwards and beforewards when there are siblings 7195 // that are not of type line break or block element 7196 _addLineBreakBeforeAndAfter(blockElement); 7197 dom.replaceWithChildNodes(blockElement); 7198 } else if (hasClasses) { 7199 // Make sure that styling is kept by renaming the element to <div> and copying over the class name 7200 dom.renameElement(blockElement, DEFAULT_NODE_NAME); 7201 } 7202 }); 7203 return; 7204 } 7205 7206 // Find similiar block element and rename it (<h2 class="foo"></h2> => <h1 class="foo"></h1>) 7207 if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) { 7208 selectedNode = composer.selection.getSelectedNode(); 7209 blockElement = dom.getParentElement(selectedNode, { 7210 nodeName: BLOCK_ELEMENTS_GROUP 7211 }); 7212 7213 if (blockElement) { 7214 composer.selection.executeAndRestoreSimple(function() { 7215 // Rename current block element to new block element and add class 7216 if (nodeName) { 7217 blockElement = dom.renameElement(blockElement, nodeName); 7218 } 7219 if (className) { 7220 _addClass(blockElement, className, classRegExp); 7221 } 7222 }); 7223 return; 7224 } 7225 } 7226 7227 if (composer.commands.support(command)) { 7228 _execCommand(doc, command, nodeName || DEFAULT_NODE_NAME, className); 7229 return; 7230 } 7231 7232 blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME); 7233 if (className) { 7234 blockElement.className = className; 7235 } 7236 _selectLineAndWrap(composer, blockElement); 7237 }, 7238 7239 state: function(composer, command, nodeName, className, classRegExp) { 7240 nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName; 7241 var selectedNode = composer.selection.getSelectedNode(); 7242 return dom.getParentElement(selectedNode, { 7243 nodeName: nodeName, 7244 className: className, 7245 classRegExp: classRegExp 7246 }); 7247 }, 7248 7249 value: function() { 7250 return undef; 7251 } 7252 }; 7253 })(wysihtml5);/** 7254 * formatInline scenarios for tag "B" (| = caret, |foo| = selected text) 7255 * 7256 * #1 caret in unformatted text: 7257 * abcdefg| 7258 * output: 7259 * abcdefg<b>|</b> 7260 * 7261 * #2 unformatted text selected: 7262 * abc|deg|h 7263 * output: 7264 * abc<b>|deg|</b>h 7265 * 7266 * #3 unformatted text selected across boundaries: 7267 * ab|c <span>defg|h</span> 7268 * output: 7269 * ab<b>|c </b><span><b>defg</b>|h</span> 7270 * 7271 * #4 formatted text entirely selected 7272 * <b>|abc|</b> 7273 * output: 7274 * |abc| 7275 * 7276 * #5 formatted text partially selected 7277 * <b>ab|c|</b> 7278 * output: 7279 * <b>ab</b>|c| 7280 * 7281 * #6 formatted text selected across boundaries 7282 * <span>ab|c</span> <b>de|fgh</b> 7283 * output: 7284 * <span>ab|c</span> de|<b>fgh</b> 7285 */ 7286 (function(wysihtml5) { 7287 var undef, 7288 // Treat <b> as <strong> and vice versa 7289 ALIAS_MAPPING = { 7290 "strong": "b", 7291 "em": "i", 7292 "b": "strong", 7293 "i": "em" 7294 }, 7295 htmlApplier = {}; 7296 7297 function _getTagNames(tagName) { 7298 var alias = ALIAS_MAPPING[tagName]; 7299 return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()]; 7300 } 7301 7302 function _getApplier(tagName, className, classRegExp) { 7303 var identifier = tagName + ":" + className; 7304 if (!htmlApplier[identifier]) { 7305 htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true); 7306 } 7307 return htmlApplier[identifier]; 7308 } 7309 7310 wysihtml5.commands.formatInline = { 7311 exec: function(composer, command, tagName, className, classRegExp) { 7312 var range = composer.selection.getRange(); 7313 if (!range) { 7314 return false; 7315 } 7316 _getApplier(tagName, className, classRegExp).toggleRange(range); 7317 composer.selection.setSelection(range); 7318 }, 7319 7320 state: function(composer, command, tagName, className, classRegExp) { 7321 var doc = composer.doc, 7322 aliasTagName = ALIAS_MAPPING[tagName] || tagName, 7323 range; 7324 7325 // Check whether the document contains a node with the desired tagName 7326 if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) && 7327 !wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) { 7328 return false; 7329 } 7330 7331 // Check whether the document contains a node with the desired className 7332 if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) { 7333 return false; 7334 } 7335 7336 range = composer.selection.getRange(); 7337 if (!range) { 7338 return false; 7339 } 7340 7341 return _getApplier(tagName, className, classRegExp).isAppliedToRange(range); 7342 }, 7343 7344 value: function() { 7345 return undef; 7346 } 7347 }; 7348 })(wysihtml5);(function(wysihtml5) { 7349 var undef; 7350 7351 wysihtml5.commands.insertHTML = { 7352 exec: function(composer, command, html) { 7353 if (composer.commands.support(command)) { 7354 composer.doc.execCommand(command, false, html); 7355 } else { 7356 composer.selection.insertHTML(html); 7357 } 7358 }, 7359 7360 state: function() { 7361 return false; 7362 }, 7363 7364 value: function() { 7365 return undef; 7366 } 7367 }; 7368 })(wysihtml5);(function(wysihtml5) { 7369 var NODE_NAME = "IMG"; 7370 7371 wysihtml5.commands.insertImage = { 7372 /** 7373 * Inserts an <img> 7374 * If selection is already an image link, it removes it 7375 * 7376 * @example 7377 * // either ... 7378 * wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg"); 7379 * // ... or ... 7380 * wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" }); 7381 */ 7382 exec: function(composer, command, value) { 7383 value = typeof(value) === "object" ? value : { src: value }; 7384 7385 var doc = composer.doc, 7386 image = this.state(composer), 7387 textNode, 7388 i, 7389 parent; 7390 7391 if (image) { 7392 // Image already selected, set the caret before it and delete it 7393 composer.selection.setBefore(image); 7394 parent = image.parentNode; 7395 parent.removeChild(image); 7396 7397 // and it's parent <a> too if it hasn't got any other relevant child nodes 7398 wysihtml5.dom.removeEmptyTextNodes(parent); 7399 if (parent.nodeName === "A" && !parent.firstChild) { 7400 composer.selection.setAfter(parent); 7401 parent.parentNode.removeChild(parent); 7402 } 7403 7404 // firefox and ie sometimes don't remove the image handles, even though the image got removed 7405 wysihtml5.quirks.redraw(composer.element); 7406 return; 7407 } 7408 7409 image = doc.createElement(NODE_NAME); 7410 7411 for (i in value) { 7412 image[i] = value[i]; 7413 } 7414 7415 composer.selection.insertNode(image); 7416 if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) { 7417 textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE); 7418 composer.selection.insertNode(textNode); 7419 composer.selection.setAfter(textNode); 7420 } else { 7421 composer.selection.setAfter(image); 7422 } 7423 }, 7424 7425 state: function(composer) { 7426 var doc = composer.doc, 7427 selectedNode, 7428 text, 7429 imagesInSelection; 7430 7431 if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) { 7432 return false; 7433 } 7434 7435 selectedNode = composer.selection.getSelectedNode(); 7436 if (!selectedNode) { 7437 return false; 7438 } 7439 7440 if (selectedNode.nodeName === NODE_NAME) { 7441 // This works perfectly in IE 7442 return selectedNode; 7443 } 7444 7445 if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) { 7446 return false; 7447 } 7448 7449 text = composer.selection.getText(); 7450 text = wysihtml5.lang.string(text).trim(); 7451 if (text) { 7452 return false; 7453 } 7454 7455 imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) { 7456 return node.nodeName === "IMG"; 7457 }); 7458 7459 if (imagesInSelection.length !== 1) { 7460 return false; 7461 } 7462 7463 return imagesInSelection[0]; 7464 }, 7465 7466 value: function(composer) { 7467 var image = this.state(composer); 7468 return image && image.src; 7469 } 7470 }; 7471 })(wysihtml5);(function(wysihtml5) { 7472 var undef, 7473 LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : ""); 7474 7475 wysihtml5.commands.insertLineBreak = { 7476 exec: function(composer, command) { 7477 if (composer.commands.support(command)) { 7478 composer.doc.execCommand(command, false, null); 7479 if (!wysihtml5.browser.autoScrollsToCaret()) { 7480 composer.selection.scrollIntoView(); 7481 } 7482 } else { 7483 composer.commands.exec("insertHTML", LINE_BREAK); 7484 } 7485 }, 7486 7487 state: function() { 7488 return false; 7489 }, 7490 7491 value: function() { 7492 return undef; 7493 } 7494 }; 7495 })(wysihtml5);(function(wysihtml5) { 7496 var undef; 7497 7498 wysihtml5.commands.insertOrderedList = { 7499 exec: function(composer, command) { 7500 var doc = composer.doc, 7501 selectedNode = composer.selection.getSelectedNode(), 7502 list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), 7503 otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), 7504 tempClassName = "_wysihtml5-temp-" + new Date().getTime(), 7505 isEmpty, 7506 tempElement; 7507 7508 if (composer.commands.support(command)) { 7509 doc.execCommand(command, false, null); 7510 return; 7511 } 7512 7513 if (list) { 7514 // Unwrap list 7515 // <ol><li>foo</li><li>bar</li></ol> 7516 // becomes: 7517 // foo<br>bar<br> 7518 composer.selection.executeAndRestoreSimple(function() { 7519 wysihtml5.dom.resolveList(list); 7520 }); 7521 } else if (otherList) { 7522 // Turn an unordered list into an ordered list 7523 // <ul><li>foo</li><li>bar</li></ul> 7524 // becomes: 7525 // <ol><li>foo</li><li>bar</li></ol> 7526 composer.selection.executeAndRestoreSimple(function() { 7527 wysihtml5.dom.renameElement(otherList, "ol"); 7528 }); 7529 } else { 7530 // Create list 7531 composer.commands.exec("formatBlock", "div", tempClassName); 7532 tempElement = doc.querySelector("." + tempClassName); 7533 isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE; 7534 composer.selection.executeAndRestoreSimple(function() { 7535 list = wysihtml5.dom.convertToList(tempElement, "ol"); 7536 }); 7537 if (isEmpty) { 7538 composer.selection.selectNode(list.querySelector("li")); 7539 } 7540 } 7541 }, 7542 7543 state: function(composer) { 7544 var selectedNode = composer.selection.getSelectedNode(); 7545 return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }); 7546 }, 7547 7548 value: function() { 7549 return undef; 7550 } 7551 }; 7552 })(wysihtml5);(function(wysihtml5) { 7553 var undef; 7554 7555 wysihtml5.commands.insertUnorderedList = { 7556 exec: function(composer, command) { 7557 var doc = composer.doc, 7558 selectedNode = composer.selection.getSelectedNode(), 7559 list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), 7560 otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), 7561 tempClassName = "_wysihtml5-temp-" + new Date().getTime(), 7562 isEmpty, 7563 tempElement; 7564 7565 if (composer.commands.support(command)) { 7566 doc.execCommand(command, false, null); 7567 return; 7568 } 7569 7570 if (list) { 7571 // Unwrap list 7572 // <ul><li>foo</li><li>bar</li></ul> 7573 // becomes: 7574 // foo<br>bar<br> 7575 composer.selection.executeAndRestoreSimple(function() { 7576 wysihtml5.dom.resolveList(list); 7577 }); 7578 } else if (otherList) { 7579 // Turn an ordered list into an unordered list 7580 // <ol><li>foo</li><li>bar</li></ol> 7581 // becomes: 7582 // <ul><li>foo</li><li>bar</li></ul> 7583 composer.selection.executeAndRestoreSimple(function() { 7584 wysihtml5.dom.renameElement(otherList, "ul"); 7585 }); 7586 } else { 7587 // Create list 7588 composer.commands.exec("formatBlock", "div", tempClassName); 7589 tempElement = doc.querySelector("." + tempClassName); 7590 isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE; 7591 composer.selection.executeAndRestoreSimple(function() { 7592 list = wysihtml5.dom.convertToList(tempElement, "ul"); 7593 }); 7594 if (isEmpty) { 7595 composer.selection.selectNode(list.querySelector("li")); 7596 } 7597 } 7598 }, 7599 7600 state: function(composer) { 7601 var selectedNode = composer.selection.getSelectedNode(); 7602 return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }); 7603 }, 7604 7605 value: function() { 7606 return undef; 7607 } 7608 }; 7609 })(wysihtml5);(function(wysihtml5) { 7610 var undef; 7611 7612 wysihtml5.commands.italic = { 7613 exec: function(composer, command) { 7614 return wysihtml5.commands.formatInline.exec(composer, command, "i"); 7615 }, 7616 7617 state: function(composer, command, color) { 7618 // element.ownerDocument.queryCommandState("italic") results: 7619 // firefox: only <i> 7620 // chrome: <i>, <em>, <blockquote>, ... 7621 // ie: <i>, <em> 7622 // opera: only <i> 7623 return wysihtml5.commands.formatInline.state(composer, command, "i"); 7624 }, 7625 7626 value: function() { 7627 return undef; 7628 } 7629 }; 7630 })(wysihtml5);(function(wysihtml5) { 7631 var undef, 7632 CLASS_NAME = "wysiwyg-text-align-center", 7633 REG_EXP = /wysiwyg-text-align-[a-z]+/g; 7634 7635 wysihtml5.commands.justifyCenter = { 7636 exec: function(composer, command) { 7637 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); 7638 }, 7639 7640 state: function(composer, command) { 7641 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); 7642 }, 7643 7644 value: function() { 7645 return undef; 7646 } 7647 }; 7648 })(wysihtml5);(function(wysihtml5) { 7649 var undef, 7650 CLASS_NAME = "wysiwyg-text-align-left", 7651 REG_EXP = /wysiwyg-text-align-[a-z]+/g; 7652 7653 wysihtml5.commands.justifyLeft = { 7654 exec: function(composer, command) { 7655 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); 7656 }, 7657 7658 state: function(composer, command) { 7659 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); 7660 }, 7661 7662 value: function() { 7663 return undef; 7664 } 7665 }; 7666 })(wysihtml5);(function(wysihtml5) { 7667 var undef, 7668 CLASS_NAME = "wysiwyg-text-align-right", 7669 REG_EXP = /wysiwyg-text-align-[a-z]+/g; 7670 7671 wysihtml5.commands.justifyRight = { 7672 exec: function(composer, command) { 7673 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); 7674 }, 7675 7676 state: function(composer, command) { 7677 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); 7678 }, 7679 7680 value: function() { 7681 return undef; 7682 } 7683 }; 7684 })(wysihtml5);(function(wysihtml5) { 7685 var undef; 7686 wysihtml5.commands.underline = { 7687 exec: function(composer, command) { 7688 return wysihtml5.commands.formatInline.exec(composer, command, "u"); 7689 }, 7690 7691 state: function(composer, command) { 7692 return wysihtml5.commands.formatInline.state(composer, command, "u"); 7693 }, 7694 7695 value: function() { 7696 return undef; 7697 } 7698 }; 7699 })(wysihtml5);/** 7700 * Undo Manager for wysihtml5 7701 * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface 7702 */ 7703 (function(wysihtml5) { 7704 var Z_KEY = 90, 7705 Y_KEY = 89, 7706 BACKSPACE_KEY = 8, 7707 DELETE_KEY = 46, 7708 MAX_HISTORY_ENTRIES = 40, 7709 UNDO_HTML = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>', 7710 REDO_HTML = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>', 7711 dom = wysihtml5.dom; 7712 7713 function cleanTempElements(doc) { 7714 var tempElement; 7715 while (tempElement = doc.querySelector("._wysihtml5-temp")) { 7716 tempElement.parentNode.removeChild(tempElement); 7717 } 7718 } 7719 7720 wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend( 7721 /** @scope wysihtml5.UndoManager.prototype */ { 7722 constructor: function(editor) { 7723 this.editor = editor; 7724 this.composer = editor.composer; 7725 this.element = this.composer.element; 7726 this.history = [this.composer.getValue()]; 7727 this.position = 1; 7728 7729 // Undo manager currently only supported in browsers who have the insertHTML command (not IE) 7730 if (this.composer.commands.support("insertHTML")) { 7731 this._observe(); 7732 } 7733 }, 7734 7735 _observe: function() { 7736 var that = this, 7737 doc = this.composer.sandbox.getDocument(), 7738 lastKey; 7739 7740 // Catch CTRL+Z and CTRL+Y 7741 dom.observe(this.element, "keydown", function(event) { 7742 if (event.altKey || (!event.ctrlKey && !event.metaKey)) { 7743 return; 7744 } 7745 7746 var keyCode = event.keyCode, 7747 isUndo = keyCode === Z_KEY && !event.shiftKey, 7748 isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY); 7749 7750 if (isUndo) { 7751 that.undo(); 7752 event.preventDefault(); 7753 } else if (isRedo) { 7754 that.redo(); 7755 event.preventDefault(); 7756 } 7757 }); 7758 7759 // Catch delete and backspace 7760 dom.observe(this.element, "keydown", function(event) { 7761 var keyCode = event.keyCode; 7762 if (keyCode === lastKey) { 7763 return; 7764 } 7765 7766 lastKey = keyCode; 7767 7768 if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) { 7769 that.transact(); 7770 } 7771 }); 7772 7773 // Now this is very hacky: 7774 // These days browsers don't offer a undo/redo event which we could hook into 7775 // to be notified when the user hits undo/redo in the contextmenu. 7776 // Therefore we simply insert two elements as soon as the contextmenu gets opened. 7777 // The last element being inserted will be immediately be removed again by a exexCommand("undo") 7778 // => When the second element appears in the dom tree then we know the user clicked "redo" in the context menu 7779 // => When the first element disappears from the dom tree then we know the user clicked "undo" in the context menu 7780 if (wysihtml5.browser.hasUndoInContextMenu()) { 7781 var interval, observed, cleanUp = function() { 7782 cleanTempElements(doc); 7783 clearInterval(interval); 7784 }; 7785 7786 dom.observe(this.element, "contextmenu", function() { 7787 cleanUp(); 7788 that.composer.selection.executeAndRestoreSimple(function() { 7789 if (that.element.lastChild) { 7790 that.composer.selection.setAfter(that.element.lastChild); 7791 } 7792 7793 // enable undo button in context menu 7794 doc.execCommand("insertHTML", false, UNDO_HTML); 7795 // enable redo button in context menu 7796 doc.execCommand("insertHTML", false, REDO_HTML); 7797 doc.execCommand("undo", false, null); 7798 }); 7799 7800 interval = setInterval(function() { 7801 if (doc.getElementById("_wysihtml5-redo")) { 7802 cleanUp(); 7803 that.redo(); 7804 } else if (!doc.getElementById("_wysihtml5-undo")) { 7805 cleanUp(); 7806 that.undo(); 7807 } 7808 }, 400); 7809 7810 if (!observed) { 7811 observed = true; 7812 dom.observe(document, "mousedown", cleanUp); 7813 dom.observe(doc, ["mousedown", "paste", "cut", "copy"], cleanUp); 7814 } 7815 }); 7816 } 7817 7818 this.editor 7819 .observe("newword:composer", function() { 7820 that.transact(); 7821 }) 7822 7823 .observe("beforecommand:composer", function() { 7824 that.transact(); 7825 }); 7826 }, 7827 7828 transact: function() { 7829 var previousHtml = this.history[this.position - 1], 7830 currentHtml = this.composer.getValue(); 7831 7832 if (currentHtml == previousHtml) { 7833 return; 7834 } 7835 7836 var length = this.history.length = this.position; 7837 if (length > MAX_HISTORY_ENTRIES) { 7838 this.history.shift(); 7839 this.position--; 7840 } 7841 7842 this.position++; 7843 this.history.push(currentHtml); 7844 }, 7845 7846 undo: function() { 7847 this.transact(); 7848 7849 if (this.position <= 1) { 7850 return; 7851 } 7852 7853 this.set(this.history[--this.position - 1]); 7854 this.editor.fire("undo:composer"); 7855 }, 7856 7857 redo: function() { 7858 if (this.position >= this.history.length) { 7859 return; 7860 } 7861 7862 this.set(this.history[++this.position - 1]); 7863 this.editor.fire("redo:composer"); 7864 }, 7865 7866 set: function(html) { 7867 this.composer.setValue(html); 7868 this.editor.focus(true); 7869 } 7870 }); 7871 })(wysihtml5); 7872 /** 7873 * TODO: the following methods still need unit test coverage 7874 */ 7875 wysihtml5.views.View = Base.extend( 7876 /** @scope wysihtml5.views.View.prototype */ { 7877 constructor: function(parent, textareaElement, config) { 7878 this.parent = parent; 7879 this.element = textareaElement; 7880 this.config = config; 7881 7882 this._observeViewChange(); 7883 }, 7884 7885 _observeViewChange: function() { 7886 var that = this; 7887 this.parent.observe("beforeload", function() { 7888 that.parent.observe("change_view", function(view) { 7889 if (view === that.name) { 7890 that.parent.currentView = that; 7891 that.show(); 7892 // Using tiny delay here to make sure that the placeholder is set before focusing 7893 setTimeout(function() { that.focus(); }, 0); 7894 } else { 7895 that.hide(); 7896 } 7897 }); 7898 }); 7899 }, 7900 7901 focus: function() { 7902 if (this.element.ownerDocument.querySelector(":focus") === this.element) { 7903 return; 7904 } 7905 7906 try { this.element.focus(); } catch(e) {} 7907 }, 7908 7909 hide: function() { 7910 this.element.style.display = "none"; 7911 }, 7912 7913 show: function() { 7914 this.element.style.display = ""; 7915 }, 7916 7917 disable: function() { 7918 this.element.setAttribute("disabled", "disabled"); 7919 }, 7920 7921 enable: function() { 7922 this.element.removeAttribute("disabled"); 7923 } 7924 });(function(wysihtml5) { 7925 var dom = wysihtml5.dom, 7926 browser = wysihtml5.browser; 7927 7928 wysihtml5.views.Composer = wysihtml5.views.View.extend( 7929 /** @scope wysihtml5.views.Composer.prototype */ { 7930 name: "composer", 7931 7932 // Needed for firefox in order to display a proper caret in an empty contentEditable 7933 CARET_HACK: "<br>", 7934 7935 constructor: function(parent, textareaElement, config) { 7936 this.base(parent, textareaElement, config); 7937 this.textarea = this.parent.textarea; 7938 this._initSandbox(); 7939 }, 7940 7941 clear: function() { 7942 this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK; 7943 }, 7944 7945 getValue: function(parse) { 7946 var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element); 7947 7948 if (parse) { 7949 value = this.parent.parse(value); 7950 } 7951 7952 // Replace all "zero width no breaking space" chars 7953 // which are used as hacks to enable some functionalities 7954 // Also remove all CARET hacks that somehow got left 7955 value = wysihtml5.lang.string(value).replace(wysihtml5.INVISIBLE_SPACE).by(""); 7956 7957 return value; 7958 }, 7959 7960 setValue: function(html, parse) { 7961 if (parse) { 7962 html = this.parent.parse(html); 7963 } 7964 this.element.innerHTML = html; 7965 }, 7966 7967 show: function() { 7968 this.iframe.style.display = this._displayStyle || ""; 7969 7970 // Firefox needs this, otherwise contentEditable becomes uneditable 7971 this.disable(); 7972 this.enable(); 7973 }, 7974 7975 hide: function() { 7976 this._displayStyle = dom.getStyle("display").from(this.iframe); 7977 if (this._displayStyle === "none") { 7978 this._displayStyle = null; 7979 } 7980 this.iframe.style.display = "none"; 7981 }, 7982 7983 disable: function() { 7984 this.element.removeAttribute("contentEditable"); 7985 this.base(); 7986 }, 7987 7988 enable: function() { 7989 this.element.setAttribute("contentEditable", "true"); 7990 this.base(); 7991 }, 7992 7993 focus: function(setToEnd) { 7994 // IE 8 fires the focus event after .focus() 7995 // This is needed by our simulate_placeholder.js to work 7996 // therefore we clear it ourselves this time 7997 if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) { 7998 this.clear(); 7999 } 8000 8001 this.base(); 8002 8003 var lastChild = this.element.lastChild; 8004 if (setToEnd && lastChild) { 8005 if (lastChild.nodeName === "BR") { 8006 this.selection.setBefore(this.element.lastChild); 8007 } else { 8008 this.selection.setAfter(this.element.lastChild); 8009 } 8010 } 8011 }, 8012 8013 getTextContent: function() { 8014 return dom.getTextContent(this.element); 8015 }, 8016 8017 hasPlaceholderSet: function() { 8018 return this.getTextContent() == this.textarea.element.getAttribute("placeholder"); 8019 }, 8020 8021 isEmpty: function() { 8022 var innerHTML = this.element.innerHTML, 8023 elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea"; 8024 return innerHTML === "" || 8025 innerHTML === this.CARET_HACK || 8026 8027 this.hasPlaceholderSet() || 8028 (this.getTextContent() === "" && !this.element.querySelector(elementsWithVisualValue)); 8029 }, 8030 8031 _initSandbox: function() { 8032 var that = this; 8033 8034 this.sandbox = new dom.Sandbox(function() { 8035 that._create(); 8036 }, { 8037 stylesheets: this.config.stylesheets 8038 }); 8039 this.iframe = this.sandbox.getIframe(); 8040 8041 // Create hidden field which tells the server after submit, that the user used an wysiwyg editor 8042 var hiddenField = document.createElement("input"); 8043 hiddenField.type = "hidden"; 8044 hiddenField.name = "_wysihtml5_mode"; 8045 hiddenField.value = 1; 8046 8047 // Store reference to current wysihtml5 instance on the textarea element 8048 var textareaElement = this.textarea.element; 8049 dom.insert(this.iframe).after(textareaElement); 8050 dom.insert(hiddenField).after(textareaElement); 8051 }, 8052 8053 _create: function() { 8054 var that = this; 8055 8056 this.doc = this.sandbox.getDocument(); 8057 this.element = this.doc.body; 8058 this.textarea = this.parent.textarea; 8059 this.element.innerHTML = this.textarea.getValue(true); 8060 this.enable(); 8061 8062 // Make sure our selection handler is ready 8063 this.selection = new wysihtml5.Selection(this.parent); 8064 8065 // Make sure commands dispatcher is ready 8066 this.commands = new wysihtml5.Commands(this.parent); 8067 8068 dom.copyAttributes([ 8069 "className", "spellcheck", "title", "lang", "dir", "accessKey" 8070 ]).from(this.textarea.element).to(this.element); 8071 8072 dom.addClass(this.element, this.config.composerClassName); 8073 8074 // Make the editor look like the original textarea, by syncing styles 8075 if (this.config.style) { 8076 this.style(); 8077 } 8078 8079 this.observe(); 8080 8081 var name = this.config.name; 8082 if (name) { 8083 dom.addClass(this.element, name); 8084 dom.addClass(this.iframe, name); 8085 } 8086 8087 // Simulate html5 placeholder attribute on contentEditable element 8088 var placeholderText = typeof(this.config.placeholder) === "string" 8089 ? this.config.placeholder 8090 : this.textarea.element.getAttribute("placeholder"); 8091 if (placeholderText) { 8092 dom.simulatePlaceholder(this.parent, this, placeholderText); 8093 } 8094 8095 // Make sure that the browser avoids using inline styles whenever possible 8096 this.commands.exec("styleWithCSS", false); 8097 8098 this._initAutoLinking(); 8099 this._initObjectResizing(); 8100 this._initUndoManager(); 8101 8102 // Simulate html5 autofocus on contentEditable element 8103 if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) { 8104 setTimeout(function() { that.focus(); }, 100); 8105 } 8106 8107 wysihtml5.quirks.insertLineBreakOnReturn(this); 8108 8109 // IE sometimes leaves a single paragraph, which can't be removed by the user 8110 if (!browser.clearsContentEditableCorrectly()) { 8111 wysihtml5.quirks.ensureProperClearing(this); 8112 } 8113 8114 if (!browser.clearsListsInContentEditableCorrectly()) { 8115 wysihtml5.quirks.ensureProperClearingOfLists(this); 8116 } 8117 8118 // Set up a sync that makes sure that textarea and editor have the same content 8119 if (this.initSync && this.config.sync) { 8120 this.initSync(); 8121 } 8122 8123 // Okay hide the textarea, we are ready to go 8124 this.textarea.hide(); 8125 8126 // Fire global (before-)load event 8127 this.parent.fire("beforeload").fire("load"); 8128 }, 8129 8130 _initAutoLinking: function() { 8131 var that = this, 8132 supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(), 8133 supportsAutoLinking = browser.doesAutoLinkingInContentEditable(); 8134 if (supportsDisablingOfAutoLinking) { 8135 this.commands.exec("autoUrlDetect", false); 8136 } 8137 8138 if (!this.config.autoLink) { 8139 return; 8140 } 8141 8142 // Only do the auto linking by ourselves when the browser doesn't support auto linking 8143 // OR when he supports auto linking but we were able to turn it off (IE9+) 8144 if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) { 8145 this.parent.observe("newword:composer", function() { 8146 that.selection.executeAndRestore(function(startContainer, endContainer) { 8147 dom.autoLink(endContainer.parentNode); 8148 }); 8149 }); 8150 } 8151 8152 // Assuming we have the following: 8153 // <a href="http://www.google.de">http://www.google.de</a> 8154 // If a user now changes the url in the innerHTML we want to make sure that 8155 // it's synchronized with the href attribute (as long as the innerHTML is still a url) 8156 var // Use a live NodeList to check whether there are any links in the document 8157 links = this.sandbox.getDocument().getElementsByTagName("a"), 8158 // The autoLink helper method reveals a reg exp to detect correct urls 8159 urlRegExp = dom.autoLink.URL_REG_EXP, 8160 getTextContent = function(element) { 8161 var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim(); 8162 if (textContent.substr(0, 4) === "www.") { 8163 textContent = "http://" + textContent; 8164 } 8165 return textContent; 8166 }; 8167 8168 dom.observe(this.element, "keydown", function(event) { 8169 if (!links.length) { 8170 return; 8171 } 8172 8173 var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument), 8174 link = dom.getParentElement(selectedNode, { nodeName: "A" }, 4), 8175 textContent; 8176 8177 if (!link) { 8178 return; 8179 } 8180 8181 textContent = getTextContent(link); 8182 // keydown is fired before the actual content is changed 8183 // therefore we set a timeout to change the href 8184 setTimeout(function() { 8185 var newTextContent = getTextContent(link); 8186 if (newTextContent === textContent) { 8187 return; 8188 } 8189 8190 // Only set href when new href looks like a valid url 8191 if (newTextContent.match(urlRegExp)) { 8192 link.setAttribute("href", newTextContent); 8193 } 8194 }, 0); 8195 }); 8196 }, 8197 8198 _initObjectResizing: function() { 8199 var properties = ["width", "height"], 8200 propertiesLength = properties.length, 8201 element = this.element; 8202 8203 this.commands.exec("enableObjectResizing", this.config.allowObjectResizing); 8204 8205 if (this.config.allowObjectResizing) { 8206 // IE sets inline styles after resizing objects 8207 // The following lines make sure that the width/height css properties 8208 // are copied over to the width/height attributes 8209 if (browser.supportsEvent("resizeend")) { 8210 dom.observe(element, "resizeend", function(event) { 8211 var target = event.target || event.srcElement, 8212 style = target.style, 8213 i = 0, 8214 property; 8215 for(; i<propertiesLength; i++) { 8216 property = properties[i]; 8217 if (style[property]) { 8218 target.setAttribute(property, parseInt(style[property], 10)); 8219 style[property] = ""; 8220 } 8221 } 8222 // After resizing IE sometimes forgets to remove the old resize handles 8223 wysihtml5.quirks.redraw(element); 8224 }); 8225 } 8226 } else { 8227 if (browser.supportsEvent("resizestart")) { 8228 dom.observe(element, "resizestart", function(event) { event.preventDefault(); }); 8229 } 8230 } 8231 }, 8232 8233 _initUndoManager: function() { 8234 new wysihtml5.UndoManager(this.parent); 8235 } 8236 }); 8237 })(wysihtml5);(function(wysihtml5) { 8238 var dom = wysihtml5.dom, 8239 doc = document, 8240 win = window, 8241 HOST_TEMPLATE = doc.createElement("div"), 8242 /** 8243 * Styles to copy from textarea to the composer element 8244 */ 8245 TEXT_FORMATTING = [ 8246 "background-color", 8247 "color", "cursor", 8248 "font-family", "font-size", "font-style", "font-variant", "font-weight", 8249 "line-height", "letter-spacing", 8250 "text-align", "text-decoration", "text-indent", "text-rendering", 8251 "word-break", "word-wrap", "word-spacing" 8252 ], 8253 /** 8254 * Styles to copy from textarea to the iframe 8255 */ 8256 BOX_FORMATTING = [ 8257 "background-color", 8258 "border-collapse", 8259 "border-bottom-color", "border-bottom-style", "border-bottom-width", 8260 "border-left-color", "border-left-style", "border-left-width", 8261 "border-right-color", "border-right-style", "border-right-width", 8262 "border-top-color", "border-top-style", "border-top-width", 8263 "clear", "display", "float", 8264 "margin-bottom", "margin-left", "margin-right", "margin-top", 8265 "outline-color", "outline-offset", "outline-width", "outline-style", 8266 "padding-left", "padding-right", "padding-top", "padding-bottom", 8267 "position", "top", "left", "right", "bottom", "z-index", 8268 "vertical-align", "text-align", 8269 "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing", 8270 "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow", 8271 "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius", 8272 "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius", 8273 "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius", 8274 "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius", 8275 "width", "height" 8276 ], 8277 /** 8278 * Styles to sync while the window gets resized 8279 */ 8280 RESIZE_STYLE = [ 8281 "width", "height", 8282 "top", "left", "right", "bottom" 8283 ], 8284 ADDITIONAL_CSS_RULES = [ 8285 "html { height: 100%; }", 8286 "body { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }", 8287 "._wysihtml5-temp { display: none; }", 8288 wysihtml5.browser.isGecko ? 8289 "body.placeholder { color: graytext !important; }" : 8290 "body.placeholder { color: #a9a9a9 !important; }", 8291 "body[disabled] { background-color: #eee !important; color: #999 !important; cursor: default !important; }", 8292 // Ensure that user see's broken images and can delete them 8293 "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }" 8294 ]; 8295 8296 /** 8297 * With "setActive" IE offers a smart way of focusing elements without scrolling them into view: 8298 * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx 8299 * 8300 * Other browsers need a more hacky way: (pssst don't tell my mama) 8301 * In order to prevent the element being scrolled into view when focusing it, we simply 8302 * move it out of the scrollable area, focus it, and reset it's position 8303 */ 8304 var focusWithoutScrolling = function(element) { 8305 if (element.setActive) { 8306 // Following line could cause a js error when the textarea is invisible 8307 // See https://github.com/xing/wysihtml5/issues/9 8308 try { element.setActive(); } catch(e) {} 8309 } else { 8310 var elementStyle = element.style, 8311 originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop, 8312 originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft, 8313 originalStyles = { 8314 position: elementStyle.position, 8315 top: elementStyle.top, 8316 left: elementStyle.left, 8317 WebkitUserSelect: elementStyle.WebkitUserSelect 8318 }; 8319 8320 dom.setStyles({ 8321 position: "absolute", 8322 top: "-99999px", 8323 left: "-99999px", 8324 // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother 8325 WebkitUserSelect: "none" 8326 }).on(element); 8327 8328 element.focus(); 8329 8330 dom.setStyles(originalStyles).on(element); 8331 8332 if (win.scrollTo) { 8333 // Some browser extensions unset this method to prevent annoyances 8334 // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100 8335 // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1 8336 win.scrollTo(originalScrollLeft, originalScrollTop); 8337 } 8338 } 8339 }; 8340 8341 8342 wysihtml5.views.Composer.prototype.style = function() { 8343 var that = this, 8344 originalActiveElement = doc.querySelector(":focus"), 8345 textareaElement = this.textarea.element, 8346 hasPlaceholder = textareaElement.hasAttribute("placeholder"), 8347 originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"); 8348 this.focusStylesHost = this.focusStylesHost || HOST_TEMPLATE.cloneNode(false); 8349 this.blurStylesHost = this.blurStylesHost || HOST_TEMPLATE.cloneNode(false); 8350 8351 // Remove placeholder before copying (as the placeholder has an affect on the computed style) 8352 if (hasPlaceholder) { 8353 textareaElement.removeAttribute("placeholder"); 8354 } 8355 8356 if (textareaElement === originalActiveElement) { 8357 textareaElement.blur(); 8358 } 8359 8360 // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) --------- 8361 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe).andTo(this.blurStylesHost); 8362 8363 // --------- editor styles --------- 8364 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost); 8365 8366 // --------- apply standard rules --------- 8367 dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument); 8368 8369 // --------- :focus styles --------- 8370 focusWithoutScrolling(textareaElement); 8371 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost); 8372 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost); 8373 8374 // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus 8375 // this is needed for when the change_view event is fired where the iframe is hidden and then 8376 // the blur event fires and re-displays it 8377 var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]); 8378 8379 // --------- restore focus --------- 8380 if (originalActiveElement) { 8381 originalActiveElement.focus(); 8382 } else { 8383 textareaElement.blur(); 8384 } 8385 8386 // --------- restore placeholder --------- 8387 if (hasPlaceholder) { 8388 textareaElement.setAttribute("placeholder", originalPlaceholder); 8389 } 8390 8391 // When copying styles, we only get the computed style which is never returned in percent unit 8392 // Therefore we've to recalculate style onresize 8393 if (!wysihtml5.browser.hasCurrentStyleProperty()) { 8394 var winObserver = dom.observe(win, "resize", function() { 8395 // Remove event listener if composer doesn't exist anymore 8396 if (!dom.contains(document.documentElement, that.iframe)) { 8397 winObserver.stop(); 8398 return; 8399 } 8400 var originalTextareaDisplayStyle = dom.getStyle("display").from(textareaElement), 8401 originalComposerDisplayStyle = dom.getStyle("display").from(that.iframe); 8402 textareaElement.style.display = ""; 8403 that.iframe.style.display = "none"; 8404 dom.copyStyles(RESIZE_STYLE) 8405 .from(textareaElement) 8406 .to(that.iframe) 8407 .andTo(that.focusStylesHost) 8408 .andTo(that.blurStylesHost); 8409 that.iframe.style.display = originalComposerDisplayStyle; 8410 textareaElement.style.display = originalTextareaDisplayStyle; 8411 }); 8412 } 8413 8414 // --------- Sync focus/blur styles --------- 8415 this.parent.observe("focus:composer", function() { 8416 dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.iframe); 8417 dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element); 8418 }); 8419 8420 this.parent.observe("blur:composer", function() { 8421 dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe); 8422 dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element); 8423 }); 8424 8425 return this; 8426 }; 8427 })(wysihtml5);/** 8428 * Taking care of events 8429 * - Simulating 'change' event on contentEditable element 8430 * - Handling drag & drop logic 8431 * - Catch paste events 8432 * - Dispatch proprietary newword:composer event 8433 * - Keyboard shortcuts 8434 */ 8435 (function(wysihtml5) { 8436 var dom = wysihtml5.dom, 8437 browser = wysihtml5.browser, 8438 /** 8439 * Map keyCodes to query commands 8440 */ 8441 shortcuts = { 8442 "66": "bold", // B 8443 "73": "italic", // I 8444 "85": "underline" // U 8445 }; 8446 8447 wysihtml5.views.Composer.prototype.observe = function() { 8448 var that = this, 8449 state = this.getValue(), 8450 iframe = this.sandbox.getIframe(), 8451 element = this.element, 8452 focusBlurElement = browser.supportsEventsInIframeCorrectly() ? element : this.sandbox.getWindow(), 8453 // Firefox < 3.5 doesn't support the drop event, instead it supports a so called "dragdrop" event which behaves almost the same 8454 pasteEvents = browser.supportsEvent("drop") ? ["drop", "paste"] : ["dragdrop", "paste"]; 8455 8456 // --------- destroy:composer event --------- 8457 dom.observe(iframe, "DOMNodeRemoved", function() { 8458 clearInterval(domNodeRemovedInterval); 8459 that.parent.fire("destroy:composer"); 8460 }); 8461 8462 // DOMNodeRemoved event is not supported in IE 8 8463 var domNodeRemovedInterval = setInterval(function() { 8464 if (!dom.contains(document.documentElement, iframe)) { 8465 clearInterval(domNodeRemovedInterval); 8466 that.parent.fire("destroy:composer"); 8467 } 8468 }, 250); 8469 8470 8471 // --------- Focus & blur logic --------- 8472 dom.observe(focusBlurElement, "focus", function() { 8473 that.parent.fire("focus").fire("focus:composer"); 8474 8475 // Delay storing of state until all focus handler are fired 8476 // especially the one which resets the placeholder 8477 setTimeout(function() { state = that.getValue(); }, 0); 8478 }); 8479 8480 dom.observe(focusBlurElement, "blur", function() { 8481 if (state !== that.getValue()) { 8482 that.parent.fire("change").fire("change:composer"); 8483 } 8484 that.parent.fire("blur").fire("blur:composer"); 8485 }); 8486 8487 if (wysihtml5.browser.isIos()) { 8488 // When on iPad/iPhone/IPod after clicking outside of editor, the editor loses focus 8489 // but the UI still acts as if the editor has focus (blinking caret and onscreen keyboard visible) 8490 // We prevent that by focusing a temporary input element which immediately loses focus 8491 dom.observe(element, "blur", function() { 8492 var input = element.ownerDocument.createElement("input"), 8493 originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop, 8494 originalScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; 8495 try { 8496 that.selection.insertNode(input); 8497 } catch(e) { 8498 element.appendChild(input); 8499 } 8500 input.focus(); 8501 input.parentNode.removeChild(input); 8502 8503 window.scrollTo(originalScrollLeft, originalScrollTop); 8504 }); 8505 } 8506 8507 // --------- Drag & Drop logic --------- 8508 dom.observe(element, "dragenter", function() { 8509 that.parent.fire("unset_placeholder"); 8510 }); 8511 8512 if (browser.firesOnDropOnlyWhenOnDragOverIsCancelled()) { 8513 dom.observe(element, ["dragover", "dragenter"], function(event) { 8514 event.preventDefault(); 8515 }); 8516 } 8517 8518 dom.observe(element, pasteEvents, function(event) { 8519 var dataTransfer = event.dataTransfer, 8520 data; 8521 8522 if (dataTransfer && browser.supportsDataTransfer()) { 8523 data = dataTransfer.getData("text/html") || dataTransfer.getData("text/plain"); 8524 } 8525 if (data) { 8526 element.focus(); 8527 that.commands.exec("insertHTML", data); 8528 that.parent.fire("paste").fire("paste:composer"); 8529 event.stopPropagation(); 8530 event.preventDefault(); 8531 } else { 8532 setTimeout(function() { 8533 that.parent.fire("paste").fire("paste:composer"); 8534 }, 0); 8535 } 8536 }); 8537 8538 // --------- neword event --------- 8539 dom.observe(element, "keyup", function(event) { 8540 var keyCode = event.keyCode; 8541 if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) { 8542 that.parent.fire("newword:composer"); 8543 } 8544 }); 8545 8546 this.parent.observe("paste:composer", function() { 8547 setTimeout(function() { that.parent.fire("newword:composer"); }, 0); 8548 }); 8549 8550 // --------- Make sure that images are selected when clicking on them --------- 8551 if (!browser.canSelectImagesInContentEditable()) { 8552 dom.observe(element, "mousedown", function(event) { 8553 var target = event.target; 8554 if (target.nodeName === "IMG") { 8555 that.selection.selectNode(target); 8556 event.preventDefault(); 8557 } 8558 }); 8559 } 8560 8561 // --------- Shortcut logic --------- 8562 dom.observe(element, "keydown", function(event) { 8563 var keyCode = event.keyCode, 8564 command = shortcuts[keyCode]; 8565 if ((event.ctrlKey || event.metaKey) && !event.altKey && command) { 8566 that.commands.exec(command); 8567 event.preventDefault(); 8568 } 8569 }); 8570 8571 // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor --------- 8572 dom.observe(element, "keydown", function(event) { 8573 var target = that.selection.getSelectedNode(true), 8574 keyCode = event.keyCode, 8575 parent; 8576 if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete 8577 parent = target.parentNode; 8578 // delete the <img> 8579 parent.removeChild(target); 8580 // and it's parent <a> too if it hasn't got any other child nodes 8581 if (parent.nodeName === "A" && !parent.firstChild) { 8582 parent.parentNode.removeChild(parent); 8583 } 8584 8585 setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0); 8586 event.preventDefault(); 8587 } 8588 }); 8589 8590 // --------- Show url in tooltip when hovering links or images --------- 8591 var titlePrefixes = { 8592 IMG: "Image: ", 8593 A: "Link: " 8594 }; 8595 8596 dom.observe(element, "mouseover", function(event) { 8597 var target = event.target, 8598 nodeName = target.nodeName, 8599 title; 8600 if (nodeName !== "A" && nodeName !== "IMG") { 8601 return; 8602 } 8603 var hasTitle = target.hasAttribute("title"); 8604 if(!hasTitle){ 8605 title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src")); 8606 target.setAttribute("title", title); 8607 } 8608 }); 8609 }; 8610 })(wysihtml5);/** 8611 * Class that takes care that the value of the composer and the textarea is always in sync 8612 */ 8613 (function(wysihtml5) { 8614 var INTERVAL = 400; 8615 8616 wysihtml5.views.Synchronizer = Base.extend( 8617 /** @scope wysihtml5.views.Synchronizer.prototype */ { 8618 8619 constructor: function(editor, textarea, composer) { 8620 this.editor = editor; 8621 this.textarea = textarea; 8622 this.composer = composer; 8623 8624 this._observe(); 8625 }, 8626 8627 /** 8628 * Sync html from composer to textarea 8629 * Takes care of placeholders 8630 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea 8631 */ 8632 fromComposerToTextarea: function(shouldParseHtml) { 8633 this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue()).trim(), shouldParseHtml); 8634 }, 8635 8636 8637 /** 8638 * Sync value of textarea to composer 8639 * Takes care of placeholders 8640 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer 8641 */ 8642 fromTextareaToComposer: function(shouldParseHtml) { 8643 var textareaValue = this.textarea.getValue(); 8644 if (textareaValue) { 8645 this.composer.setValue(textareaValue, shouldParseHtml); 8646 } else { 8647 this.composer.clear(); 8648 this.editor.fire("set_placeholder"); 8649 } 8650 }, 8651 8652 /** 8653 * Invoke syncing based on view state 8654 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea 8655 */ 8656 sync: function(shouldParseHtml) { 8657 if (this.editor.currentView.name === "textarea") { 8658 this.fromTextareaToComposer(shouldParseHtml); 8659 } else { 8660 this.fromComposerToTextarea(shouldParseHtml); 8661 } 8662 }, 8663 8664 /** 8665 * Initializes interval-based syncing 8666 * also makes sure that on-submit the composer's content is synced with the textarea 8667 * immediately when the form gets submitted 8668 */ 8669 _observe: function() { 8670 var interval, 8671 that = this, 8672 form = this.textarea.element.form, 8673 startInterval = function() { 8674 interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL); 8675 }, 8676 stopInterval = function() { 8677 clearInterval(interval); 8678 interval = null; 8679 }; 8680 8681 startInterval(); 8682 8683 if (form) { 8684 // If the textarea is in a form make sure that after onreset and onsubmit the composer 8685 // has the correct state 8686 wysihtml5.dom.observe(form, "submit", function() { 8687 that.sync(true); 8688 }); 8689 wysihtml5.dom.observe(form, "reset", function() { 8690 setTimeout(function() { that.fromTextareaToComposer(); }, 0); 8691 }); 8692 } 8693 8694 this.editor.observe("change_view", function(view) { 8695 if (view === "composer" && !interval) { 8696 that.fromTextareaToComposer(true); 8697 startInterval(); 8698 } else if (view === "textarea") { 8699 that.fromComposerToTextarea(true); 8700 stopInterval(); 8701 } 8702 }); 8703 8704 this.editor.observe("destroy:composer", stopInterval); 8705 } 8706 }); 8707 })(wysihtml5); 8708 wysihtml5.views.Textarea = wysihtml5.views.View.extend( 8709 /** @scope wysihtml5.views.Textarea.prototype */ { 8710 name: "textarea", 8711 8712 constructor: function(parent, textareaElement, config) { 8713 this.base(parent, textareaElement, config); 8714 8715 this._observe(); 8716 }, 8717 8718 clear: function() { 8719 this.element.value = ""; 8720 }, 8721 8722 getValue: function(parse) { 8723 var value = this.isEmpty() ? "" : this.element.value; 8724 if (parse) { 8725 value = this.parent.parse(value); 8726 } 8727 return value; 8728 }, 8729 8730 setValue: function(html, parse) { 8731 if (parse) { 8732 html = this.parent.parse(html); 8733 } 8734 this.element.value = html; 8735 }, 8736 8737 hasPlaceholderSet: function() { 8738 var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element), 8739 placeholderText = this.element.getAttribute("placeholder") || null, 8740 value = this.element.value, 8741 isEmpty = !value; 8742 return (supportsPlaceholder && isEmpty) || (value === placeholderText); 8743 }, 8744 8745 isEmpty: function() { 8746 return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet(); 8747 }, 8748 8749 _observe: function() { 8750 var element = this.element, 8751 parent = this.parent, 8752 eventMapping = { 8753 focusin: "focus", 8754 focusout: "blur" 8755 }, 8756 /** 8757 * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events 8758 * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai 8759 */ 8760 events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"]; 8761 8762 parent.observe("beforeload", function() { 8763 wysihtml5.dom.observe(element, events, function(event) { 8764 var eventName = eventMapping[event.type] || event.type; 8765 parent.fire(eventName).fire(eventName + ":textarea"); 8766 }); 8767 8768 wysihtml5.dom.observe(element, ["paste", "drop"], function() { 8769 setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0); 8770 }); 8771 }); 8772 } 8773 });/** 8774 * Toolbar Dialog 8775 * 8776 * @param {Element} link The toolbar link which causes the dialog to show up 8777 * @param {Element} container The dialog container 8778 * 8779 * @example 8780 * <!-- Toolbar link --> 8781 * <a data-wysihtml5-command="insertImage">insert an image</a> 8782 * 8783 * <!-- Dialog --> 8784 * <div data-wysihtml5-dialog="insertImage" style="display: none;"> 8785 * <label> 8786 * URL: <input data-wysihtml5-dialog-field="src" value="http://"> 8787 * </label> 8788 * <label> 8789 * Alternative text: <input data-wysihtml5-dialog-field="alt" value=""> 8790 * </label> 8791 * </div> 8792 * 8793 * <script> 8794 * var dialog = new wysihtml5.toolbar.Dialog( 8795 * document.querySelector("[data-wysihtml5-command='insertImage']"), 8796 * document.querySelector("[data-wysihtml5-dialog='insertImage']") 8797 * ); 8798 * dialog.observe("save", function(attributes) { 8799 * // do something 8800 * }); 8801 * </script> 8802 */ 8803 (function(wysihtml5) { 8804 var dom = wysihtml5.dom, 8805 CLASS_NAME_OPENED = "wysihtml5-command-dialog-opened", 8806 SELECTOR_FORM_ELEMENTS = "input, select, textarea", 8807 SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]", 8808 ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field"; 8809 8810 8811 wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend( 8812 /** @scope wysihtml5.toolbar.Dialog.prototype */ { 8813 constructor: function(link, container) { 8814 this.link = link; 8815 this.container = container; 8816 }, 8817 8818 _observe: function() { 8819 if (this._observed) { 8820 return; 8821 } 8822 8823 var that = this, 8824 callbackWrapper = function(event) { 8825 var attributes = that._serialize(); 8826 if (attributes == that.elementToChange) { 8827 that.fire("edit", attributes); 8828 } else { 8829 that.fire("save", attributes); 8830 } 8831 that.hide(); 8832 event.preventDefault(); 8833 event.stopPropagation(); 8834 }; 8835 8836 dom.observe(that.link, "click", function(event) { 8837 if (dom.hasClass(that.link, CLASS_NAME_OPENED)) { 8838 setTimeout(function() { that.hide(); }, 0); 8839 } 8840 }); 8841 8842 dom.observe(this.container, "keydown", function(event) { 8843 var keyCode = event.keyCode; 8844 if (keyCode === wysihtml5.ENTER_KEY) { 8845 callbackWrapper(event); 8846 } 8847 if (keyCode === wysihtml5.ESCAPE_KEY) { 8848 that.hide(); 8849 } 8850 }); 8851 8852 dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper); 8853 8854 dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) { 8855 that.fire("cancel"); 8856 that.hide(); 8857 event.preventDefault(); 8858 event.stopPropagation(); 8859 }); 8860 8861 var formElements = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS), 8862 i = 0, 8863 length = formElements.length, 8864 _clearInterval = function() { clearInterval(that.interval); }; 8865 for (; i<length; i++) { 8866 dom.observe(formElements[i], "change", _clearInterval); 8867 } 8868 8869 this._observed = true; 8870 }, 8871 8872 /** 8873 * Grabs all fields in the dialog and puts them in key=>value style in an object which 8874 * then gets returned 8875 */ 8876 _serialize: function() { 8877 var data = this.elementToChange || {}, 8878 fields = this.container.querySelectorAll(SELECTOR_FIELDS), 8879 length = fields.length, 8880 i = 0; 8881 for (; i<length; i++) { 8882 data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value; 8883 } 8884 return data; 8885 }, 8886 8887 /** 8888 * Takes the attributes of the "elementToChange" 8889 * and inserts them in their corresponding dialog input fields 8890 * 8891 * Assume the "elementToChange" looks like this: 8892 * <a href="http://www.google.com" target="_blank">foo</a> 8893 * 8894 * and we have the following dialog: 8895 * <input type="text" data-wysihtml5-dialog-field="href" value=""> 8896 * <input type="text" data-wysihtml5-dialog-field="target" value=""> 8897 * 8898 * after calling _interpolate() the dialog will look like this 8899 * <input type="text" data-wysihtml5-dialog-field="href" value="http://www.google.com"> 8900 * <input type="text" data-wysihtml5-dialog-field="target" value="_blank"> 8901 * 8902 * Basically it adopted the attribute values into the corresponding input fields 8903 * 8904 */ 8905 _interpolate: function(avoidHiddenFields) { 8906 var field, 8907 fieldName, 8908 newValue, 8909 focusedElement = document.querySelector(":focus"), 8910 fields = this.container.querySelectorAll(SELECTOR_FIELDS), 8911 length = fields.length, 8912 i = 0; 8913 for (; i<length; i++) { 8914 field = fields[i]; 8915 8916 // Never change elements where the user is currently typing in 8917 if (field === focusedElement) { 8918 continue; 8919 } 8920 8921 // Don't update hidden fields 8922 // See https://github.com/xing/wysihtml5/pull/14 8923 if (avoidHiddenFields && field.type === "hidden") { 8924 continue; 8925 } 8926 8927 fieldName = field.getAttribute(ATTRIBUTE_FIELDS); 8928 newValue = this.elementToChange ? (this.elementToChange[fieldName] || "") : field.defaultValue; 8929 field.value = newValue; 8930 } 8931 }, 8932 8933 /** 8934 * Show the dialog element 8935 */ 8936 show: function(elementToChange) { 8937 var that = this, 8938 firstField = this.container.querySelector(SELECTOR_FORM_ELEMENTS); 8939 this.elementToChange = elementToChange; 8940 this._observe(); 8941 this._interpolate(); 8942 if (elementToChange) { 8943 this.interval = setInterval(function() { that._interpolate(true); }, 500); 8944 } 8945 dom.addClass(this.link, CLASS_NAME_OPENED); 8946 this.container.style.display = ""; 8947 this.fire("show"); 8948 if (firstField && !elementToChange) { 8949 try { 8950 firstField.focus(); 8951 } catch(e) {} 8952 } 8953 }, 8954 8955 /** 8956 * Hide the dialog element 8957 */ 8958 hide: function() { 8959 clearInterval(this.interval); 8960 this.elementToChange = null; 8961 dom.removeClass(this.link, CLASS_NAME_OPENED); 8962 this.container.style.display = "none"; 8963 this.fire("hide"); 8964 } 8965 }); 8966 })(wysihtml5); 8967 /** 8968 * Converts speech-to-text and inserts this into the editor 8969 * As of now (2011/03/25) this only is supported in Chrome >= 11 8970 * 8971 * Note that it sends the recorded audio to the google speech recognition api: 8972 * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec 8973 * 8974 * Current HTML5 draft can be found here 8975 * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html 8976 * 8977 * "Accessing Google Speech API Chrome 11" 8978 * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/ 8979 */ 8980 (function(wysihtml5) { 8981 var dom = wysihtml5.dom; 8982 8983 var linkStyles = { 8984 position: "relative" 8985 }; 8986 8987 var wrapperStyles = { 8988 left: 0, 8989 margin: 0, 8990 opacity: 0, 8991 overflow: "hidden", 8992 padding: 0, 8993 position: "absolute", 8994 top: 0, 8995 zIndex: 1 8996 }; 8997 8998 var inputStyles = { 8999 cursor: "inherit", 9000 fontSize: "50px", 9001 height: "50px", 9002 marginTop: "-25px", 9003 outline: 0, 9004 padding: 0, 9005 position: "absolute", 9006 right: "-4px", 9007 top: "50%" 9008 }; 9009 9010 var inputAttributes = { 9011 "x-webkit-speech": "", 9012 "speech": "" 9013 }; 9014 9015 wysihtml5.toolbar.Speech = function(parent, link) { 9016 var input = document.createElement("input"); 9017 if (!wysihtml5.browser.supportsSpeechApiOn(input)) { 9018 link.style.display = "none"; 9019 return; 9020 } 9021 9022 var wrapper = document.createElement("div"); 9023 9024 wysihtml5.lang.object(wrapperStyles).merge({ 9025 width: link.offsetWidth + "px", 9026 height: link.offsetHeight + "px" 9027 }); 9028 9029 dom.insert(input).into(wrapper); 9030 dom.insert(wrapper).into(link); 9031 9032 dom.setStyles(inputStyles).on(input); 9033 dom.setAttributes(inputAttributes).on(input) 9034 9035 dom.setStyles(wrapperStyles).on(wrapper); 9036 dom.setStyles(linkStyles).on(link); 9037 9038 var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange"; 9039 dom.observe(input, eventName, function() { 9040 parent.execCommand("insertText", input.value); 9041 input.value = ""; 9042 }); 9043 9044 dom.observe(input, "click", function(event) { 9045 if (dom.hasClass(link, "wysihtml5-command-disabled")) { 9046 event.preventDefault(); 9047 } 9048 9049 event.stopPropagation(); 9050 }); 9051 }; 9052 })(wysihtml5);/** 9053 * Toolbar 9054 * 9055 * @param {Object} parent Reference to instance of Editor instance 9056 * @param {Element} container Reference to the toolbar container element 9057 * 9058 * @example 9059 * <div id="toolbar"> 9060 * <a data-wysihtml5-command="createLink">insert link</a> 9061 * <a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">insert h1</a> 9062 * </div> 9063 * 9064 * <script> 9065 * var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar")); 9066 * </script> 9067 */ 9068 (function(wysihtml5) { 9069 var CLASS_NAME_COMMAND_DISABLED = "wysihtml5-command-disabled", 9070 CLASS_NAME_COMMANDS_DISABLED = "wysihtml5-commands-disabled", 9071 CLASS_NAME_COMMAND_ACTIVE = "wysihtml5-command-active", 9072 CLASS_NAME_ACTION_ACTIVE = "wysihtml5-action-active", 9073 dom = wysihtml5.dom; 9074 9075 wysihtml5.toolbar.Toolbar = Base.extend( 9076 /** @scope wysihtml5.toolbar.Toolbar.prototype */ { 9077 constructor: function(editor, container) { 9078 this.editor = editor; 9079 this.container = typeof(container) === "string" ? document.getElementById(container) : container; 9080 this.composer = editor.composer; 9081 9082 this._getLinks("command"); 9083 this._getLinks("action"); 9084 9085 this._observe(); 9086 this.show(); 9087 9088 var speechInputLinks = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"), 9089 length = speechInputLinks.length, 9090 i = 0; 9091 for (; i<length; i++) { 9092 new wysihtml5.toolbar.Speech(this, speechInputLinks[i]); 9093 } 9094 }, 9095 9096 _getLinks: function(type) { 9097 var links = this[type + "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("[data-wysihtml5-" + type + "]")).get(), 9098 length = links.length, 9099 i = 0, 9100 mapping = this[type + "Mapping"] = {}, 9101 link, 9102 group, 9103 name, 9104 value, 9105 dialog; 9106 for (; i<length; i++) { 9107 link = links[i]; 9108 name = link.getAttribute("data-wysihtml5-" + type); 9109 value = link.getAttribute("data-wysihtml5-" + type + "-value"); 9110 group = this.container.querySelector("[data-wysihtml5-" + type + "-group='" + name + "']"); 9111 dialog = this._getDialog(link, name); 9112 9113 mapping[name + ":" + value] = { 9114 link: link, 9115 group: group, 9116 name: name, 9117 value: value, 9118 dialog: dialog, 9119 state: false 9120 }; 9121 } 9122 }, 9123 9124 _getDialog: function(link, command) { 9125 var that = this, 9126 dialogElement = this.container.querySelector("[data-wysihtml5-dialog='" + command + "']"), 9127 dialog, 9128 caretBookmark; 9129 9130 if (dialogElement) { 9131 dialog = new wysihtml5.toolbar.Dialog(link, dialogElement); 9132 9133 dialog.observe("show", function() { 9134 caretBookmark = that.composer.selection.getBookmark(); 9135 9136 that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); 9137 }); 9138 9139 dialog.observe("save", function(attributes) { 9140 if (caretBookmark) { 9141 that.composer.selection.setBookmark(caretBookmark); 9142 } 9143 that._execCommand(command, attributes); 9144 9145 that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); 9146 }); 9147 9148 dialog.observe("cancel", function() { 9149 that.editor.focus(false); 9150 that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); 9151 }); 9152 } 9153 return dialog; 9154 }, 9155 9156 /** 9157 * @example 9158 * var toolbar = new wysihtml5.Toolbar(); 9159 * // Insert a <blockquote> element or wrap current selection in <blockquote> 9160 * toolbar.execCommand("formatBlock", "blockquote"); 9161 */ 9162 execCommand: function(command, commandValue) { 9163 if (this.commandsDisabled) { 9164 return; 9165 } 9166 9167 var commandObj = this.commandMapping[command + ":" + commandValue]; 9168 9169 // Show dialog when available 9170 if (commandObj && commandObj.dialog && !commandObj.state) { 9171 commandObj.dialog.show(); 9172 } else { 9173 this._execCommand(command, commandValue); 9174 } 9175 }, 9176 9177 _execCommand: function(command, commandValue) { 9178 // Make sure that composer is focussed (false => don't move caret to the end) 9179 this.editor.focus(false); 9180 9181 this.composer.commands.exec(command, commandValue); 9182 this._updateLinkStates(); 9183 }, 9184 9185 execAction: function(action) { 9186 var editor = this.editor; 9187 switch(action) { 9188 case "change_view": 9189 if (editor.currentView === editor.textarea) { 9190 editor.fire("change_view", "composer"); 9191 } else { 9192 editor.fire("change_view", "textarea"); 9193 } 9194 break; 9195 } 9196 }, 9197 9198 _observe: function() { 9199 var that = this, 9200 editor = this.editor, 9201 container = this.container, 9202 links = this.commandLinks.concat(this.actionLinks), 9203 length = links.length, 9204 i = 0; 9205 9206 for (; i<length; i++) { 9207 // 'javascript:;' and unselectable=on Needed for IE, but done in all browsers to make sure that all get the same css applied 9208 // (you know, a:link { ... } doesn't match anchors with missing href attribute) 9209 dom.setAttributes({ 9210 href: "javascript:;", 9211 unselectable: "on" 9212 }).on(links[i]); 9213 } 9214 9215 // Needed for opera 9216 dom.delegate(container, "[data-wysihtml5-command]", "mousedown", function(event) { event.preventDefault(); }); 9217 9218 dom.delegate(container, "[data-wysihtml5-command]", "click", function(event) { 9219 var link = this, 9220 command = link.getAttribute("data-wysihtml5-command"), 9221 commandValue = link.getAttribute("data-wysihtml5-command-value"); 9222 that.execCommand(command, commandValue); 9223 event.preventDefault(); 9224 }); 9225 9226 dom.delegate(container, "[data-wysihtml5-action]", "click", function(event) { 9227 var action = this.getAttribute("data-wysihtml5-action"); 9228 that.execAction(action); 9229 event.preventDefault(); 9230 }); 9231 9232 editor.observe("focus:composer", function() { 9233 that.bookmark = null; 9234 clearInterval(that.interval); 9235 that.interval = setInterval(function() { that._updateLinkStates(); }, 500); 9236 }); 9237 9238 editor.observe("blur:composer", function() { 9239 clearInterval(that.interval); 9240 }); 9241 9242 editor.observe("destroy:composer", function() { 9243 clearInterval(that.interval); 9244 }); 9245 9246 editor.observe("change_view", function(currentView) { 9247 // Set timeout needed in order to let the blur event fire first 9248 setTimeout(function() { 9249 that.commandsDisabled = (currentView !== "composer"); 9250 that._updateLinkStates(); 9251 if (that.commandsDisabled) { 9252 dom.addClass(container, CLASS_NAME_COMMANDS_DISABLED); 9253 } else { 9254 dom.removeClass(container, CLASS_NAME_COMMANDS_DISABLED); 9255 } 9256 }, 0); 9257 }); 9258 }, 9259 9260 _updateLinkStates: function() { 9261 var element = this.composer.element, 9262 commandMapping = this.commandMapping, 9263 actionMapping = this.actionMapping, 9264 i, 9265 state, 9266 action, 9267 command; 9268 // every millisecond counts... this is executed quite often 9269 for (i in commandMapping) { 9270 command = commandMapping[i]; 9271 if (this.commandsDisabled) { 9272 state = false; 9273 dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE); 9274 if (command.group) { 9275 dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE); 9276 } 9277 if (command.dialog) { 9278 command.dialog.hide(); 9279 } 9280 } else { 9281 state = this.composer.commands.state(command.name, command.value); 9282 if (wysihtml5.lang.object(state).isArray()) { 9283 // Grab first and only object/element in state array, otherwise convert state into boolean 9284 // to avoid showing a dialog for multiple selected elements which may have different attributes 9285 // eg. when two links with different href are selected, the state will be an array consisting of both link elements 9286 // but the dialog interface can only update one 9287 state = state.length === 1 ? state[0] : true; 9288 } 9289 dom.removeClass(command.link, CLASS_NAME_COMMAND_DISABLED); 9290 if (command.group) { 9291 dom.removeClass(command.group, CLASS_NAME_COMMAND_DISABLED); 9292 } 9293 } 9294 9295 if (command.state === state) { 9296 continue; 9297 } 9298 9299 command.state = state; 9300 if (state) { 9301 dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE); 9302 if (command.group) { 9303 dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE); 9304 } 9305 if (command.dialog) { 9306 if (typeof(state) === "object") { 9307 command.dialog.show(state); 9308 } else { 9309 command.dialog.hide(); 9310 } 9311 } 9312 } else { 9313 dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE); 9314 if (command.group) { 9315 dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE); 9316 } 9317 if (command.dialog) { 9318 command.dialog.hide(); 9319 } 9320 } 9321 } 9322 9323 for (i in actionMapping) { 9324 action = actionMapping[i]; 9325 9326 if (action.name === "change_view") { 9327 action.state = this.editor.currentView === this.editor.textarea; 9328 if (action.state) { 9329 dom.addClass(action.link, CLASS_NAME_ACTION_ACTIVE); 9330 } else { 9331 dom.removeClass(action.link, CLASS_NAME_ACTION_ACTIVE); 9332 } 9333 } 9334 } 9335 }, 9336 9337 show: function() { 9338 this.container.style.display = ""; 9339 }, 9340 9341 hide: function() { 9342 this.container.style.display = "none"; 9343 } 9344 }); 9345 9346 })(wysihtml5); 9347 /** 9348 * WYSIHTML5 Editor 9349 * 9350 * @param {Element} textareaElement Reference to the textarea which should be turned into a rich text interface 9351 * @param {Object} [config] See defaultConfig object below for explanation of each individual config option 9352 * 9353 * @events 9354 * load 9355 * beforeload (for internal use only) 9356 * focus 9357 * focus:composer 9358 * focus:textarea 9359 * blur 9360 * blur:composer 9361 * blur:textarea 9362 * change 9363 * change:composer 9364 * change:textarea 9365 * paste 9366 * paste:composer 9367 * paste:textarea 9368 * newword:composer 9369 * destroy:composer 9370 * undo:composer 9371 * redo:composer 9372 * beforecommand:composer 9373 * aftercommand:composer 9374 * change_view 9375 */ 9376 (function(wysihtml5) { 9377 var undef; 9378 9379 var defaultConfig = { 9380 // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body 9381 name: undef, 9382 // Whether the editor should look like the textarea (by adopting styles) 9383 style: true, 9384 // Id of the toolbar element, pass falsey value if you don't want any toolbar logic 9385 toolbar: undef, 9386 // Whether urls, entered by the user should automatically become clickable-links 9387 autoLink: true, 9388 // Object which includes parser rules to apply when html gets inserted via copy & paste 9389 // See parser_rules/*.js for examples 9390 parserRules: { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} }, 9391 // Parser method to use when the user inserts content via copy & paste 9392 parser: wysihtml5.dom.parse, 9393 // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option 9394 composerClassName: "wysihtml5-editor", 9395 // Class name to add to the body when the wysihtml5 editor is supported 9396 bodyClassName: "wysihtml5-supported", 9397 // Array (or single string) of stylesheet urls to be loaded in the editor's iframe 9398 stylesheets: [], 9399 // Placeholder text to use, defaults to the placeholder attribute on the textarea element 9400 placeholderText: undef, 9401 // Whether the composer should allow the user to manually resize images, tables etc. 9402 allowObjectResizing: true, 9403 // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5) 9404 supportTouchDevices: true 9405 }; 9406 9407 wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend( 9408 /** @scope wysihtml5.Editor.prototype */ { 9409 constructor: function(textareaElement, config) { 9410 this.textareaElement = typeof(textareaElement) === "string" ? document.getElementById(textareaElement) : textareaElement; 9411 this.config = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get(); 9412 this.textarea = new wysihtml5.views.Textarea(this, this.textareaElement, this.config); 9413 this.currentView = this.textarea; 9414 this._isCompatible = wysihtml5.browser.supported(); 9415 9416 // Sort out unsupported/unwanted browsers here 9417 if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) { 9418 var that = this; 9419 setTimeout(function() { that.fire("beforeload").fire("load"); }, 0); 9420 return; 9421 } 9422 9423 // Add class name to body, to indicate that the editor is supported 9424 wysihtml5.dom.addClass(document.body, this.config.bodyClassName); 9425 9426 this.composer = new wysihtml5.views.Composer(this, this.textareaElement, this.config); 9427 this.currentView = this.composer; 9428 9429 if (typeof(this.config.parser) === "function") { 9430 this._initParser(); 9431 } 9432 9433 this.observe("beforeload", function() { 9434 this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer); 9435 if (this.config.toolbar) { 9436 this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar); 9437 } 9438 }); 9439 9440 try { 9441 console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5"); 9442 } catch(e) {} 9443 }, 9444 9445 isCompatible: function() { 9446 return this._isCompatible; 9447 }, 9448 9449 clear: function() { 9450 this.currentView.clear(); 9451 return this; 9452 }, 9453 9454 getValue: function(parse) { 9455 return this.currentView.getValue(parse); 9456 }, 9457 9458 setValue: function(html, parse) { 9459 if (!html) { 9460 return this.clear(); 9461 } 9462 this.currentView.setValue(html, parse); 9463 return this; 9464 }, 9465 9466 focus: function(setToEnd) { 9467 this.currentView.focus(setToEnd); 9468 return this; 9469 }, 9470 9471 /** 9472 * Deactivate editor (make it readonly) 9473 */ 9474 disable: function() { 9475 this.currentView.disable(); 9476 return this; 9477 }, 9478 9479 /** 9480 * Activate editor 9481 */ 9482 enable: function() { 9483 this.currentView.enable(); 9484 return this; 9485 }, 9486 9487 isEmpty: function() { 9488 return this.currentView.isEmpty(); 9489 }, 9490 9491 hasPlaceholderSet: function() { 9492 return this.currentView.hasPlaceholderSet(); 9493 }, 9494 9495 parse: function(htmlOrElement) { 9496 var returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), true); 9497 if (typeof(htmlOrElement) === "object") { 9498 wysihtml5.quirks.redraw(htmlOrElement); 9499 } 9500 return returnValue; 9501 }, 9502 9503 /** 9504 * Prepare html parser logic 9505 * - Observes for paste and drop 9506 */ 9507 _initParser: function() { 9508 this.observe("paste:composer", function() { 9509 var keepScrollPosition = true, 9510 that = this; 9511 that.composer.selection.executeAndRestore(function() { 9512 wysihtml5.quirks.cleanPastedHTML(that.composer.element); 9513 that.parse(that.composer.element); 9514 }, keepScrollPosition); 9515 }); 9516 9517 this.observe("paste:textarea", function() { 9518 var value = this.textarea.getValue(), 9519 newValue; 9520 newValue = this.parse(value); 9521 this.textarea.setValue(newValue); 9522 }); 9523 } 9524 }); 9525 })(wysihtml5);