[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/editor/atto/yui/src/rangy/js/ -> rangy-classapplier.js (source)

   1  /**

   2   * Class Applier module for Rangy.

   3   * Adds, removes and toggles classes on Ranges and Selections

   4   *

   5   * Part of Rangy, a cross-browser JavaScript range and selection library

   6   * https://github.com/timdown/rangy

   7   *

   8   * Depends on Rangy core.

   9   *

  10   * Copyright 2015, Tim Down

  11   * Licensed under the MIT license.

  12   * Version: 1.3.0

  13   * Build date: 10 May 2015

  14   */
  15  (function(factory, root) {
  16      // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
  17      factory(root.rangy);
  18  })(function(rangy) {
  19      rangy.createModule("ClassApplier", ["WrappedSelection"], function(api, module) {
  20          var dom = api.dom;
  21          var DomPosition = dom.DomPosition;
  22          var contains = dom.arrayContains;
  23          var util = api.util;
  24          var forEach = util.forEach;
  25  
  26  
  27          var defaultTagName = "span";
  28          var createElementNSSupported = util.isHostMethod(document, "createElementNS");
  29  
  30          function each(obj, func) {
  31              for (var i in obj) {
  32                  if (obj.hasOwnProperty(i)) {
  33                      if (func(i, obj[i]) === false) {
  34                          return false;
  35                      }
  36                  }
  37              }
  38              return true;
  39          }
  40  
  41          function trim(str) {
  42              return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
  43          }
  44  
  45          function classNameContainsClass(fullClassName, className) {
  46              return !!fullClassName && new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(fullClassName);
  47          }
  48  
  49          // Inefficient, inelegant nonsense for IE's svg element, which has no classList and non-HTML className implementation
  50          function hasClass(el, className) {
  51              if (typeof el.classList == "object") {
  52                  return el.classList.contains(className);
  53              } else {
  54                  var classNameSupported = (typeof el.className == "string");
  55                  var elClass = classNameSupported ? el.className : el.getAttribute("class");
  56                  return classNameContainsClass(elClass, className);
  57              }
  58          }
  59  
  60          function addClass(el, className) {
  61              if (typeof el.classList == "object") {
  62                  el.classList.add(className);
  63              } else {
  64                  var classNameSupported = (typeof el.className == "string");
  65                  var elClass = classNameSupported ? el.className : el.getAttribute("class");
  66                  if (elClass) {
  67                      if (!classNameContainsClass(elClass, className)) {
  68                          elClass += " " + className;
  69                      }
  70                  } else {
  71                      elClass = className;
  72                  }
  73                  if (classNameSupported) {
  74                      el.className = elClass;
  75                  } else {
  76                      el.setAttribute("class", elClass);
  77                  }
  78              }
  79          }
  80  
  81          var removeClass = (function() {
  82              function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
  83                  return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
  84              }
  85  
  86              return function(el, className) {
  87                  if (typeof el.classList == "object") {
  88                      el.classList.remove(className);
  89                  } else {
  90                      var classNameSupported = (typeof el.className == "string");
  91                      var elClass = classNameSupported ? el.className : el.getAttribute("class");
  92                      elClass = elClass.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer);
  93                      if (classNameSupported) {
  94                          el.className = elClass;
  95                      } else {
  96                          el.setAttribute("class", elClass);
  97                      }
  98                  }
  99              };
 100          })();
 101  
 102          function getClass(el) {
 103              var classNameSupported = (typeof el.className == "string");
 104              return classNameSupported ? el.className : el.getAttribute("class");
 105          }
 106  
 107          function sortClassName(className) {
 108              return className && className.split(/\s+/).sort().join(" ");
 109          }
 110  
 111          function getSortedClassName(el) {
 112              return sortClassName( getClass(el) );
 113          }
 114  
 115          function haveSameClasses(el1, el2) {
 116              return getSortedClassName(el1) == getSortedClassName(el2);
 117          }
 118  
 119          function hasAllClasses(el, className) {
 120              var classes = className.split(/\s+/);
 121              for (var i = 0, len = classes.length; i < len; ++i) {
 122                  if (!hasClass(el, trim(classes[i]))) {
 123                      return false;
 124                  }
 125              }
 126              return true;
 127          }
 128  
 129          function canTextBeStyled(textNode) {
 130              var parent = textNode.parentNode;
 131              return (parent && parent.nodeType == 1 && !/^(textarea|style|script|select|iframe)$/i.test(parent.nodeName));
 132          }
 133  
 134          function movePosition(position, oldParent, oldIndex, newParent, newIndex) {
 135              var posNode = position.node, posOffset = position.offset;
 136              var newNode = posNode, newOffset = posOffset;
 137  
 138              if (posNode == newParent && posOffset > newIndex) {
 139                  ++newOffset;
 140              }
 141  
 142              if (posNode == oldParent && (posOffset == oldIndex  || posOffset == oldIndex + 1)) {
 143                  newNode = newParent;
 144                  newOffset += newIndex - oldIndex;
 145              }
 146  
 147              if (posNode == oldParent && posOffset > oldIndex + 1) {
 148                  --newOffset;
 149              }
 150  
 151              position.node = newNode;
 152              position.offset = newOffset;
 153          }
 154  
 155          function movePositionWhenRemovingNode(position, parentNode, index) {
 156              if (position.node == parentNode && position.offset > index) {
 157                  --position.offset;
 158              }
 159          }
 160  
 161          function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) {
 162              // For convenience, allow newIndex to be -1 to mean "insert at the end".
 163              if (newIndex == -1) {
 164                  newIndex = newParent.childNodes.length;
 165              }
 166  
 167              var oldParent = node.parentNode;
 168              var oldIndex = dom.getNodeIndex(node);
 169  
 170              forEach(positionsToPreserve, function(position) {
 171                  movePosition(position, oldParent, oldIndex, newParent, newIndex);
 172              });
 173  
 174              // Now actually move the node.
 175              if (newParent.childNodes.length == newIndex) {
 176                  newParent.appendChild(node);
 177              } else {
 178                  newParent.insertBefore(node, newParent.childNodes[newIndex]);
 179              }
 180          }
 181  
 182          function removePreservingPositions(node, positionsToPreserve) {
 183  
 184              var oldParent = node.parentNode;
 185              var oldIndex = dom.getNodeIndex(node);
 186  
 187              forEach(positionsToPreserve, function(position) {
 188                  movePositionWhenRemovingNode(position, oldParent, oldIndex);
 189              });
 190  
 191              dom.removeNode(node);
 192          }
 193  
 194          function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) {
 195              var child, children = [];
 196              while ( (child = node.firstChild) ) {
 197                  movePreservingPositions(child, newParent, newIndex++, positionsToPreserve);
 198                  children.push(child);
 199              }
 200              if (removeNode) {
 201                  removePreservingPositions(node, positionsToPreserve);
 202              }
 203              return children;
 204          }
 205  
 206          function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) {
 207              return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve);
 208          }
 209  
 210          function rangeSelectsAnyText(range, textNode) {
 211              var textNodeRange = range.cloneRange();
 212              textNodeRange.selectNodeContents(textNode);
 213  
 214              var intersectionRange = textNodeRange.intersection(range);
 215              var text = intersectionRange ? intersectionRange.toString() : "";
 216  
 217              return text != "";
 218          }
 219  
 220          function getEffectiveTextNodes(range) {
 221              var nodes = range.getNodes([3]);
 222  
 223              // Optimization as per issue 145
 224  
 225              // Remove non-intersecting text nodes from the start of the range
 226              var start = 0, node;
 227              while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) {
 228                  ++start;
 229              }
 230  
 231              // Remove non-intersecting text nodes from the start of the range
 232              var end = nodes.length - 1;
 233              while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) {
 234                  --end;
 235              }
 236  
 237              return nodes.slice(start, end + 1);
 238          }
 239  
 240          function elementsHaveSameNonClassAttributes(el1, el2) {
 241              if (el1.attributes.length != el2.attributes.length) return false;
 242              for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
 243                  attr1 = el1.attributes[i];
 244                  name = attr1.name;
 245                  if (name != "class") {
 246                      attr2 = el2.attributes.getNamedItem(name);
 247                      if ( (attr1 === null) != (attr2 === null) ) return false;
 248                      if (attr1.specified != attr2.specified) return false;
 249                      if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
 250                  }
 251              }
 252              return true;
 253          }
 254  
 255          function elementHasNonClassAttributes(el, exceptions) {
 256              for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
 257                  attrName = el.attributes[i].name;
 258                  if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
 259                      return true;
 260                  }
 261              }
 262              return false;
 263          }
 264  
 265          var getComputedStyleProperty = dom.getComputedStyleProperty;
 266          var isEditableElement = (function() {
 267              var testEl = document.createElement("div");
 268              return typeof testEl.isContentEditable == "boolean" ?
 269                  function (node) {
 270                      return node && node.nodeType == 1 && node.isContentEditable;
 271                  } :
 272                  function (node) {
 273                      if (!node || node.nodeType != 1 || node.contentEditable == "false") {
 274                          return false;
 275                      }
 276                      return node.contentEditable == "true" || isEditableElement(node.parentNode);
 277                  };
 278          })();
 279  
 280          function isEditingHost(node) {
 281              var parent;
 282              return node && node.nodeType == 1 &&
 283                  (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") ||
 284                  (isEditableElement(node) && !isEditableElement(node.parentNode)));
 285          }
 286  
 287          function isEditable(node) {
 288              return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
 289          }
 290  
 291          var inlineDisplayRegex = /^inline(-block|-table)?$/i;
 292  
 293          function isNonInlineElement(node) {
 294              return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
 295          }
 296  
 297          // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
 298          var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
 299  
 300          function isUnrenderedWhiteSpaceNode(node) {
 301              if (node.data.length == 0) {
 302                  return true;
 303              }
 304              if (htmlNonWhiteSpaceRegex.test(node.data)) {
 305                  return false;
 306              }
 307              var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
 308              switch (cssWhiteSpace) {
 309                  case "pre":
 310                  case "pre-wrap":
 311                  case "-moz-pre-wrap":
 312                      return false;
 313                  case "pre-line":
 314                      if (/[\r\n]/.test(node.data)) {
 315                          return false;
 316                      }
 317              }
 318  
 319              // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
 320              // non-inline element, it will not be rendered. This seems to be a good enough definition.
 321              return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
 322          }
 323  
 324          function getRangeBoundaries(ranges) {
 325              var positions = [], i, range;
 326              for (i = 0; range = ranges[i++]; ) {
 327                  positions.push(
 328                      new DomPosition(range.startContainer, range.startOffset),
 329                      new DomPosition(range.endContainer, range.endOffset)
 330                  );
 331              }
 332              return positions;
 333          }
 334  
 335          function updateRangesFromBoundaries(ranges, positions) {
 336              for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) {
 337                  range = ranges[i];
 338                  start = positions[i * 2];
 339                  end = positions[i * 2 + 1];
 340                  range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
 341              }
 342          }
 343  
 344          function isSplitPoint(node, offset) {
 345              if (dom.isCharacterDataNode(node)) {
 346                  if (offset == 0) {
 347                      return !!node.previousSibling;
 348                  } else if (offset == node.length) {
 349                      return !!node.nextSibling;
 350                  } else {
 351                      return true;
 352                  }
 353              }
 354  
 355              return offset > 0 && offset < node.childNodes.length;
 356          }
 357  
 358          function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) {
 359              var newNode, parentNode;
 360              var splitAtStart = (descendantOffset == 0);
 361  
 362              if (dom.isAncestorOf(descendantNode, node)) {
 363                  return node;
 364              }
 365  
 366              if (dom.isCharacterDataNode(descendantNode)) {
 367                  var descendantIndex = dom.getNodeIndex(descendantNode);
 368                  if (descendantOffset == 0) {
 369                      descendantOffset = descendantIndex;
 370                  } else if (descendantOffset == descendantNode.length) {
 371                      descendantOffset = descendantIndex + 1;
 372                  } else {
 373                      throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node (" +
 374                          descendantOffset + " in " + descendantNode.data);
 375                  }
 376                  descendantNode = descendantNode.parentNode;
 377              }
 378  
 379              if (isSplitPoint(descendantNode, descendantOffset)) {
 380                  // descendantNode is now guaranteed not to be a text or other character node
 381                  newNode = descendantNode.cloneNode(false);
 382                  parentNode = descendantNode.parentNode;
 383                  if (newNode.id) {
 384                      newNode.removeAttribute("id");
 385                  }
 386                  var child, newChildIndex = 0;
 387  
 388                  while ( (child = descendantNode.childNodes[descendantOffset]) ) {
 389                      movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve);
 390                  }
 391                  movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve);
 392                  return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve);
 393              } else if (node != descendantNode) {
 394                  newNode = descendantNode.parentNode;
 395  
 396                  // Work out a new split point in the parent node
 397                  var newNodeIndex = dom.getNodeIndex(descendantNode);
 398  
 399                  if (!splitAtStart) {
 400                      newNodeIndex++;
 401                  }
 402                  return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve);
 403              }
 404              return node;
 405          }
 406  
 407          function areElementsMergeable(el1, el2) {
 408              return el1.namespaceURI == el2.namespaceURI &&
 409                  el1.tagName.toLowerCase() == el2.tagName.toLowerCase() &&
 410                  haveSameClasses(el1, el2) &&
 411                  elementsHaveSameNonClassAttributes(el1, el2) &&
 412                  getComputedStyleProperty(el1, "display") == "inline" &&
 413                  getComputedStyleProperty(el2, "display") == "inline";
 414          }
 415  
 416          function createAdjacentMergeableTextNodeGetter(forward) {
 417              var siblingPropName = forward ? "nextSibling" : "previousSibling";
 418  
 419              return function(textNode, checkParentElement) {
 420                  var el = textNode.parentNode;
 421                  var adjacentNode = textNode[siblingPropName];
 422                  if (adjacentNode) {
 423                      // Can merge if the node's previous/next sibling is a text node
 424                      if (adjacentNode && adjacentNode.nodeType == 3) {
 425                          return adjacentNode;
 426                      }
 427                  } else if (checkParentElement) {
 428                      // Compare text node parent element with its sibling
 429                      adjacentNode = el[siblingPropName];
 430                      if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
 431                          var adjacentNodeChild = adjacentNode[forward ? "firstChild" : "lastChild"];
 432                          if (adjacentNodeChild && adjacentNodeChild.nodeType == 3) {
 433                              return adjacentNodeChild;
 434                          }
 435                      }
 436                  }
 437                  return null;
 438              };
 439          }
 440  
 441          var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
 442              getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
 443  
 444      
 445          function Merge(firstNode) {
 446              this.isElementMerge = (firstNode.nodeType == 1);
 447              this.textNodes = [];
 448              var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
 449              if (firstTextNode) {
 450                  this.textNodes[0] = firstTextNode;
 451              }
 452          }
 453  
 454          Merge.prototype = {
 455              doMerge: function(positionsToPreserve) {
 456                  var textNodes = this.textNodes;
 457                  var firstTextNode = textNodes[0];
 458                  if (textNodes.length > 1) {
 459                      var firstTextNodeIndex = dom.getNodeIndex(firstTextNode);
 460                      var textParts = [], combinedTextLength = 0, textNode, parent;
 461                      forEach(textNodes, function(textNode, i) {
 462                          parent = textNode.parentNode;
 463                          if (i > 0) {
 464                              parent.removeChild(textNode);
 465                              if (!parent.hasChildNodes()) {
 466                                  dom.removeNode(parent);
 467                              }
 468                              if (positionsToPreserve) {
 469                                  forEach(positionsToPreserve, function(position) {
 470                                      // Handle case where position is inside the text node being merged into a preceding node
 471                                      if (position.node == textNode) {
 472                                          position.node = firstTextNode;
 473                                          position.offset += combinedTextLength;
 474                                      }
 475                                      // Handle case where both text nodes precede the position within the same parent node
 476                                      if (position.node == parent && position.offset > firstTextNodeIndex) {
 477                                          --position.offset;
 478                                          if (position.offset == firstTextNodeIndex + 1 && i < len - 1) {
 479                                              position.node = firstTextNode;
 480                                              position.offset = combinedTextLength;
 481                                          }
 482                                      }
 483                                  });
 484                              }
 485                          }
 486                          textParts[i] = textNode.data;
 487                          combinedTextLength += textNode.data.length;
 488                      });
 489                      firstTextNode.data = textParts.join("");
 490                  }
 491                  return firstTextNode.data;
 492              },
 493  
 494              getLength: function() {
 495                  var i = this.textNodes.length, len = 0;
 496                  while (i--) {
 497                      len += this.textNodes[i].length;
 498                  }
 499                  return len;
 500              },
 501  
 502              toString: function() {
 503                  var textParts = [];
 504                  forEach(this.textNodes, function(textNode, i) {
 505                      textParts[i] = "'" + textNode.data + "'";
 506                  });
 507                  return "[Merge(" + textParts.join(",") + ")]";
 508              }
 509          };
 510  
 511          var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements",
 512              "removeEmptyElements", "onElementCreate"];
 513  
 514          // TODO: Populate this with every attribute name that corresponds to a property with a different name. Really??
 515          var attrNamesForProperties = {};
 516  
 517          function ClassApplier(className, options, tagNames) {
 518              var normalize, i, len, propName, applier = this;
 519              applier.cssClass = applier.className = className; // cssClass property is for backward compatibility
 520  
 521              var elementPropertiesFromOptions = null, elementAttributes = {};
 522  
 523              // Initialize from options object
 524              if (typeof options == "object" && options !== null) {
 525                  if (typeof options.elementTagName !== "undefined") {
 526                      options.elementTagName = options.elementTagName.toLowerCase();
 527                  }
 528                  tagNames = options.tagNames;
 529                  elementPropertiesFromOptions = options.elementProperties;
 530                  elementAttributes = options.elementAttributes;
 531  
 532                  for (i = 0; propName = optionProperties[i++]; ) {
 533                      if (options.hasOwnProperty(propName)) {
 534                          applier[propName] = options[propName];
 535                      }
 536                  }
 537                  normalize = options.normalize;
 538              } else {
 539                  normalize = options;
 540              }
 541  
 542              // Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying
 543              applier.normalize = (typeof normalize == "undefined") ? true : normalize;
 544  
 545              // Initialize element properties and attribute exceptions
 546              applier.attrExceptions = [];
 547              var el = document.createElement(applier.elementTagName);
 548              applier.elementProperties = applier.copyPropertiesToElement(elementPropertiesFromOptions, el, true);
 549              each(elementAttributes, function(attrName, attrValue) {
 550                  applier.attrExceptions.push(attrName);
 551                  // Ensure each attribute value is a string
 552                  elementAttributes[attrName] = "" + attrValue;
 553              });
 554              applier.elementAttributes = elementAttributes;
 555  
 556              applier.elementSortedClassName = applier.elementProperties.hasOwnProperty("className") ?
 557                  sortClassName(applier.elementProperties.className + " " + className) : className;
 558  
 559              // Initialize tag names
 560              applier.applyToAnyTagName = false;
 561              var type = typeof tagNames;
 562              if (type == "string") {
 563                  if (tagNames == "*") {
 564                      applier.applyToAnyTagName = true;
 565                  } else {
 566                      applier.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
 567                  }
 568              } else if (type == "object" && typeof tagNames.length == "number") {
 569                  applier.tagNames = [];
 570                  for (i = 0, len = tagNames.length; i < len; ++i) {
 571                      if (tagNames[i] == "*") {
 572                          applier.applyToAnyTagName = true;
 573                      } else {
 574                          applier.tagNames.push(tagNames[i].toLowerCase());
 575                      }
 576                  }
 577              } else {
 578                  applier.tagNames = [applier.elementTagName];
 579              }
 580          }
 581  
 582          ClassApplier.prototype = {
 583              elementTagName: defaultTagName,
 584              elementProperties: {},
 585              elementAttributes: {},
 586              ignoreWhiteSpace: true,
 587              applyToEditableOnly: false,
 588              useExistingElements: true,
 589              removeEmptyElements: true,
 590              onElementCreate: null,
 591  
 592              copyPropertiesToElement: function(props, el, createCopy) {
 593                  var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName;
 594  
 595                  for (var p in props) {
 596                      if (props.hasOwnProperty(p)) {
 597                          propValue = props[p];
 598                          elPropValue = el[p];
 599  
 600                          // Special case for class. The copied properties object has the applier's class as well as its own
 601                          // to simplify checks when removing styling elements
 602                          if (p == "className") {
 603                              addClass(el, propValue);
 604                              addClass(el, this.className);
 605                              el[p] = sortClassName(el[p]);
 606                              if (createCopy) {
 607                                  elProps[p] = propValue;
 608                              }
 609                          }
 610  
 611                          // Special case for style
 612                          else if (p == "style") {
 613                              elStyle = elPropValue;
 614                              if (createCopy) {
 615                                  elProps[p] = elPropsStyle = {};
 616                              }
 617                              for (s in props[p]) {
 618                                  if (props[p].hasOwnProperty(s)) {
 619                                      elStyle[s] = propValue[s];
 620                                      if (createCopy) {
 621                                          elPropsStyle[s] = elStyle[s];
 622                                      }
 623                                  }
 624                              }
 625                              this.attrExceptions.push(p);
 626                          } else {
 627                              el[p] = propValue;
 628                              // Copy the property back from the dummy element so that later comparisons to check whether
 629                              // elements may be removed are checking against the right value. For example, the href property
 630                              // of an element returns a fully qualified URL even if it was previously assigned a relative
 631                              // URL.
 632                              if (createCopy) {
 633                                  elProps[p] = el[p];
 634  
 635                                  // Not all properties map to identically-named attributes
 636                                  attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p;
 637                                  this.attrExceptions.push(attrName);
 638                              }
 639                          }
 640                      }
 641                  }
 642  
 643                  return createCopy ? elProps : "";
 644              },
 645  
 646              copyAttributesToElement: function(attrs, el) {
 647                  for (var attrName in attrs) {
 648                      if (attrs.hasOwnProperty(attrName) && !/^class(?:Name)?$/i.test(attrName)) {
 649                          el.setAttribute(attrName, attrs[attrName]);
 650                      }
 651                  }
 652              },
 653  
 654              appliesToElement: function(el) {
 655                  return contains(this.tagNames, el.tagName.toLowerCase());
 656              },
 657  
 658              getEmptyElements: function(range) {
 659                  var applier = this;
 660                  return range.getNodes([1], function(el) {
 661                      return applier.appliesToElement(el) && !el.hasChildNodes();
 662                  });
 663              },
 664  
 665              hasClass: function(node) {
 666                  return node.nodeType == 1 &&
 667                      (this.applyToAnyTagName || this.appliesToElement(node)) &&
 668                      hasClass(node, this.className);
 669              },
 670  
 671              getSelfOrAncestorWithClass: function(node) {
 672                  while (node) {
 673                      if (this.hasClass(node)) {
 674                          return node;
 675                      }
 676                      node = node.parentNode;
 677                  }
 678                  return null;
 679              },
 680  
 681              isModifiable: function(node) {
 682                  return !this.applyToEditableOnly || isEditable(node);
 683              },
 684  
 685              // White space adjacent to an unwrappable node can be ignored for wrapping
 686              isIgnorableWhiteSpaceNode: function(node) {
 687                  return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
 688              },
 689  
 690              // Normalizes nodes after applying a class to a Range.
 691              postApply: function(textNodes, range, positionsToPreserve, isUndo) {
 692                  var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
 693  
 694                  var merges = [], currentMerge;
 695  
 696                  var rangeStartNode = firstNode, rangeEndNode = lastNode;
 697                  var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
 698  
 699                  var textNode, precedingTextNode;
 700  
 701                  // Check for every required merge and create a Merge object for each
 702                  forEach(textNodes, function(textNode) {
 703                      precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
 704                      if (precedingTextNode) {
 705                          if (!currentMerge) {
 706                              currentMerge = new Merge(precedingTextNode);
 707                              merges.push(currentMerge);
 708                          }
 709                          currentMerge.textNodes.push(textNode);
 710                          if (textNode === firstNode) {
 711                              rangeStartNode = currentMerge.textNodes[0];
 712                              rangeStartOffset = rangeStartNode.length;
 713                          }
 714                          if (textNode === lastNode) {
 715                              rangeEndNode = currentMerge.textNodes[0];
 716                              rangeEndOffset = currentMerge.getLength();
 717                          }
 718                      } else {
 719                          currentMerge = null;
 720                      }
 721                  });
 722  
 723                  // Test whether the first node after the range needs merging
 724                  var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
 725  
 726                  if (nextTextNode) {
 727                      if (!currentMerge) {
 728                          currentMerge = new Merge(lastNode);
 729                          merges.push(currentMerge);
 730                      }
 731                      currentMerge.textNodes.push(nextTextNode);
 732                  }
 733  
 734                  // Apply the merges
 735                  if (merges.length) {
 736                      for (i = 0, len = merges.length; i < len; ++i) {
 737                          merges[i].doMerge(positionsToPreserve);
 738                      }
 739  
 740                      // Set the range boundaries
 741                      range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset);
 742                  }
 743              },
 744  
 745              createContainer: function(parentNode) {
 746                  var doc = dom.getDocument(parentNode);
 747                  var namespace;
 748                  var el = createElementNSSupported && !dom.isHtmlNamespace(parentNode) && (namespace = parentNode.namespaceURI) ?
 749                      doc.createElementNS(parentNode.namespaceURI, this.elementTagName) :
 750                      doc.createElement(this.elementTagName);
 751  
 752                  this.copyPropertiesToElement(this.elementProperties, el, false);
 753                  this.copyAttributesToElement(this.elementAttributes, el);
 754                  addClass(el, this.className);
 755                  if (this.onElementCreate) {
 756                      this.onElementCreate(el, this);
 757                  }
 758                  return el;
 759              },
 760  
 761              elementHasProperties: function(el, props) {
 762                  var applier = this;
 763                  return each(props, function(p, propValue) {
 764                      if (p == "className") {
 765                          // For checking whether we should reuse an existing element, we just want to check that the element
 766                          // has all the classes specified in the className property. When deciding whether the element is
 767                          // removable when unapplying a class, there is separate special handling to check whether the
 768                          // element has extra classes so the same simple check will do.
 769                          return hasAllClasses(el, propValue);
 770                      } else if (typeof propValue == "object") {
 771                          if (!applier.elementHasProperties(el[p], propValue)) {
 772                              return false;
 773                          }
 774                      } else if (el[p] !== propValue) {
 775                          return false;
 776                      }
 777                  });
 778              },
 779  
 780              elementHasAttributes: function(el, attrs) {
 781                  return each(attrs, function(name, value) {
 782                      if (el.getAttribute(name) !== value) {
 783                          return false;
 784                      }
 785                  });
 786              },
 787  
 788              applyToTextNode: function(textNode, positionsToPreserve) {
 789  
 790                  // Check whether the text node can be styled. Text within a <style> or <script> element, for example,
 791                  // should not be styled. See issue 283.
 792                  if (canTextBeStyled(textNode)) {
 793                      var parent = textNode.parentNode;
 794                      if (parent.childNodes.length == 1 &&
 795                          this.useExistingElements &&
 796                          this.appliesToElement(parent) &&
 797                          this.elementHasProperties(parent, this.elementProperties) &&
 798                          this.elementHasAttributes(parent, this.elementAttributes)) {
 799  
 800                          addClass(parent, this.className);
 801                      } else {
 802                          var textNodeParent = textNode.parentNode;
 803                          var el = this.createContainer(textNodeParent);
 804                          textNodeParent.insertBefore(el, textNode);
 805                          el.appendChild(textNode);
 806                      }
 807                  }
 808  
 809              },
 810  
 811              isRemovable: function(el) {
 812                  return el.tagName.toLowerCase() == this.elementTagName &&
 813                      getSortedClassName(el) == this.elementSortedClassName &&
 814                      this.elementHasProperties(el, this.elementProperties) &&
 815                      !elementHasNonClassAttributes(el, this.attrExceptions) &&
 816                      this.elementHasAttributes(el, this.elementAttributes) &&
 817                      this.isModifiable(el);
 818              },
 819  
 820              isEmptyContainer: function(el) {
 821                  var childNodeCount = el.childNodes.length;
 822                  return el.nodeType == 1 &&
 823                      this.isRemovable(el) &&
 824                      (childNodeCount == 0 || (childNodeCount == 1 && this.isEmptyContainer(el.firstChild)));
 825              },
 826  
 827              removeEmptyContainers: function(range) {
 828                  var applier = this;
 829                  var nodesToRemove = range.getNodes([1], function(el) {
 830                      return applier.isEmptyContainer(el);
 831                  });
 832  
 833                  var rangesToPreserve = [range];
 834                  var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
 835  
 836                  forEach(nodesToRemove, function(node) {
 837                      removePreservingPositions(node, positionsToPreserve);
 838                  });
 839  
 840                  // Update the range from the preserved boundary positions
 841                  updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
 842              },
 843  
 844              undoToTextNode: function(textNode, range, ancestorWithClass, positionsToPreserve) {
 845                  if (!range.containsNode(ancestorWithClass)) {
 846                      // Split out the portion of the ancestor from which we can remove the class
 847                      //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
 848                      var ancestorRange = range.cloneRange();
 849                      ancestorRange.selectNode(ancestorWithClass);
 850                      if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)) {
 851                          splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, positionsToPreserve);
 852                          range.setEndAfter(ancestorWithClass);
 853                      }
 854                      if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)) {
 855                          ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, positionsToPreserve);
 856                      }
 857                  }
 858  
 859                  if (this.isRemovable(ancestorWithClass)) {
 860                      replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
 861                  } else {
 862                      removeClass(ancestorWithClass, this.className);
 863                  }
 864              },
 865  
 866              splitAncestorWithClass: function(container, offset, positionsToPreserve) {
 867                  var ancestorWithClass = this.getSelfOrAncestorWithClass(container);
 868                  if (ancestorWithClass) {
 869                      splitNodeAt(ancestorWithClass, container, offset, positionsToPreserve);
 870                  }
 871              },
 872  
 873              undoToAncestor: function(ancestorWithClass, positionsToPreserve) {
 874                  if (this.isRemovable(ancestorWithClass)) {
 875                      replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
 876                  } else {
 877                      removeClass(ancestorWithClass, this.className);
 878                  }
 879              },
 880  
 881              applyToRange: function(range, rangesToPreserve) {
 882                  var applier = this;
 883                  rangesToPreserve = rangesToPreserve || [];
 884  
 885                  // Create an array of range boundaries to preserve
 886                  var positionsToPreserve = getRangeBoundaries(rangesToPreserve || []);
 887  
 888                  range.splitBoundariesPreservingPositions(positionsToPreserve);
 889  
 890                  // Tidy up the DOM by removing empty containers
 891                  if (applier.removeEmptyElements) {
 892                      applier.removeEmptyContainers(range);
 893                  }
 894  
 895                  var textNodes = getEffectiveTextNodes(range);
 896  
 897                  if (textNodes.length) {
 898                      forEach(textNodes, function(textNode) {
 899                          if (!applier.isIgnorableWhiteSpaceNode(textNode) && !applier.getSelfOrAncestorWithClass(textNode) &&
 900                                  applier.isModifiable(textNode)) {
 901                              applier.applyToTextNode(textNode, positionsToPreserve);
 902                          }
 903                      });
 904                      var lastTextNode = textNodes[textNodes.length - 1];
 905                      range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
 906                      if (applier.normalize) {
 907                          applier.postApply(textNodes, range, positionsToPreserve, false);
 908                      }
 909  
 910                      // Update the ranges from the preserved boundary positions
 911                      updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
 912                  }
 913  
 914                  // Apply classes to any appropriate empty elements
 915                  var emptyElements = applier.getEmptyElements(range);
 916  
 917                  forEach(emptyElements, function(el) {
 918                      addClass(el, applier.className);
 919                  });
 920              },
 921  
 922              applyToRanges: function(ranges) {
 923  
 924                  var i = ranges.length;
 925                  while (i--) {
 926                      this.applyToRange(ranges[i], ranges);
 927                  }
 928  
 929  
 930                  return ranges;
 931              },
 932  
 933              applyToSelection: function(win) {
 934                  var sel = api.getSelection(win);
 935                  sel.setRanges( this.applyToRanges(sel.getAllRanges()) );
 936              },
 937  
 938              undoToRange: function(range, rangesToPreserve) {
 939                  var applier = this;
 940                  // Create an array of range boundaries to preserve
 941                  rangesToPreserve = rangesToPreserve || [];
 942                  var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
 943  
 944  
 945                  range.splitBoundariesPreservingPositions(positionsToPreserve);
 946  
 947                  // Tidy up the DOM by removing empty containers
 948                  if (applier.removeEmptyElements) {
 949                      applier.removeEmptyContainers(range, positionsToPreserve);
 950                  }
 951  
 952                  var textNodes = getEffectiveTextNodes(range);
 953                  var textNode, ancestorWithClass;
 954                  var lastTextNode = textNodes[textNodes.length - 1];
 955  
 956                  if (textNodes.length) {
 957                      applier.splitAncestorWithClass(range.endContainer, range.endOffset, positionsToPreserve);
 958                      applier.splitAncestorWithClass(range.startContainer, range.startOffset, positionsToPreserve);
 959                      for (var i = 0, len = textNodes.length; i < len; ++i) {
 960                          textNode = textNodes[i];
 961                          ancestorWithClass = applier.getSelfOrAncestorWithClass(textNode);
 962                          if (ancestorWithClass && applier.isModifiable(textNode)) {
 963                              applier.undoToAncestor(ancestorWithClass, positionsToPreserve);
 964                          }
 965                      }
 966                      // Ensure the range is still valid
 967                      range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
 968  
 969  
 970                      if (applier.normalize) {
 971                          applier.postApply(textNodes, range, positionsToPreserve, true);
 972                      }
 973  
 974                      // Update the ranges from the preserved boundary positions
 975                      updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
 976                  }
 977  
 978                  // Remove class from any appropriate empty elements
 979                  var emptyElements = applier.getEmptyElements(range);
 980  
 981                  forEach(emptyElements, function(el) {
 982                      removeClass(el, applier.className);
 983                  });
 984              },
 985  
 986              undoToRanges: function(ranges) {
 987                  // Get ranges returned in document order
 988                  var i = ranges.length;
 989  
 990                  while (i--) {
 991                      this.undoToRange(ranges[i], ranges);
 992                  }
 993  
 994                  return ranges;
 995              },
 996  
 997              undoToSelection: function(win) {
 998                  var sel = api.getSelection(win);
 999                  var ranges = api.getSelection(win).getAllRanges();
1000                  this.undoToRanges(ranges);
1001                  sel.setRanges(ranges);
1002              },
1003  
1004              isAppliedToRange: function(range) {
1005                  if (range.collapsed || range.toString() == "") {
1006                      return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
1007                  } else {
1008                      var textNodes = range.getNodes( [3] );
1009                      if (textNodes.length)
1010                      for (var i = 0, textNode; textNode = textNodes[i++]; ) {
1011                          if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) &&
1012                                  this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
1013                              return false;
1014                          }
1015                      }
1016                      return true;
1017                  }
1018              },
1019  
1020              isAppliedToRanges: function(ranges) {
1021                  var i = ranges.length;
1022                  if (i == 0) {
1023                      return false;
1024                  }
1025                  while (i--) {
1026                      if (!this.isAppliedToRange(ranges[i])) {
1027                          return false;
1028                      }
1029                  }
1030                  return true;
1031              },
1032  
1033              isAppliedToSelection: function(win) {
1034                  var sel = api.getSelection(win);
1035                  return this.isAppliedToRanges(sel.getAllRanges());
1036              },
1037  
1038              toggleRange: function(range) {
1039                  if (this.isAppliedToRange(range)) {
1040                      this.undoToRange(range);
1041                  } else {
1042                      this.applyToRange(range);
1043                  }
1044              },
1045  
1046              toggleSelection: function(win) {
1047                  if (this.isAppliedToSelection(win)) {
1048                      this.undoToSelection(win);
1049                  } else {
1050                      this.applyToSelection(win);
1051                  }
1052              },
1053  
1054              getElementsWithClassIntersectingRange: function(range) {
1055                  var elements = [];
1056                  var applier = this;
1057                  range.getNodes([3], function(textNode) {
1058                      var el = applier.getSelfOrAncestorWithClass(textNode);
1059                      if (el && !contains(elements, el)) {
1060                          elements.push(el);
1061                      }
1062                  });
1063                  return elements;
1064              },
1065  
1066              detach: function() {}
1067          };
1068  
1069          function createClassApplier(className, options, tagNames) {
1070              return new ClassApplier(className, options, tagNames);
1071          }
1072  
1073          ClassApplier.util = {
1074              hasClass: hasClass,
1075              addClass: addClass,
1076              removeClass: removeClass,
1077              getClass: getClass,
1078              hasSameClasses: haveSameClasses,
1079              hasAllClasses: hasAllClasses,
1080              replaceWithOwnChildren: replaceWithOwnChildrenPreservingPositions,
1081              elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
1082              elementHasNonClassAttributes: elementHasNonClassAttributes,
1083              splitNodeAt: splitNodeAt,
1084              isEditableElement: isEditableElement,
1085              isEditingHost: isEditingHost,
1086              isEditable: isEditable
1087          };
1088  
1089          api.CssClassApplier = api.ClassApplier = ClassApplier;
1090          api.createClassApplier = createClassApplier;
1091          util.createAliasForDeprecatedMethod(api, "createCssClassApplier", "createClassApplier", module);
1092      });
1093      
1094      return rangy;
1095  }, this);


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1