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>&nbsp;</p>" ||
  5708              innerHTML == "<p>&nbsp;</p><p>&nbsp;</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);