rangy-cssclassapplier.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  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. * http://code.google.com/p/rangy/
  7. *
  8. * Depends on Rangy core.
  9. *
  10. * Copyright 2013, Tim Down
  11. * Licensed under the MIT license.
  12. * Version: 1.3alpha.804
  13. * Build date: 8 December 2013
  14. */
  15. rangy.createModule("ClassApplier", ["WrappedSelection"], function(api, module) {
  16. var dom = api.dom;
  17. var DomPosition = dom.DomPosition;
  18. var contains = dom.arrayContains;
  19. var defaultTagName = "span";
  20. function each(obj, func) {
  21. for (var i in obj) {
  22. if (obj.hasOwnProperty(i)) {
  23. if (func(i, obj[i]) === false) {
  24. return false;
  25. }
  26. }
  27. }
  28. return true;
  29. }
  30. function trim(str) {
  31. return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
  32. }
  33. function hasClass(el, cssClass) {
  34. return el.className && new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)").test(el.className);
  35. }
  36. function addClass(el, cssClass) {
  37. if (el.className) {
  38. if (!hasClass(el, cssClass)) {
  39. el.className += " " + cssClass;
  40. }
  41. } else {
  42. el.className = cssClass;
  43. }
  44. }
  45. var removeClass = (function() {
  46. function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
  47. return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
  48. }
  49. return function(el, cssClass) {
  50. if (el.className) {
  51. el.className = el.className.replace(new RegExp("(^|\\s)" + cssClass + "(\\s|$)"), replacer);
  52. }
  53. };
  54. })();
  55. function sortClassName(className) {
  56. return className.split(/\s+/).sort().join(" ");
  57. }
  58. function getSortedClassName(el) {
  59. return sortClassName(el.className);
  60. }
  61. function haveSameClasses(el1, el2) {
  62. return getSortedClassName(el1) == getSortedClassName(el2);
  63. }
  64. function movePosition(position, oldParent, oldIndex, newParent, newIndex) {
  65. var node = position.node, offset = position.offset;
  66. var newNode = node, newOffset = offset;
  67. if (node == newParent && offset > newIndex) {
  68. ++newOffset;
  69. }
  70. if (node == oldParent && (offset == oldIndex || offset == oldIndex + 1)) {
  71. newNode = newParent;
  72. newOffset += newIndex - oldIndex;
  73. }
  74. if (node == oldParent && offset > oldIndex + 1) {
  75. --newOffset;
  76. }
  77. position.node = newNode;
  78. position.offset = newOffset;
  79. }
  80. function movePositionWhenRemovingNode(position, parentNode, index) {
  81. if (position.node == parentNode && position.offset > index) {
  82. --position.offset;
  83. }
  84. }
  85. function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) {
  86. // For convenience, allow newIndex to be -1 to mean "insert at the end".
  87. if (newIndex == -1) {
  88. newIndex = newParent.childNodes.length;
  89. }
  90. var oldParent = node.parentNode;
  91. var oldIndex = dom.getNodeIndex(node);
  92. for (var i = 0, position; position = positionsToPreserve[i++]; ) {
  93. movePosition(position, oldParent, oldIndex, newParent, newIndex);
  94. }
  95. // Now actually move the node.
  96. if (newParent.childNodes.length == newIndex) {
  97. newParent.appendChild(node);
  98. } else {
  99. newParent.insertBefore(node, newParent.childNodes[newIndex]);
  100. }
  101. }
  102. function removePreservingPositions(node, positionsToPreserve) {
  103. var oldParent = node.parentNode;
  104. var oldIndex = dom.getNodeIndex(node);
  105. for (var i = 0, position; position = positionsToPreserve[i++]; ) {
  106. movePositionWhenRemovingNode(position, oldParent, oldIndex);
  107. }
  108. node.parentNode.removeChild(node);
  109. }
  110. function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) {
  111. var child, children = [];
  112. while ( (child = node.firstChild) ) {
  113. movePreservingPositions(child, newParent, newIndex++, positionsToPreserve);
  114. children.push(child);
  115. }
  116. if (removeNode) {
  117. node.parentNode.removeChild(node);
  118. }
  119. return children;
  120. }
  121. function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) {
  122. return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve);
  123. }
  124. function rangeSelectsAnyText(range, textNode) {
  125. var textNodeRange = range.cloneRange();
  126. textNodeRange.selectNodeContents(textNode);
  127. var intersectionRange = textNodeRange.intersection(range);
  128. var text = intersectionRange ? intersectionRange.toString() : "";
  129. textNodeRange.detach();
  130. return text != "";
  131. }
  132. function getEffectiveTextNodes(range) {
  133. var nodes = range.getNodes([3]);
  134. // Optimization as per issue 145
  135. // Remove non-intersecting text nodes from the start of the range
  136. var start = 0, node;
  137. while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) {
  138. ++start;
  139. }
  140. // Remove non-intersecting text nodes from the start of the range
  141. var end = nodes.length - 1;
  142. while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) {
  143. --end;
  144. }
  145. return nodes.slice(start, end + 1);
  146. }
  147. function elementsHaveSameNonClassAttributes(el1, el2) {
  148. if (el1.attributes.length != el2.attributes.length) return false;
  149. for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
  150. attr1 = el1.attributes[i];
  151. name = attr1.name;
  152. if (name != "class") {
  153. attr2 = el2.attributes.getNamedItem(name);
  154. if ( (attr1 === null) != (attr2 === null) ) return false;
  155. if (attr1.specified != attr2.specified) return false;
  156. if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
  157. }
  158. }
  159. return true;
  160. }
  161. function elementHasNonClassAttributes(el, exceptions) {
  162. for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
  163. attrName = el.attributes[i].name;
  164. if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
  165. return true;
  166. }
  167. }
  168. return false;
  169. }
  170. function elementHasProperties(el, props) {
  171. each(props, function(p, propValue) {
  172. if (typeof propValue == "object") {
  173. if (!elementHasProperties(el[p], propValue)) {
  174. return false;
  175. }
  176. } else if (el[p] !== propValue) {
  177. return false;
  178. }
  179. });
  180. return true;
  181. }
  182. var getComputedStyleProperty = dom.getComputedStyleProperty;
  183. var isEditableElement = (function() {
  184. var testEl = document.createElement("div");
  185. return typeof testEl.isContentEditable == "boolean" ?
  186. function (node) {
  187. return node && node.nodeType == 1 && node.isContentEditable;
  188. } :
  189. function (node) {
  190. if (!node || node.nodeType != 1 || node.contentEditable == "false") {
  191. return false;
  192. }
  193. return node.contentEditable == "true" || isEditableElement(node.parentNode);
  194. };
  195. })();
  196. function isEditingHost(node) {
  197. var parent;
  198. return node && node.nodeType == 1
  199. && (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on")
  200. || (isEditableElement(node) && !isEditableElement(node.parentNode)));
  201. }
  202. function isEditable(node) {
  203. return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
  204. }
  205. var inlineDisplayRegex = /^inline(-block|-table)?$/i;
  206. function isNonInlineElement(node) {
  207. return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
  208. }
  209. // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
  210. var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
  211. function isUnrenderedWhiteSpaceNode(node) {
  212. if (node.data.length == 0) {
  213. return true;
  214. }
  215. if (htmlNonWhiteSpaceRegex.test(node.data)) {
  216. return false;
  217. }
  218. var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
  219. switch (cssWhiteSpace) {
  220. case "pre":
  221. case "pre-wrap":
  222. case "-moz-pre-wrap":
  223. return false;
  224. case "pre-line":
  225. if (/[\r\n]/.test(node.data)) {
  226. return false;
  227. }
  228. }
  229. // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
  230. // non-inline element, it will not be rendered. This seems to be a good enough definition.
  231. return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
  232. }
  233. function getRangeBoundaries(ranges) {
  234. var positions = [], i, range;
  235. for (i = 0; range = ranges[i++]; ) {
  236. positions.push(
  237. new DomPosition(range.startContainer, range.startOffset),
  238. new DomPosition(range.endContainer, range.endOffset)
  239. );
  240. }
  241. return positions;
  242. }
  243. function updateRangesFromBoundaries(ranges, positions) {
  244. for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) {
  245. range = ranges[i];
  246. start = positions[i * 2];
  247. end = positions[i * 2 + 1];
  248. range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
  249. }
  250. }
  251. function isSplitPoint(node, offset) {
  252. if (dom.isCharacterDataNode(node)) {
  253. if (offset == 0) {
  254. return !!node.previousSibling;
  255. } else if (offset == node.length) {
  256. return !!node.nextSibling;
  257. } else {
  258. return true;
  259. }
  260. }
  261. return offset > 0 && offset < node.childNodes.length;
  262. }
  263. function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) {
  264. var newNode, parentNode;
  265. var splitAtStart = (descendantOffset == 0);
  266. if (dom.isAncestorOf(descendantNode, node)) {
  267. return node;
  268. }
  269. if (dom.isCharacterDataNode(descendantNode)) {
  270. var descendantIndex = dom.getNodeIndex(descendantNode);
  271. if (descendantOffset == 0) {
  272. descendantOffset = descendantIndex;
  273. } else if (descendantOffset == descendantNode.length) {
  274. descendantOffset = descendantIndex + 1;
  275. } else {
  276. throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node ("
  277. + descendantOffset + " in " + descendantNode.data);
  278. }
  279. descendantNode = descendantNode.parentNode;
  280. }
  281. if (isSplitPoint(descendantNode, descendantOffset)) {
  282. // descendantNode is now guaranteed not to be a text or other character node
  283. newNode = descendantNode.cloneNode(false);
  284. parentNode = descendantNode.parentNode;
  285. if (newNode.id) {
  286. newNode.removeAttribute("id");
  287. }
  288. var child, newChildIndex = 0;
  289. while ( (child = descendantNode.childNodes[descendantOffset]) ) {
  290. movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve);
  291. }
  292. movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve);
  293. return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve);
  294. } else if (node != descendantNode) {
  295. newNode = descendantNode.parentNode;
  296. // Work out a new split point in the parent node
  297. var newNodeIndex = dom.getNodeIndex(descendantNode);
  298. if (!splitAtStart) {
  299. newNodeIndex++;
  300. }
  301. return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve);
  302. }
  303. return node;
  304. }
  305. function areElementsMergeable(el1, el2) {
  306. return el1.tagName == el2.tagName
  307. && haveSameClasses(el1, el2)
  308. && elementsHaveSameNonClassAttributes(el1, el2)
  309. && getComputedStyleProperty(el1, "display") == "inline"
  310. && getComputedStyleProperty(el2, "display") == "inline";
  311. }
  312. function createAdjacentMergeableTextNodeGetter(forward) {
  313. var siblingPropName = forward ? "nextSibling" : "previousSibling";
  314. return function(textNode, checkParentElement) {
  315. var el = textNode.parentNode;
  316. var adjacentNode = textNode[siblingPropName];
  317. if (adjacentNode) {
  318. // Can merge if the node's previous/next sibling is a text node
  319. if (adjacentNode && adjacentNode.nodeType == 3) {
  320. return adjacentNode;
  321. }
  322. } else if (checkParentElement) {
  323. // Compare text node parent element with its sibling
  324. adjacentNode = el[siblingPropName];
  325. if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
  326. var adjacentNodeChild = adjacentNode[forward ? "firstChild" : "lastChild"];
  327. if (adjacentNodeChild && adjacentNodeChild.nodeType == 3) {
  328. return adjacentNodeChild;
  329. }
  330. }
  331. }
  332. return null;
  333. };
  334. }
  335. var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
  336. getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
  337. function Merge(firstNode) {
  338. this.isElementMerge = (firstNode.nodeType == 1);
  339. this.textNodes = [];
  340. var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
  341. if (firstTextNode) {
  342. this.textNodes[0] = firstTextNode;
  343. }
  344. }
  345. Merge.prototype = {
  346. doMerge: function(positionsToPreserve) {
  347. var textNodes = this.textNodes;
  348. var firstTextNode = textNodes[0];
  349. if (textNodes.length > 1) {
  350. var textParts = [], combinedTextLength = 0, textNode, parent;
  351. for (var i = 0, len = textNodes.length, j, position; i < len; ++i) {
  352. textNode = textNodes[i];
  353. parent = textNode.parentNode;
  354. if (i > 0) {
  355. parent.removeChild(textNode);
  356. if (!parent.hasChildNodes()) {
  357. parent.parentNode.removeChild(parent);
  358. }
  359. if (positionsToPreserve) {
  360. for (j = 0; position = positionsToPreserve[j++]; ) {
  361. // Handle case where position is inside the text node being merged into a preceding node
  362. if (position.node == textNode) {
  363. position.node = firstTextNode;
  364. position.offset += combinedTextLength;
  365. }
  366. }
  367. }
  368. }
  369. textParts[i] = textNode.data;
  370. combinedTextLength += textNode.data.length;
  371. }
  372. firstTextNode.data = textParts.join("");
  373. }
  374. return firstTextNode.data;
  375. },
  376. getLength: function() {
  377. var i = this.textNodes.length, len = 0;
  378. while (i--) {
  379. len += this.textNodes[i].length;
  380. }
  381. return len;
  382. },
  383. toString: function() {
  384. var textParts = [];
  385. for (var i = 0, len = this.textNodes.length; i < len; ++i) {
  386. textParts[i] = "'" + this.textNodes[i].data + "'";
  387. }
  388. return "[Merge(" + textParts.join(",") + ")]";
  389. }
  390. };
  391. var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements",
  392. "removeEmptyElements", "onElementCreate"];
  393. // TODO: Populate this with every attribute name that corresponds to a property with a different name. Really??
  394. var attrNamesForProperties = {};
  395. function ClassApplier(cssClass, options, tagNames) {
  396. var normalize, i, len, propName, applier = this;
  397. applier.cssClass = cssClass;
  398. var elementPropertiesFromOptions = null, elementAttributes = {};
  399. // Initialize from options object
  400. if (typeof options == "object" && options !== null) {
  401. tagNames = options.tagNames;
  402. elementPropertiesFromOptions = options.elementProperties;
  403. elementAttributes = options.elementAttributes;
  404. for (i = 0; propName = optionProperties[i++]; ) {
  405. if (options.hasOwnProperty(propName)) {
  406. applier[propName] = options[propName];
  407. }
  408. }
  409. normalize = options.normalize;
  410. } else {
  411. normalize = options;
  412. }
  413. // Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying
  414. applier.normalize = (typeof normalize == "undefined") ? true : normalize;
  415. // Initialize element properties and attribute exceptions
  416. applier.attrExceptions = [];
  417. var el = document.createElement(applier.elementTagName);
  418. applier.elementProperties = applier.copyPropertiesToElement(elementPropertiesFromOptions, el, true);
  419. each(elementAttributes, function(attrName) {
  420. applier.attrExceptions.push(attrName);
  421. });
  422. applier.elementAttributes = elementAttributes;
  423. applier.elementSortedClassName = applier.elementProperties.hasOwnProperty("className") ?
  424. applier.elementProperties.className : cssClass;
  425. // Initialize tag names
  426. applier.applyToAnyTagName = false;
  427. var type = typeof tagNames;
  428. if (type == "string") {
  429. if (tagNames == "*") {
  430. applier.applyToAnyTagName = true;
  431. } else {
  432. applier.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
  433. }
  434. } else if (type == "object" && typeof tagNames.length == "number") {
  435. applier.tagNames = [];
  436. for (i = 0, len = tagNames.length; i < len; ++i) {
  437. if (tagNames[i] == "*") {
  438. applier.applyToAnyTagName = true;
  439. } else {
  440. applier.tagNames.push(tagNames[i].toLowerCase());
  441. }
  442. }
  443. } else {
  444. applier.tagNames = [applier.elementTagName];
  445. }
  446. }
  447. ClassApplier.prototype = {
  448. elementTagName: defaultTagName,
  449. elementProperties: {},
  450. elementAttributes: {},
  451. ignoreWhiteSpace: true,
  452. applyToEditableOnly: false,
  453. useExistingElements: true,
  454. removeEmptyElements: true,
  455. onElementCreate: null,
  456. copyPropertiesToElement: function(props, el, createCopy) {
  457. var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName;
  458. for (var p in props) {
  459. if (props.hasOwnProperty(p)) {
  460. propValue = props[p];
  461. elPropValue = el[p];
  462. // Special case for class. The copied properties object has the applier's CSS class as well as its
  463. // own to simplify checks when removing styling elements
  464. if (p == "className") {
  465. addClass(el, propValue);
  466. addClass(el, this.cssClass);
  467. el[p] = sortClassName(el[p]);
  468. if (createCopy) {
  469. elProps[p] = el[p];
  470. }
  471. }
  472. // Special case for style
  473. else if (p == "style") {
  474. elStyle = elPropValue;
  475. if (createCopy) {
  476. elProps[p] = elPropsStyle = {};
  477. }
  478. for (s in props[p]) {
  479. elStyle[s] = propValue[s];
  480. if (createCopy) {
  481. elPropsStyle[s] = elStyle[s];
  482. }
  483. }
  484. this.attrExceptions.push(p);
  485. } else {
  486. el[p] = propValue;
  487. // Copy the property back from the dummy element so that later comparisons to check whether
  488. // elements may be removed are checking against the right value. For example, the href property
  489. // of an element returns a fully qualified URL even if it was previously assigned a relative
  490. // URL.
  491. if (createCopy) {
  492. elProps[p] = el[p];
  493. // Not all properties map to identically-named attributes
  494. attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p;
  495. this.attrExceptions.push(attrName);
  496. }
  497. }
  498. }
  499. }
  500. return createCopy ? elProps : "";
  501. },
  502. copyAttributesToElement: function(attrs, el) {
  503. for (var attrName in attrs) {
  504. if (attrs.hasOwnProperty(attrName)) {
  505. el.setAttribute(attrName, attrs[attrName]);
  506. }
  507. }
  508. },
  509. hasClass: function(node) {
  510. return node.nodeType == 1 &&
  511. contains(this.tagNames, node.tagName.toLowerCase()) &&
  512. hasClass(node, this.cssClass);
  513. },
  514. getSelfOrAncestorWithClass: function(node) {
  515. while (node) {
  516. if (this.hasClass(node)) {
  517. return node;
  518. }
  519. node = node.parentNode;
  520. }
  521. return null;
  522. },
  523. isModifiable: function(node) {
  524. return !this.applyToEditableOnly || isEditable(node);
  525. },
  526. // White space adjacent to an unwrappable node can be ignored for wrapping
  527. isIgnorableWhiteSpaceNode: function(node) {
  528. return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
  529. },
  530. // Normalizes nodes after applying a CSS class to a Range.
  531. postApply: function(textNodes, range, positionsToPreserve, isUndo) {
  532. var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
  533. var merges = [], currentMerge;
  534. var rangeStartNode = firstNode, rangeEndNode = lastNode;
  535. var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
  536. var textNode, precedingTextNode;
  537. // Check for every required merge and create a Merge object for each
  538. for (var i = 0, len = textNodes.length; i < len; ++i) {
  539. textNode = textNodes[i];
  540. precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
  541. if (precedingTextNode) {
  542. if (!currentMerge) {
  543. currentMerge = new Merge(precedingTextNode);
  544. merges.push(currentMerge);
  545. }
  546. currentMerge.textNodes.push(textNode);
  547. if (textNode === firstNode) {
  548. rangeStartNode = currentMerge.textNodes[0];
  549. rangeStartOffset = rangeStartNode.length;
  550. }
  551. if (textNode === lastNode) {
  552. rangeEndNode = currentMerge.textNodes[0];
  553. rangeEndOffset = currentMerge.getLength();
  554. }
  555. } else {
  556. currentMerge = null;
  557. }
  558. }
  559. // Test whether the first node after the range needs merging
  560. var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
  561. if (nextTextNode) {
  562. if (!currentMerge) {
  563. currentMerge = new Merge(lastNode);
  564. merges.push(currentMerge);
  565. }
  566. currentMerge.textNodes.push(nextTextNode);
  567. }
  568. // Apply the merges
  569. if (merges.length) {
  570. for (i = 0, len = merges.length; i < len; ++i) {
  571. merges[i].doMerge(positionsToPreserve);
  572. }
  573. // Set the range boundaries
  574. range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset);
  575. }
  576. },
  577. createContainer: function(doc) {
  578. var el = doc.createElement(this.elementTagName);
  579. this.copyPropertiesToElement(this.elementProperties, el, false);
  580. this.copyAttributesToElement(this.elementAttributes, el);
  581. addClass(el, this.cssClass);
  582. if (this.onElementCreate) {
  583. this.onElementCreate(el, this);
  584. }
  585. return el;
  586. },
  587. applyToTextNode: function(textNode, positionsToPreserve) {
  588. var parent = textNode.parentNode;
  589. if (parent.childNodes.length == 1 &&
  590. this.useExistingElements &&
  591. contains(this.tagNames, parent.tagName.toLowerCase()) &&
  592. elementHasProperties(parent, this.elementProperties)) {
  593. addClass(parent, this.cssClass);
  594. } else {
  595. var el = this.createContainer(dom.getDocument(textNode));
  596. textNode.parentNode.insertBefore(el, textNode);
  597. el.appendChild(textNode);
  598. }
  599. },
  600. isRemovable: function(el) {
  601. return el.tagName.toLowerCase() == this.elementTagName
  602. && getSortedClassName(el) == this.elementSortedClassName
  603. && elementHasProperties(el, this.elementProperties)
  604. && !elementHasNonClassAttributes(el, this.attrExceptions)
  605. && this.isModifiable(el);
  606. },
  607. isEmptyContainer: function(el) {
  608. var childNodeCount = el.childNodes.length;
  609. return el.nodeType == 1
  610. && this.isRemovable(el)
  611. && (childNodeCount == 0 || (childNodeCount == 1 && this.isEmptyContainer(el.firstChild)));
  612. },
  613. removeEmptyContainers: function(range) {
  614. var applier = this;
  615. var nodesToRemove = range.getNodes([1], function(el) {
  616. return applier.isEmptyContainer(el);
  617. });
  618. var rangesToPreserve = [range]
  619. var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
  620. for (var i = 0, node; node = nodesToRemove[i++]; ) {
  621. removePreservingPositions(node, positionsToPreserve);
  622. }
  623. // Update the range from the preserved boundary positions
  624. updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
  625. },
  626. undoToTextNode: function(textNode, range, ancestorWithClass, positionsToPreserve) {
  627. if (!range.containsNode(ancestorWithClass)) {
  628. // Split out the portion of the ancestor from which we can remove the CSS class
  629. //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
  630. var ancestorRange = range.cloneRange();
  631. ancestorRange.selectNode(ancestorWithClass);
  632. if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)) {
  633. splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, positionsToPreserve);
  634. range.setEndAfter(ancestorWithClass);
  635. }
  636. if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)) {
  637. ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, positionsToPreserve);
  638. }
  639. }
  640. if (this.isRemovable(ancestorWithClass)) {
  641. replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
  642. } else {
  643. removeClass(ancestorWithClass, this.cssClass);
  644. }
  645. },
  646. applyToRange: function(range, rangesToPreserve) {
  647. rangesToPreserve = rangesToPreserve || [];
  648. // Create an array of range boundaries to preserve
  649. var positionsToPreserve = getRangeBoundaries(rangesToPreserve || []);
  650. range.splitBoundariesPreservingPositions(positionsToPreserve);
  651. // Tidy up the DOM by removing empty containers
  652. if (this.removeEmptyElements) {
  653. this.removeEmptyContainers(range);
  654. }
  655. var textNodes = getEffectiveTextNodes(range);
  656. if (textNodes.length) {
  657. for (var i = 0, textNode; textNode = textNodes[i++]; ) {
  658. if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode)
  659. && this.isModifiable(textNode)) {
  660. this.applyToTextNode(textNode, positionsToPreserve);
  661. }
  662. }
  663. textNode = textNodes[textNodes.length - 1];
  664. range.setStartAndEnd(textNodes[0], 0, textNode, textNode.length);
  665. if (this.normalize) {
  666. this.postApply(textNodes, range, positionsToPreserve, false);
  667. }
  668. // Update the ranges from the preserved boundary positions
  669. updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
  670. }
  671. },
  672. applyToRanges: function(ranges) {
  673. var i = ranges.length;
  674. while (i--) {
  675. this.applyToRange(ranges[i], ranges);
  676. }
  677. return ranges;
  678. },
  679. applyToSelection: function(win) {
  680. var sel = api.getSelection(win);
  681. sel.setRanges( this.applyToRanges(sel.getAllRanges()) );
  682. },
  683. undoToRange: function(range, rangesToPreserve) {
  684. // Create an array of range boundaries to preserve
  685. rangesToPreserve = rangesToPreserve || [];
  686. var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
  687. range.splitBoundariesPreservingPositions(positionsToPreserve);
  688. // Tidy up the DOM by removing empty containers
  689. if (this.removeEmptyElements) {
  690. this.removeEmptyContainers(range, positionsToPreserve);
  691. }
  692. var textNodes = getEffectiveTextNodes(range);
  693. var textNode, ancestorWithClass;
  694. var lastTextNode = textNodes[textNodes.length - 1];
  695. if (textNodes.length) {
  696. for (var i = 0, len = textNodes.length; i < len; ++i) {
  697. textNode = textNodes[i];
  698. ancestorWithClass = this.getSelfOrAncestorWithClass(textNode);
  699. if (ancestorWithClass && this.isModifiable(textNode)) {
  700. this.undoToTextNode(textNode, range, ancestorWithClass, positionsToPreserve);
  701. }
  702. // Ensure the range is still valid
  703. range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
  704. }
  705. if (this.normalize) {
  706. this.postApply(textNodes, range, positionsToPreserve, true);
  707. }
  708. // Update the ranges from the preserved boundary positions
  709. updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
  710. }
  711. },
  712. undoToRanges: function(ranges) {
  713. // Get ranges returned in document order
  714. var i = ranges.length;
  715. while (i--) {
  716. this.undoToRange(ranges[i], ranges);
  717. }
  718. return ranges;
  719. },
  720. undoToSelection: function(win) {
  721. var sel = api.getSelection(win);
  722. var ranges = api.getSelection(win).getAllRanges();
  723. this.undoToRanges(ranges);
  724. sel.setRanges(ranges);
  725. },
  726. /*
  727. getTextSelectedByRange: function(textNode, range) {
  728. var textRange = range.cloneRange();
  729. textRange.selectNodeContents(textNode);
  730. var intersectionRange = textRange.intersection(range);
  731. var text = intersectionRange ? intersectionRange.toString() : "";
  732. textRange.detach();
  733. return text;
  734. },
  735. */
  736. isAppliedToRange: function(range) {
  737. if (range.collapsed || range.toString() == "") {
  738. return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
  739. } else {
  740. var textNodes = range.getNodes( [3] );
  741. if (textNodes.length)
  742. for (var i = 0, textNode; textNode = textNodes[i++]; ) {
  743. if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode)
  744. && this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
  745. return false;
  746. }
  747. }
  748. return true;
  749. }
  750. },
  751. isAppliedToRanges: function(ranges) {
  752. var i = ranges.length;
  753. if (i == 0) {
  754. return false;
  755. }
  756. while (i--) {
  757. if (!this.isAppliedToRange(ranges[i])) {
  758. return false;
  759. }
  760. }
  761. return true;
  762. },
  763. isAppliedToSelection: function(win) {
  764. var sel = api.getSelection(win);
  765. return this.isAppliedToRanges(sel.getAllRanges());
  766. },
  767. toggleRange: function(range) {
  768. if (this.isAppliedToRange(range)) {
  769. this.undoToRange(range);
  770. } else {
  771. this.applyToRange(range);
  772. }
  773. },
  774. /*
  775. toggleRanges: function(ranges) {
  776. if (this.isAppliedToRanges(ranges)) {
  777. this.undoToRanges(ranges);
  778. } else {
  779. this.applyToRanges(ranges);
  780. }
  781. },
  782. */
  783. toggleSelection: function(win) {
  784. if (this.isAppliedToSelection(win)) {
  785. this.undoToSelection(win);
  786. } else {
  787. this.applyToSelection(win);
  788. }
  789. },
  790. getElementsWithClassIntersectingRange: function(range) {
  791. var elements = [];
  792. var applier = this;
  793. range.getNodes([3], function(textNode) {
  794. var el = applier.getSelfOrAncestorWithClass(textNode);
  795. if (el && !contains(elements, el)) {
  796. elements.push(el);
  797. }
  798. });
  799. return elements;
  800. },
  801. /*
  802. getElementsWithClassIntersectingSelection: function(win) {
  803. var sel = api.getSelection(win);
  804. var elements = [];
  805. var applier = this;
  806. sel.eachRange(function(range) {
  807. var rangeElements = applier.getElementsWithClassIntersectingRange(range);
  808. for (var i = 0, el; el = rangeElements[i++]; ) {
  809. if (!contains(elements, el)) {
  810. elements.push(el);
  811. }
  812. }
  813. });
  814. return elements;
  815. },
  816. */
  817. detach: function() {}
  818. };
  819. function createClassApplier(cssClass, options, tagNames) {
  820. return new ClassApplier(cssClass, options, tagNames);
  821. }
  822. ClassApplier.util = {
  823. hasClass: hasClass,
  824. addClass: addClass,
  825. removeClass: removeClass,
  826. hasSameClasses: haveSameClasses,
  827. replaceWithOwnChildren: replaceWithOwnChildrenPreservingPositions,
  828. elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
  829. elementHasNonClassAttributes: elementHasNonClassAttributes,
  830. splitNodeAt: splitNodeAt,
  831. isEditableElement: isEditableElement,
  832. isEditingHost: isEditingHost,
  833. isEditable: isEditable
  834. };
  835. api.CssClassApplier = api.ClassApplier = ClassApplier;
  836. api.createCssClassApplier = api.createClassApplier = createClassApplier;
  837. });