rangy-classapplier.js 46 KB

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