Formatter.js 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018
  1. /**
  2. * Formatter.js
  3. *
  4. * Copyright 2009, Moxiecode Systems AB
  5. * Released under LGPL License.
  6. *
  7. * License: http://tinymce.moxiecode.com/license
  8. * Contributing: http://tinymce.moxiecode.com/contributing
  9. */
  10. (function(tinymce) {
  11. /**
  12. * Text formatter engine class. This class is used to apply formats like bold, italic, font size
  13. * etc to the current selection or specific nodes. This engine was build to replace the browsers
  14. * default formatting logic for execCommand due to it's inconsistant and buggy behavior.
  15. *
  16. * @class tinymce.Formatter
  17. * @example
  18. * tinymce.activeEditor.formatter.register('mycustomformat', {
  19. * inline : 'span',
  20. * styles : {color : '#ff0000'}
  21. * });
  22. *
  23. * tinymce.activeEditor.formatter.apply('mycustomformat');
  24. */
  25. /**
  26. * Constructs a new formatter instance.
  27. *
  28. * @constructor Formatter
  29. * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to.
  30. */
  31. tinymce.Formatter = function(ed) {
  32. var formats = {},
  33. each = tinymce.each,
  34. dom = ed.dom,
  35. selection = ed.selection,
  36. TreeWalker = tinymce.dom.TreeWalker,
  37. rangeUtils = new tinymce.dom.RangeUtils(dom),
  38. isValid = ed.schema.isValidChild,
  39. isBlock = dom.isBlock,
  40. forcedRootBlock = ed.settings.forced_root_block,
  41. nodeIndex = dom.nodeIndex,
  42. INVISIBLE_CHAR = '\uFEFF',
  43. MCE_ATTR_RE = /^(src|href|style)$/,
  44. FALSE = false,
  45. TRUE = true,
  46. undefined;
  47. function isArray(obj) {
  48. return obj instanceof Array;
  49. };
  50. function getParents(node, selector) {
  51. return dom.getParents(node, selector, dom.getRoot());
  52. };
  53. function isCaretNode(node) {
  54. return node.nodeType === 1 && (node.face === 'mceinline' || node.style.fontFamily === 'mceinline');
  55. };
  56. // Public functions
  57. /**
  58. * Returns the format by name or all formats if no name is specified.
  59. *
  60. * @method get
  61. * @param {String} name Optional name to retrive by.
  62. * @return {Array/Object} Array/Object with all registred formats or a specific format.
  63. */
  64. function get(name) {
  65. return name ? formats[name] : formats;
  66. };
  67. /**
  68. * Registers a specific format by name.
  69. *
  70. * @method register
  71. * @param {Object/String} name Name of the format for example "bold".
  72. * @param {Object/Array} format Optional format object or array of format variants can only be omitted if the first arg is an object.
  73. */
  74. function register(name, format) {
  75. if (name) {
  76. if (typeof(name) !== 'string') {
  77. each(name, function(format, name) {
  78. register(name, format);
  79. });
  80. } else {
  81. // Force format into array and add it to internal collection
  82. format = format.length ? format : [format];
  83. each(format, function(format) {
  84. // Set deep to false by default on selector formats this to avoid removing
  85. // alignment on images inside paragraphs when alignment is changed on paragraphs
  86. if (format.deep === undefined)
  87. format.deep = !format.selector;
  88. // Default to true
  89. if (format.split === undefined)
  90. format.split = !format.selector || format.inline;
  91. // Default to true
  92. if (format.remove === undefined && format.selector && !format.inline)
  93. format.remove = 'none';
  94. // Mark format as a mixed format inline + block level
  95. if (format.selector && format.inline) {
  96. format.mixed = true;
  97. format.block_expand = true;
  98. }
  99. // Split classes if needed
  100. if (typeof(format.classes) === 'string')
  101. format.classes = format.classes.split(/\s+/);
  102. });
  103. formats[name] = format;
  104. }
  105. }
  106. };
  107. var getTextDecoration = function(node) {
  108. var decoration;
  109. ed.dom.getParent(node, function(n) {
  110. decoration = ed.dom.getStyle(n, 'text-decoration');
  111. return decoration && decoration !== 'none';
  112. });
  113. return decoration;
  114. };
  115. var processUnderlineAndColor = function(node) {
  116. var textDecoration;
  117. if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
  118. textDecoration = getTextDecoration(node.parentNode);
  119. if (ed.dom.getStyle(node, 'color') && textDecoration) {
  120. ed.dom.setStyle(node, 'text-decoration', textDecoration);
  121. } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) {
  122. ed.dom.setStyle(node, 'text-decoration', null);
  123. }
  124. }
  125. };
  126. /**
  127. * Applies the specified format to the current selection or specified node.
  128. *
  129. * @method apply
  130. * @param {String} name Name of format to apply.
  131. * @param {Object} vars Optional list of variables to replace within format before applying it.
  132. * @param {Node} node Optional node to apply the format to defaults to current selection.
  133. */
  134. function apply(name, vars, node) {
  135. var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed();
  136. /**
  137. * Moves the start to the first suitable text node.
  138. */
  139. function moveStart(rng) {
  140. var container = rng.startContainer,
  141. offset = rng.startOffset,
  142. walker, node;
  143. // Move startContainer/startOffset in to a suitable node
  144. if (container.nodeType == 1 || container.nodeValue === "") {
  145. container = container.nodeType == 1 ? container.childNodes[offset] : container;
  146. // Might fail if the offset is behind the last element in it's container
  147. if (container) {
  148. walker = new TreeWalker(container, container.parentNode);
  149. for (node = walker.current(); node; node = walker.next()) {
  150. if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
  151. rng.setStart(node, 0);
  152. break;
  153. }
  154. }
  155. }
  156. }
  157. return rng;
  158. };
  159. function setElementFormat(elm, fmt) {
  160. fmt = fmt || format;
  161. if (elm) {
  162. if (fmt.onformat) {
  163. fmt.onformat(elm, fmt, vars, node);
  164. }
  165. each(fmt.styles, function(value, name) {
  166. dom.setStyle(elm, name, replaceVars(value, vars));
  167. });
  168. each(fmt.attributes, function(value, name) {
  169. dom.setAttrib(elm, name, replaceVars(value, vars));
  170. });
  171. each(fmt.classes, function(value) {
  172. value = replaceVars(value, vars);
  173. if (!dom.hasClass(elm, value))
  174. dom.addClass(elm, value);
  175. });
  176. }
  177. };
  178. function adjustSelectionToVisibleSelection() {
  179. function findSelectionEnd(start, end) {
  180. var walker = new TreeWalker(end);
  181. for (node = walker.current(); node; node = walker.prev()) {
  182. if (node.childNodes.length > 1 || node == start) {
  183. return node;
  184. }
  185. }
  186. };
  187. // Adjust selection so that a end container with a end offset of zero is not included in the selection
  188. // as this isn't visible to the user.
  189. var rng = ed.selection.getRng();
  190. var start = rng.startContainer;
  191. var end = rng.endContainer;
  192. if (start != end && rng.endOffset == 0) {
  193. var newEnd = findSelectionEnd(start, end);
  194. var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length;
  195. rng.setEnd(newEnd, endOffset);
  196. }
  197. return rng;
  198. }
  199. function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){
  200. var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm;
  201. // find the index of the first child list.
  202. each(node.childNodes, function(n, index) {
  203. if (n.nodeName === "UL" || n.nodeName === "OL") {
  204. listIndex = index;
  205. list = n;
  206. return false;
  207. }
  208. });
  209. // get the index of the bookmarks
  210. each(node.childNodes, function(n, index) {
  211. if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") {
  212. if (n.id == bookmark.id + "_start") {
  213. startIndex = index;
  214. } else if (n.id == bookmark.id + "_end") {
  215. endIndex = index;
  216. }
  217. }
  218. });
  219. // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally
  220. if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) {
  221. each(tinymce.grep(node.childNodes), process);
  222. return 0;
  223. } else {
  224. currentWrapElm = wrapElm.cloneNode(FALSE);
  225. // create a list of the nodes on the same side of the list as the selection
  226. each(tinymce.grep(node.childNodes), function(n, index) {
  227. if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) {
  228. nodes.push(n);
  229. n.parentNode.removeChild(n);
  230. }
  231. });
  232. // insert the wrapping element either before or after the list.
  233. if (startIndex < listIndex) {
  234. node.insertBefore(currentWrapElm, list);
  235. } else if (startIndex > listIndex) {
  236. node.insertBefore(currentWrapElm, list.nextSibling);
  237. }
  238. // add the new nodes to the list.
  239. newWrappers.push(currentWrapElm);
  240. each(nodes, function(node) {
  241. currentWrapElm.appendChild(node);
  242. });
  243. return currentWrapElm;
  244. }
  245. };
  246. function applyRngStyle(rng, bookmark, node_specific) {
  247. var newWrappers = [], wrapName, wrapElm;
  248. // Setup wrapper element
  249. wrapName = format.inline || format.block;
  250. wrapElm = dom.create(wrapName);
  251. setElementFormat(wrapElm);
  252. rangeUtils.walk(rng, function(nodes) {
  253. var currentWrapElm;
  254. /**
  255. * Process a list of nodes wrap them.
  256. */
  257. function process(node) {
  258. var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase(), found;
  259. // Stop wrapping on br elements
  260. if (isEq(nodeName, 'br')) {
  261. currentWrapElm = 0;
  262. // Remove any br elements when we wrap things
  263. if (format.block)
  264. dom.remove(node);
  265. return;
  266. }
  267. // If node is wrapper type
  268. if (format.wrapper && matchNode(node, name, vars)) {
  269. currentWrapElm = 0;
  270. return;
  271. }
  272. // Can we rename the block
  273. if (format.block && !format.wrapper && isTextBlock(nodeName)) {
  274. node = dom.rename(node, wrapName);
  275. setElementFormat(node);
  276. newWrappers.push(node);
  277. currentWrapElm = 0;
  278. return;
  279. }
  280. // Handle selector patterns
  281. if (format.selector) {
  282. // Look for matching formats
  283. each(formatList, function(format) {
  284. // Check collapsed state if it exists
  285. if ('collapsed' in format && format.collapsed !== isCollapsed) {
  286. return;
  287. }
  288. if (dom.is(node, format.selector) && !isCaretNode(node)) {
  289. setElementFormat(node, format);
  290. found = true;
  291. }
  292. });
  293. // Continue processing if a selector match wasn't found and a inline element is defined
  294. if (!format.inline || found) {
  295. currentWrapElm = 0;
  296. return;
  297. }
  298. }
  299. // Is it valid to wrap this item
  300. if (isValid(wrapName, nodeName) && isValid(parentName, wrapName) &&
  301. !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && node.id !== '_mce_caret') {
  302. // Start wrapping
  303. if (!currentWrapElm) {
  304. // Wrap the node
  305. currentWrapElm = wrapElm.cloneNode(FALSE);
  306. node.parentNode.insertBefore(currentWrapElm, node);
  307. newWrappers.push(currentWrapElm);
  308. }
  309. currentWrapElm.appendChild(node);
  310. } else if (nodeName == 'li' && bookmark) {
  311. // Start wrapping - if we are in a list node and have a bookmark, then we will always begin by wrapping in a new element.
  312. currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process);
  313. } else {
  314. // Start a new wrapper for possible children
  315. currentWrapElm = 0;
  316. each(tinymce.grep(node.childNodes), process);
  317. // End the last wrapper
  318. currentWrapElm = 0;
  319. }
  320. };
  321. // Process siblings from range
  322. each(nodes, process);
  323. });
  324. // Wrap links inside as well, for example color inside a link when the wrapper is around the link
  325. if (format.wrap_links === false) {
  326. each(newWrappers, function(node) {
  327. function process(node) {
  328. var i, currentWrapElm, children;
  329. if (node.nodeName === 'A') {
  330. currentWrapElm = wrapElm.cloneNode(FALSE);
  331. newWrappers.push(currentWrapElm);
  332. children = tinymce.grep(node.childNodes);
  333. for (i = 0; i < children.length; i++)
  334. currentWrapElm.appendChild(children[i]);
  335. node.appendChild(currentWrapElm);
  336. }
  337. each(tinymce.grep(node.childNodes), process);
  338. };
  339. process(node);
  340. });
  341. }
  342. // Cleanup
  343. each(newWrappers, function(node) {
  344. var childCount;
  345. function getChildCount(node) {
  346. var count = 0;
  347. each(node.childNodes, function(node) {
  348. if (!isWhiteSpaceNode(node) && !isBookmarkNode(node))
  349. count++;
  350. });
  351. return count;
  352. };
  353. function mergeStyles(node) {
  354. var child, clone;
  355. each(node.childNodes, function(node) {
  356. if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) {
  357. child = node;
  358. return FALSE; // break loop
  359. }
  360. });
  361. // If child was found and of the same type as the current node
  362. if (child && matchName(child, format)) {
  363. clone = child.cloneNode(FALSE);
  364. setElementFormat(clone);
  365. dom.replace(clone, node, TRUE);
  366. dom.remove(child, 1);
  367. }
  368. return clone || node;
  369. };
  370. childCount = getChildCount(node);
  371. // Remove empty nodes but only if there is multiple wrappers and they are not block
  372. // elements so never remove single <h1></h1> since that would remove the currrent empty block element where the caret is at
  373. if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) {
  374. dom.remove(node, 1);
  375. return;
  376. }
  377. if (format.inline || format.wrapper) {
  378. // Merges the current node with it's children of similar type to reduce the number of elements
  379. if (!format.exact && childCount === 1)
  380. node = mergeStyles(node);
  381. // Remove/merge children
  382. each(formatList, function(format) {
  383. // Merge all children of similar type will move styles from child to parent
  384. // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
  385. // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
  386. each(dom.select(format.inline, node), function(child) {
  387. var parent;
  388. // When wrap_links is set to false we don't want
  389. // to remove the format on children within links
  390. if (format.wrap_links === false) {
  391. parent = child.parentNode;
  392. do {
  393. if (parent.nodeName === 'A')
  394. return;
  395. } while (parent = parent.parentNode);
  396. }
  397. removeFormat(format, vars, child, format.exact ? child : null);
  398. });
  399. });
  400. // Remove child if direct parent is of same type
  401. if (matchNode(node.parentNode, name, vars)) {
  402. dom.remove(node, 1);
  403. node = 0;
  404. return TRUE;
  405. }
  406. // Look for parent with similar style format
  407. if (format.merge_with_parents) {
  408. dom.getParent(node.parentNode, function(parent) {
  409. if (matchNode(parent, name, vars)) {
  410. dom.remove(node, 1);
  411. node = 0;
  412. return TRUE;
  413. }
  414. });
  415. }
  416. // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
  417. if (node && format.merge_siblings !== false) {
  418. node = mergeSiblings(getNonWhiteSpaceSibling(node), node);
  419. node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE));
  420. }
  421. }
  422. });
  423. };
  424. if (format) {
  425. if (node) {
  426. if (node.nodeType) {
  427. rng = dom.createRng();
  428. rng.setStartBefore(node);
  429. rng.setEndAfter(node);
  430. applyRngStyle(expandRng(rng, formatList), null, true);
  431. } else {
  432. applyRngStyle(node, null, true);
  433. }
  434. } else {
  435. if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {
  436. // Obtain selection node before selection is unselected by applyRngStyle()
  437. var curSelNode = ed.selection.getNode();
  438. // Apply formatting to selection
  439. ed.selection.setRng(adjustSelectionToVisibleSelection());
  440. bookmark = selection.getBookmark();
  441. applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark);
  442. // Colored nodes should be underlined so that the color of the underline matches the text color.
  443. if (format.styles && (format.styles.color || format.styles.textDecoration)) {
  444. tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes');
  445. processUnderlineAndColor(curSelNode);
  446. }
  447. selection.moveToBookmark(bookmark);
  448. selection.setRng(moveStart(selection.getRng(TRUE)));
  449. ed.nodeChanged();
  450. } else
  451. performCaretAction('apply', name, vars);
  452. }
  453. }
  454. };
  455. /**
  456. * Removes the specified format from the current selection or specified node.
  457. *
  458. * @method remove
  459. * @param {String} name Name of format to remove.
  460. * @param {Object} vars Optional list of variables to replace within format before removing it.
  461. * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection.
  462. */
  463. function remove(name, vars, node) {
  464. var formatList = get(name), format = formatList[0], bookmark, i, rng;
  465. /**
  466. * Moves the start to the first suitable text node.
  467. */
  468. function moveStart(rng) {
  469. var container = rng.startContainer,
  470. offset = rng.startOffset,
  471. walker, node, nodes, tmpNode;
  472. // Convert text node into index if possible
  473. if (container.nodeType == 3 && offset >= container.nodeValue.length - 1) {
  474. container = container.parentNode;
  475. offset = nodeIndex(container) + 1;
  476. }
  477. // Move startContainer/startOffset in to a suitable node
  478. if (container.nodeType == 1) {
  479. nodes = container.childNodes;
  480. container = nodes[Math.min(offset, nodes.length - 1)];
  481. walker = new TreeWalker(container);
  482. // If offset is at end of the parent node walk to the next one
  483. if (offset > nodes.length - 1)
  484. walker.next();
  485. for (node = walker.current(); node; node = walker.next()) {
  486. if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
  487. // IE has a "neat" feature where it moves the start node into the closest element
  488. // we can avoid this by inserting an element before it and then remove it after we set the selection
  489. tmpNode = dom.create('a', null, INVISIBLE_CHAR);
  490. node.parentNode.insertBefore(tmpNode, node);
  491. // Set selection and remove tmpNode
  492. rng.setStart(node, 0);
  493. selection.setRng(rng);
  494. dom.remove(tmpNode);
  495. return;
  496. }
  497. }
  498. }
  499. };
  500. // Merges the styles for each node
  501. function process(node) {
  502. var children, i, l;
  503. // Grab the children first since the nodelist might be changed
  504. children = tinymce.grep(node.childNodes);
  505. // Process current node
  506. for (i = 0, l = formatList.length; i < l; i++) {
  507. if (removeFormat(formatList[i], vars, node, node))
  508. break;
  509. }
  510. // Process the children
  511. if (format.deep) {
  512. for (i = 0, l = children.length; i < l; i++)
  513. process(children[i]);
  514. }
  515. };
  516. function findFormatRoot(container) {
  517. var formatRoot;
  518. // Find format root
  519. each(getParents(container.parentNode).reverse(), function(parent) {
  520. var format;
  521. // Find format root element
  522. if (!formatRoot && parent.id != '_start' && parent.id != '_end') {
  523. // Is the node matching the format we are looking for
  524. format = matchNode(parent, name, vars);
  525. if (format && format.split !== false)
  526. formatRoot = parent;
  527. }
  528. });
  529. return formatRoot;
  530. };
  531. function wrapAndSplit(format_root, container, target, split) {
  532. var parent, clone, lastClone, firstClone, i, formatRootParent;
  533. // Format root found then clone formats and split it
  534. if (format_root) {
  535. formatRootParent = format_root.parentNode;
  536. for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) {
  537. clone = parent.cloneNode(FALSE);
  538. for (i = 0; i < formatList.length; i++) {
  539. if (removeFormat(formatList[i], vars, clone, clone)) {
  540. clone = 0;
  541. break;
  542. }
  543. }
  544. // Build wrapper node
  545. if (clone) {
  546. if (lastClone)
  547. clone.appendChild(lastClone);
  548. if (!firstClone)
  549. firstClone = clone;
  550. lastClone = clone;
  551. }
  552. }
  553. // Never split block elements if the format is mixed
  554. if (split && (!format.mixed || !isBlock(format_root)))
  555. container = dom.split(format_root, container);
  556. // Wrap container in cloned formats
  557. if (lastClone) {
  558. target.parentNode.insertBefore(lastClone, target);
  559. firstClone.appendChild(target);
  560. }
  561. }
  562. return container;
  563. };
  564. function splitToFormatRoot(container) {
  565. return wrapAndSplit(findFormatRoot(container), container, container, true);
  566. };
  567. function unwrap(start) {
  568. var node = dom.get(start ? '_start' : '_end'),
  569. out = node[start ? 'firstChild' : 'lastChild'];
  570. // If the end is placed within the start the result will be removed
  571. // So this checks if the out node is a bookmark node if it is it
  572. // checks for another more suitable node
  573. if (isBookmarkNode(out))
  574. out = out[start ? 'firstChild' : 'lastChild'];
  575. dom.remove(node, true);
  576. return out;
  577. };
  578. function removeRngStyle(rng) {
  579. var startContainer, endContainer;
  580. rng = expandRng(rng, formatList, TRUE);
  581. if (format.split) {
  582. startContainer = getContainer(rng, TRUE);
  583. endContainer = getContainer(rng);
  584. if (startContainer != endContainer) {
  585. // Wrap start/end nodes in span element since these might be cloned/moved
  586. startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'});
  587. endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'});
  588. // Split start/end
  589. splitToFormatRoot(startContainer);
  590. splitToFormatRoot(endContainer);
  591. // Unwrap start/end to get real elements again
  592. startContainer = unwrap(TRUE);
  593. endContainer = unwrap();
  594. } else
  595. startContainer = endContainer = splitToFormatRoot(startContainer);
  596. // Update range positions since they might have changed after the split operations
  597. rng.startContainer = startContainer.parentNode;
  598. rng.startOffset = nodeIndex(startContainer);
  599. rng.endContainer = endContainer.parentNode;
  600. rng.endOffset = nodeIndex(endContainer) + 1;
  601. }
  602. // Remove items between start/end
  603. rangeUtils.walk(rng, function(nodes) {
  604. each(nodes, function(node) {
  605. process(node);
  606. // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined.
  607. if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') {
  608. removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node);
  609. }
  610. });
  611. });
  612. };
  613. // Handle node
  614. if (node) {
  615. if (node.nodeType) {
  616. rng = dom.createRng();
  617. rng.setStartBefore(node);
  618. rng.setEndAfter(node);
  619. removeRngStyle(rng);
  620. } else {
  621. removeRngStyle(node);
  622. }
  623. return;
  624. }
  625. if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {
  626. bookmark = selection.getBookmark();
  627. removeRngStyle(selection.getRng(TRUE));
  628. selection.moveToBookmark(bookmark);
  629. // Check if start element still has formatting then we are at: "<b>text|</b>text" and need to move the start into the next text node
  630. if (format.inline && match(name, vars, selection.getStart())) {
  631. moveStart(selection.getRng(true));
  632. }
  633. ed.nodeChanged();
  634. } else
  635. performCaretAction('remove', name, vars);
  636. // When you remove formatting from a table cell in WebKit (cell, not the contents of a cell) there is a rendering issue with column width
  637. if (tinymce.isWebKit) {
  638. ed.execCommand('mceCleanup');
  639. }
  640. };
  641. /**
  642. * Toggles the specified format on/off.
  643. *
  644. * @method toggle
  645. * @param {String} name Name of format to apply/remove.
  646. * @param {Object} vars Optional list of variables to replace within format before applying/removing it.
  647. * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection.
  648. */
  649. function toggle(name, vars, node) {
  650. var fmt = get(name);
  651. if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0]['toggle']))
  652. remove(name, vars, node);
  653. else
  654. apply(name, vars, node);
  655. };
  656. /**
  657. * Return true/false if the specified node has the specified format.
  658. *
  659. * @method matchNode
  660. * @param {Node} node Node to check the format on.
  661. * @param {String} name Format name to check.
  662. * @param {Object} vars Optional list of variables to replace before checking it.
  663. * @param {Boolean} similar Match format that has similar properties.
  664. * @return {Object} Returns the format object it matches or undefined if it doesn't match.
  665. */
  666. function matchNode(node, name, vars, similar) {
  667. var formatList = get(name), format, i, classes;
  668. function matchItems(node, format, item_name) {
  669. var key, value, items = format[item_name], i;
  670. // Custom match
  671. if (format.onmatch) {
  672. return format.onmatch(node, format, item_name);
  673. }
  674. // Check all items
  675. if (items) {
  676. // Non indexed object
  677. if (items.length === undefined) {
  678. for (key in items) {
  679. if (items.hasOwnProperty(key)) {
  680. if (item_name === 'attributes')
  681. value = dom.getAttrib(node, key);
  682. else
  683. value = getStyle(node, key);
  684. if (similar && !value && !format.exact)
  685. return;
  686. if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars)))
  687. return;
  688. }
  689. }
  690. } else {
  691. // Only one match needed for indexed arrays
  692. for (i = 0; i < items.length; i++) {
  693. if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i]))
  694. return format;
  695. }
  696. }
  697. }
  698. return format;
  699. };
  700. if (formatList && node) {
  701. // Check each format in list
  702. for (i = 0; i < formatList.length; i++) {
  703. format = formatList[i];
  704. // Name name, attributes, styles and classes
  705. if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) {
  706. // Match classes
  707. if (classes = format.classes) {
  708. for (i = 0; i < classes.length; i++) {
  709. if (!dom.hasClass(node, classes[i]))
  710. return;
  711. }
  712. }
  713. return format;
  714. }
  715. }
  716. }
  717. };
  718. /**
  719. * Matches the current selection or specified node against the specified format name.
  720. *
  721. * @method match
  722. * @param {String} name Name of format to match.
  723. * @param {Object} vars Optional list of variables to replace before checking it.
  724. * @param {Node} node Optional node to check.
  725. * @return {boolean} true/false if the specified selection/node matches the format.
  726. */
  727. function match(name, vars, node) {
  728. var startNode;
  729. function matchParents(node) {
  730. // Find first node with similar format settings
  731. node = dom.getParent(node, function(node) {
  732. return !!matchNode(node, name, vars, true);
  733. });
  734. // Do an exact check on the similar format element
  735. return matchNode(node, name, vars);
  736. };
  737. // Check specified node
  738. if (node)
  739. return matchParents(node);
  740. // Check selected node
  741. node = selection.getNode();
  742. if (matchParents(node))
  743. return TRUE;
  744. // Check start node if it's different
  745. startNode = selection.getStart();
  746. if (startNode != node) {
  747. if (matchParents(startNode))
  748. return TRUE;
  749. }
  750. return FALSE;
  751. };
  752. /**
  753. * Matches the current selection against the array of formats and returns a new array with matching formats.
  754. *
  755. * @method matchAll
  756. * @param {Array} names Name of format to match.
  757. * @param {Object} vars Optional list of variables to replace before checking it.
  758. * @return {Array} Array with matched formats.
  759. */
  760. function matchAll(names, vars) {
  761. var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name;
  762. // Check start of selection for formats
  763. startElement = selection.getStart();
  764. dom.getParent(startElement, function(node) {
  765. var i, name;
  766. for (i = 0; i < names.length; i++) {
  767. name = names[i];
  768. if (!checkedMap[name] && matchNode(node, name, vars)) {
  769. checkedMap[name] = true;
  770. matchedFormatNames.push(name);
  771. }
  772. }
  773. });
  774. return matchedFormatNames;
  775. };
  776. /**
  777. * Returns true/false if the specified format can be applied to the current selection or not. It will currently only check the state for selector formats, it returns true on all other format types.
  778. *
  779. * @method canApply
  780. * @param {String} name Name of format to check.
  781. * @return {boolean} true/false if the specified format can be applied to the current selection/node.
  782. */
  783. function canApply(name) {
  784. var formatList = get(name), startNode, parents, i, x, selector;
  785. if (formatList) {
  786. startNode = selection.getStart();
  787. parents = getParents(startNode);
  788. for (x = formatList.length - 1; x >= 0; x--) {
  789. selector = formatList[x].selector;
  790. // Format is not selector based, then always return TRUE
  791. if (!selector)
  792. return TRUE;
  793. for (i = parents.length - 1; i >= 0; i--) {
  794. if (dom.is(parents[i], selector))
  795. return TRUE;
  796. }
  797. }
  798. }
  799. return FALSE;
  800. };
  801. // Expose to public
  802. tinymce.extend(this, {
  803. get : get,
  804. register : register,
  805. apply : apply,
  806. remove : remove,
  807. toggle : toggle,
  808. match : match,
  809. matchAll : matchAll,
  810. matchNode : matchNode,
  811. canApply : canApply
  812. });
  813. // Private functions
  814. /**
  815. * Checks if the specified nodes name matches the format inline/block or selector.
  816. *
  817. * @private
  818. * @param {Node} node Node to match against the specified format.
  819. * @param {Object} format Format object o match with.
  820. * @return {boolean} true/false if the format matches.
  821. */
  822. function matchName(node, format) {
  823. // Check for inline match
  824. if (isEq(node, format.inline))
  825. return TRUE;
  826. // Check for block match
  827. if (isEq(node, format.block))
  828. return TRUE;
  829. // Check for selector match
  830. if (format.selector)
  831. return dom.is(node, format.selector);
  832. };
  833. /**
  834. * Compares two string/nodes regardless of their case.
  835. *
  836. * @private
  837. * @param {String/Node} Node or string to compare.
  838. * @param {String/Node} Node or string to compare.
  839. * @return {boolean} True/false if they match.
  840. */
  841. function isEq(str1, str2) {
  842. str1 = str1 || '';
  843. str2 = str2 || '';
  844. str1 = '' + (str1.nodeName || str1);
  845. str2 = '' + (str2.nodeName || str2);
  846. return str1.toLowerCase() == str2.toLowerCase();
  847. };
  848. /**
  849. * Returns the style by name on the specified node. This method modifies the style
  850. * contents to make it more easy to match. This will resolve a few browser issues.
  851. *
  852. * @private
  853. * @param {Node} node to get style from.
  854. * @param {String} name Style name to get.
  855. * @return {String} Style item value.
  856. */
  857. function getStyle(node, name) {
  858. var styleVal = dom.getStyle(node, name);
  859. // Force the format to hex
  860. if (name == 'color' || name == 'backgroundColor')
  861. styleVal = dom.toHex(styleVal);
  862. // Opera will return bold as 700
  863. if (name == 'fontWeight' && styleVal == 700)
  864. styleVal = 'bold';
  865. return '' + styleVal;
  866. };
  867. /**
  868. * Replaces variables in the value. The variable format is %var.
  869. *
  870. * @private
  871. * @param {String} value Value to replace variables in.
  872. * @param {Object} vars Name/value array with variables to replace.
  873. * @return {String} New value with replaced variables.
  874. */
  875. function replaceVars(value, vars) {
  876. if (typeof(value) != "string")
  877. value = value(vars);
  878. else if (vars) {
  879. value = value.replace(/%(\w+)/g, function(str, name) {
  880. return vars[name] || str;
  881. });
  882. }
  883. return value;
  884. };
  885. function isWhiteSpaceNode(node) {
  886. return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue);
  887. };
  888. function wrap(node, name, attrs) {
  889. var wrapper = dom.create(name, attrs);
  890. node.parentNode.insertBefore(wrapper, node);
  891. wrapper.appendChild(node);
  892. return wrapper;
  893. };
  894. /**
  895. * Expands the specified range like object to depending on format.
  896. *
  897. * For example on block formats it will move the start/end position
  898. * to the beginning of the current block.
  899. *
  900. * @private
  901. * @param {Object} rng Range like object.
  902. * @param {Array} formats Array with formats to expand by.
  903. * @return {Object} Expanded range like object.
  904. */
  905. function expandRng(rng, format, remove) {
  906. var startContainer = rng.startContainer,
  907. startOffset = rng.startOffset,
  908. endContainer = rng.endContainer,
  909. endOffset = rng.endOffset, sibling, lastIdx, leaf, endPoint;
  910. // This function walks up the tree if there is no siblings before/after the node
  911. function findParentContainer(start) {
  912. var container, parent, child, sibling, siblingName;
  913. container = parent = start ? startContainer : endContainer;
  914. siblingName = start ? 'previousSibling' : 'nextSibling';
  915. root = dom.getRoot();
  916. // If it's a text node and the offset is inside the text
  917. if (container.nodeType == 3 && !isWhiteSpaceNode(container)) {
  918. if (start ? startOffset > 0 : endOffset < container.nodeValue.length) {
  919. return container;
  920. }
  921. }
  922. for (;;) {
  923. // Stop expanding on block elements or root depending on format
  924. if (parent == root || (!format[0].block_expand && isBlock(parent)))
  925. return parent;
  926. // Walk left/right
  927. for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {
  928. if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) {
  929. return parent;
  930. }
  931. }
  932. // Check if we can move up are we at root level or body level
  933. parent = parent.parentNode;
  934. }
  935. return container;
  936. };
  937. // This function walks down the tree to find the leaf at the selection.
  938. // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
  939. function findLeaf(node, offset) {
  940. if (offset === undefined)
  941. offset = node.nodeType === 3 ? node.length : node.childNodes.length;
  942. while (node && node.hasChildNodes()) {
  943. node = node.childNodes[offset];
  944. if (node)
  945. offset = node.nodeType === 3 ? node.length : node.childNodes.length;
  946. }
  947. return { node: node, offset: offset };
  948. }
  949. // If index based start position then resolve it
  950. if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
  951. lastIdx = startContainer.childNodes.length - 1;
  952. startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset];
  953. if (startContainer.nodeType == 3)
  954. startOffset = 0;
  955. }
  956. // If index based end position then resolve it
  957. if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
  958. lastIdx = endContainer.childNodes.length - 1;
  959. endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1];
  960. if (endContainer.nodeType == 3)
  961. endOffset = endContainer.nodeValue.length;
  962. }
  963. // Exclude bookmark nodes if possible
  964. if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) {
  965. startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode;
  966. startContainer = startContainer.nextSibling || startContainer;
  967. if (startContainer.nodeType == 3)
  968. startOffset = 0;
  969. }
  970. if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) {
  971. endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode;
  972. endContainer = endContainer.previousSibling || endContainer;
  973. if (endContainer.nodeType == 3)
  974. endOffset = endContainer.length;
  975. }
  976. if (format[0].inline) {
  977. if (rng.collapsed) {
  978. function findWordEndPoint(container, offset, start) {
  979. var walker, node, pos, lastTextNode;
  980. function findSpace(node, offset) {
  981. var pos, pos2, str = node.nodeValue;
  982. if (typeof(offset) == "undefined") {
  983. offset = start ? str.length : 0;
  984. }
  985. if (start) {
  986. pos = str.lastIndexOf(' ', offset);
  987. pos2 = str.lastIndexOf('\u00a0', offset);
  988. pos = pos > pos2 ? pos : pos2;
  989. // Include the space on remove to avoid tag soup
  990. if (pos !== -1 && !remove) {
  991. pos++;
  992. }
  993. } else {
  994. pos = str.indexOf(' ', offset);
  995. pos2 = str.indexOf('\u00a0', offset);
  996. pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2;
  997. }
  998. return pos;
  999. };
  1000. if (container.nodeType === 3) {
  1001. pos = findSpace(container, offset);
  1002. if (pos !== -1) {
  1003. return {container : container, offset : pos};
  1004. }
  1005. lastTextNode = container;
  1006. }
  1007. // Walk the nodes inside the block
  1008. walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody());
  1009. while (node = walker[start ? 'prev' : 'next']()) {
  1010. if (node.nodeType === 3) {
  1011. lastTextNode = node;
  1012. pos = findSpace(node);
  1013. if (pos !== -1) {
  1014. return {container : node, offset : pos};
  1015. }
  1016. } else if (isBlock(node)) {
  1017. break;
  1018. }
  1019. }
  1020. if (lastTextNode) {
  1021. if (start) {
  1022. offset = 0;
  1023. } else {
  1024. offset = lastTextNode.length;
  1025. }
  1026. return {container: lastTextNode, offset: offset};
  1027. }
  1028. }
  1029. // Expand left to closest word boundery
  1030. endPoint = findWordEndPoint(startContainer, startOffset, true);
  1031. if (endPoint) {
  1032. startContainer = endPoint.container;
  1033. startOffset = endPoint.offset;
  1034. }
  1035. // Expand right to closest word boundery
  1036. endPoint = findWordEndPoint(endContainer, endOffset);
  1037. if (endPoint) {
  1038. endContainer = endPoint.container;
  1039. endOffset = endPoint.offset;
  1040. }
  1041. }
  1042. // Avoid applying formatting to a trailing space.
  1043. leaf = findLeaf(endContainer, endOffset);
  1044. if (leaf.node) {
  1045. while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling)
  1046. leaf = findLeaf(leaf.node.previousSibling);
  1047. if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
  1048. leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {
  1049. if (leaf.offset > 1) {
  1050. endContainer = leaf.node;
  1051. endContainer.splitText(leaf.offset - 1);
  1052. } else if (leaf.node.previousSibling) {
  1053. // TODO: Figure out why this is in here
  1054. //endContainer = leaf.node.previousSibling;
  1055. }
  1056. }
  1057. }
  1058. }
  1059. // Move start/end point up the tree if the leaves are sharp and if we are in different containers
  1060. // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
  1061. // This will reduce the number of wrapper elements that needs to be created
  1062. // Move start point up the tree
  1063. if (format[0].inline || format[0].block_expand) {
  1064. if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) {
  1065. startContainer = findParentContainer(true);
  1066. }
  1067. if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) {
  1068. endContainer = findParentContainer();
  1069. }
  1070. }
  1071. // Expand start/end container to matching selector
  1072. if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) {
  1073. function findSelectorEndPoint(container, sibling_name) {
  1074. var parents, i, y, curFormat;
  1075. if (container.nodeType == 3 && container.nodeValue.length == 0 && container[sibling_name])
  1076. container = container[sibling_name];
  1077. parents = getParents(container);
  1078. for (i = 0; i < parents.length; i++) {
  1079. for (y = 0; y < format.length; y++) {
  1080. curFormat = format[y];
  1081. // If collapsed state is set then skip formats that doesn't match that
  1082. if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed)
  1083. continue;
  1084. if (dom.is(parents[i], curFormat.selector))
  1085. return parents[i];
  1086. }
  1087. }
  1088. return container;
  1089. };
  1090. // Find new startContainer/endContainer if there is better one
  1091. startContainer = findSelectorEndPoint(startContainer, 'previousSibling');
  1092. endContainer = findSelectorEndPoint(endContainer, 'nextSibling');
  1093. }
  1094. // Expand start/end container to matching block element or text node
  1095. if (format[0].block || format[0].selector) {
  1096. function findBlockEndPoint(container, sibling_name, sibling_name2) {
  1097. var node;
  1098. // Expand to block of similar type
  1099. if (!format[0].wrapper)
  1100. node = dom.getParent(container, format[0].block);
  1101. // Expand to first wrappable block element or any block element
  1102. if (!node)
  1103. node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock);
  1104. // Exclude inner lists from wrapping
  1105. if (node && format[0].wrapper)
  1106. node = getParents(node, 'ul,ol').reverse()[0] || node;
  1107. // Didn't find a block element look for first/last wrappable element
  1108. if (!node) {
  1109. node = container;
  1110. while (node[sibling_name] && !isBlock(node[sibling_name])) {
  1111. node = node[sibling_name];
  1112. // Break on BR but include it will be removed later on
  1113. // we can't remove it now since we need to check if it can be wrapped
  1114. if (isEq(node, 'br'))
  1115. break;
  1116. }
  1117. }
  1118. return node || container;
  1119. };
  1120. // Find new startContainer/endContainer if there is better one
  1121. startContainer = findBlockEndPoint(startContainer, 'previousSibling');
  1122. endContainer = findBlockEndPoint(endContainer, 'nextSibling');
  1123. // Non block element then try to expand up the leaf
  1124. if (format[0].block) {
  1125. if (!isBlock(startContainer))
  1126. startContainer = findParentContainer(true);
  1127. if (!isBlock(endContainer))
  1128. endContainer = findParentContainer();
  1129. }
  1130. }
  1131. // Setup index for startContainer
  1132. if (startContainer.nodeType == 1) {
  1133. startOffset = nodeIndex(startContainer);
  1134. startContainer = startContainer.parentNode;
  1135. }
  1136. // Setup index for endContainer
  1137. if (endContainer.nodeType == 1) {
  1138. endOffset = nodeIndex(endContainer) + 1;
  1139. endContainer = endContainer.parentNode;
  1140. }
  1141. // Return new range like object
  1142. return {
  1143. startContainer : startContainer,
  1144. startOffset : startOffset,
  1145. endContainer : endContainer,
  1146. endOffset : endOffset
  1147. };
  1148. }
  1149. /**
  1150. * Removes the specified format for the specified node. It will also remove the node if it doesn't have
  1151. * any attributes if the format specifies it to do so.
  1152. *
  1153. * @private
  1154. * @param {Object} format Format object with items to remove from node.
  1155. * @param {Object} vars Name/value object with variables to apply to format.
  1156. * @param {Node} node Node to remove the format styles on.
  1157. * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node.
  1158. * @return {Boolean} True/false if the node was removed or not.
  1159. */
  1160. function removeFormat(format, vars, node, compare_node) {
  1161. var i, attrs, stylesModified;
  1162. // Check if node matches format
  1163. if (!matchName(node, format))
  1164. return FALSE;
  1165. // Should we compare with format attribs and styles
  1166. if (format.remove != 'all') {
  1167. // Remove styles
  1168. each(format.styles, function(value, name) {
  1169. value = replaceVars(value, vars);
  1170. // Indexed array
  1171. if (typeof(name) === 'number') {
  1172. name = value;
  1173. compare_node = 0;
  1174. }
  1175. if (!compare_node || isEq(getStyle(compare_node, name), value))
  1176. dom.setStyle(node, name, '');
  1177. stylesModified = 1;
  1178. });
  1179. // Remove style attribute if it's empty
  1180. if (stylesModified && dom.getAttrib(node, 'style') == '') {
  1181. node.removeAttribute('style');
  1182. node.removeAttribute('data-mce-style');
  1183. }
  1184. // Remove attributes
  1185. each(format.attributes, function(value, name) {
  1186. var valueOut;
  1187. value = replaceVars(value, vars);
  1188. // Indexed array
  1189. if (typeof(name) === 'number') {
  1190. name = value;
  1191. compare_node = 0;
  1192. }
  1193. if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) {
  1194. // Keep internal classes
  1195. if (name == 'class') {
  1196. value = dom.getAttrib(node, name);
  1197. if (value) {
  1198. // Build new class value where everything is removed except the internal prefixed classes
  1199. valueOut = '';
  1200. each(value.split(/\s+/), function(cls) {
  1201. if (/mce\w+/.test(cls))
  1202. valueOut += (valueOut ? ' ' : '') + cls;
  1203. });
  1204. // We got some internal classes left
  1205. if (valueOut) {
  1206. dom.setAttrib(node, name, valueOut);
  1207. return;
  1208. }
  1209. }
  1210. }
  1211. // IE6 has a bug where the attribute doesn't get removed correctly
  1212. if (name == "class")
  1213. node.removeAttribute('className');
  1214. // Remove mce prefixed attributes
  1215. if (MCE_ATTR_RE.test(name))
  1216. node.removeAttribute('data-mce-' + name);
  1217. node.removeAttribute(name);
  1218. }
  1219. });
  1220. // Remove classes
  1221. each(format.classes, function(value) {
  1222. value = replaceVars(value, vars);
  1223. if (!compare_node || dom.hasClass(compare_node, value))
  1224. dom.removeClass(node, value);
  1225. });
  1226. // Check for non internal attributes
  1227. attrs = dom.getAttribs(node);
  1228. for (i = 0; i < attrs.length; i++) {
  1229. if (attrs[i].nodeName.indexOf('_') !== 0)
  1230. return FALSE;
  1231. }
  1232. }
  1233. // Remove the inline child if it's empty for example <b> or <span>
  1234. if (format.remove != 'none') {
  1235. removeNode(node, format);
  1236. return TRUE;
  1237. }
  1238. };
  1239. /**
  1240. * Removes the node and wrap it's children in paragraphs before doing so or
  1241. * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled.
  1242. *
  1243. * If the div in the node below gets removed:
  1244. * text<div>text</div>text
  1245. *
  1246. * Output becomes:
  1247. * text<div><br />text<br /></div>text
  1248. *
  1249. * So when the div is removed the result is:
  1250. * text<br />text<br />text
  1251. *
  1252. * @private
  1253. * @param {Node} node Node to remove + apply BR/P elements to.
  1254. * @param {Object} format Format rule.
  1255. * @return {Node} Input node.
  1256. */
  1257. function removeNode(node, format) {
  1258. var parentNode = node.parentNode, rootBlockElm;
  1259. if (format.block) {
  1260. if (!forcedRootBlock) {
  1261. function find(node, next, inc) {
  1262. node = getNonWhiteSpaceSibling(node, next, inc);
  1263. return !node || (node.nodeName == 'BR' || isBlock(node));
  1264. };
  1265. // Append BR elements if needed before we remove the block
  1266. if (isBlock(node) && !isBlock(parentNode)) {
  1267. if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1))
  1268. node.insertBefore(dom.create('br'), node.firstChild);
  1269. if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1))
  1270. node.appendChild(dom.create('br'));
  1271. }
  1272. } else {
  1273. // Wrap the block in a forcedRootBlock if we are at the root of document
  1274. if (parentNode == dom.getRoot()) {
  1275. if (!format.list_block || !isEq(node, format.list_block)) {
  1276. each(tinymce.grep(node.childNodes), function(node) {
  1277. if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) {
  1278. if (!rootBlockElm)
  1279. rootBlockElm = wrap(node, forcedRootBlock);
  1280. else
  1281. rootBlockElm.appendChild(node);
  1282. } else
  1283. rootBlockElm = 0;
  1284. });
  1285. }
  1286. }
  1287. }
  1288. }
  1289. // Never remove nodes that isn't the specified inline element if a selector is specified too
  1290. if (format.selector && format.inline && !isEq(format.inline, node))
  1291. return;
  1292. dom.remove(node, 1);
  1293. };
  1294. /**
  1295. * Returns the next/previous non whitespace node.
  1296. *
  1297. * @private
  1298. * @param {Node} node Node to start at.
  1299. * @param {boolean} next (Optional) Include next or previous node defaults to previous.
  1300. * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false.
  1301. * @return {Node} Next or previous node or undefined if it wasn't found.
  1302. */
  1303. function getNonWhiteSpaceSibling(node, next, inc) {
  1304. if (node) {
  1305. next = next ? 'nextSibling' : 'previousSibling';
  1306. for (node = inc ? node : node[next]; node; node = node[next]) {
  1307. if (node.nodeType == 1 || !isWhiteSpaceNode(node))
  1308. return node;
  1309. }
  1310. }
  1311. };
  1312. /**
  1313. * Checks if the specified node is a bookmark node or not.
  1314. *
  1315. * @param {Node} node Node to check if it's a bookmark node or not.
  1316. * @return {Boolean} true/false if the node is a bookmark node.
  1317. */
  1318. function isBookmarkNode(node) {
  1319. return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark';
  1320. };
  1321. /**
  1322. * Merges the next/previous sibling element if they match.
  1323. *
  1324. * @private
  1325. * @param {Node} prev Previous node to compare/merge.
  1326. * @param {Node} next Next node to compare/merge.
  1327. * @return {Node} Next node if we didn't merge and prev node if we did.
  1328. */
  1329. function mergeSiblings(prev, next) {
  1330. var marker, sibling, tmpSibling;
  1331. /**
  1332. * Compares two nodes and checks if it's attributes and styles matches.
  1333. * This doesn't compare classes as items since their order is significant.
  1334. *
  1335. * @private
  1336. * @param {Node} node1 First node to compare with.
  1337. * @param {Node} node2 Second node to compare with.
  1338. * @return {boolean} True/false if the nodes are the same or not.
  1339. */
  1340. function compareElements(node1, node2) {
  1341. // Not the same name
  1342. if (node1.nodeName != node2.nodeName)
  1343. return FALSE;
  1344. /**
  1345. * Returns all the nodes attributes excluding internal ones, styles and classes.
  1346. *
  1347. * @private
  1348. * @param {Node} node Node to get attributes from.
  1349. * @return {Object} Name/value object with attributes and attribute values.
  1350. */
  1351. function getAttribs(node) {
  1352. var attribs = {};
  1353. each(dom.getAttribs(node), function(attr) {
  1354. var name = attr.nodeName.toLowerCase();
  1355. // Don't compare internal attributes or style
  1356. if (name.indexOf('_') !== 0 && name !== 'style')
  1357. attribs[name] = dom.getAttrib(node, name);
  1358. });
  1359. return attribs;
  1360. };
  1361. /**
  1362. * Compares two objects checks if it's key + value exists in the other one.
  1363. *
  1364. * @private
  1365. * @param {Object} obj1 First object to compare.
  1366. * @param {Object} obj2 Second object to compare.
  1367. * @return {boolean} True/false if the objects matches or not.
  1368. */
  1369. function compareObjects(obj1, obj2) {
  1370. var value, name;
  1371. for (name in obj1) {
  1372. // Obj1 has item obj2 doesn't have
  1373. if (obj1.hasOwnProperty(name)) {
  1374. value = obj2[name];
  1375. // Obj2 doesn't have obj1 item
  1376. if (value === undefined)
  1377. return FALSE;
  1378. // Obj2 item has a different value
  1379. if (obj1[name] != value)
  1380. return FALSE;
  1381. // Delete similar value
  1382. delete obj2[name];
  1383. }
  1384. }
  1385. // Check if obj 2 has something obj 1 doesn't have
  1386. for (name in obj2) {
  1387. // Obj2 has item obj1 doesn't have
  1388. if (obj2.hasOwnProperty(name))
  1389. return FALSE;
  1390. }
  1391. return TRUE;
  1392. };
  1393. // Attribs are not the same
  1394. if (!compareObjects(getAttribs(node1), getAttribs(node2)))
  1395. return FALSE;
  1396. // Styles are not the same
  1397. if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style'))))
  1398. return FALSE;
  1399. return TRUE;
  1400. };
  1401. // Check if next/prev exists and that they are elements
  1402. if (prev && next) {
  1403. function findElementSibling(node, sibling_name) {
  1404. for (sibling = node; sibling; sibling = sibling[sibling_name]) {
  1405. if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0)
  1406. return node;
  1407. if (sibling.nodeType == 1 && !isBookmarkNode(sibling))
  1408. return sibling;
  1409. }
  1410. return node;
  1411. };
  1412. // If previous sibling is empty then jump over it
  1413. prev = findElementSibling(prev, 'previousSibling');
  1414. next = findElementSibling(next, 'nextSibling');
  1415. // Compare next and previous nodes
  1416. if (compareElements(prev, next)) {
  1417. // Append nodes between
  1418. for (sibling = prev.nextSibling; sibling && sibling != next;) {
  1419. tmpSibling = sibling;
  1420. sibling = sibling.nextSibling;
  1421. prev.appendChild(tmpSibling);
  1422. }
  1423. // Remove next node
  1424. dom.remove(next);
  1425. // Move children into prev node
  1426. each(tinymce.grep(next.childNodes), function(node) {
  1427. prev.appendChild(node);
  1428. });
  1429. return prev;
  1430. }
  1431. }
  1432. return next;
  1433. };
  1434. /**
  1435. * Returns true/false if the specified node is a text block or not.
  1436. *
  1437. * @private
  1438. * @param {Node} node Node to check.
  1439. * @return {boolean} True/false if the node is a text block.
  1440. */
  1441. function isTextBlock(name) {
  1442. return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name);
  1443. };
  1444. function getContainer(rng, start) {
  1445. var container, offset, lastIdx, walker;
  1446. container = rng[start ? 'startContainer' : 'endContainer'];
  1447. offset = rng[start ? 'startOffset' : 'endOffset'];
  1448. if (container.nodeType == 1) {
  1449. lastIdx = container.childNodes.length - 1;
  1450. if (!start && offset)
  1451. offset--;
  1452. container = container.childNodes[offset > lastIdx ? lastIdx : offset];
  1453. }
  1454. // If start text node is excluded then walk to the next node
  1455. if (container.nodeType === 3 && start && offset >= container.nodeValue.length) {
  1456. container = new TreeWalker(container, ed.getBody()).next() || container;
  1457. }
  1458. // If end text node is excluded then walk to the previous node
  1459. if (container.nodeType === 3 && !start && offset == 0) {
  1460. container = new TreeWalker(container, ed.getBody()).prev() || container;
  1461. }
  1462. return container;
  1463. };
  1464. function performCaretAction(type, name, vars) {
  1465. var invisibleChar, caretContainerId = '_mce_caret', debug = ed.settings.caret_debug;
  1466. // Setup invisible character use zero width space on Gecko since it doesn't change the heigt of the container
  1467. invisibleChar = tinymce.isGecko ? '\u200B' : INVISIBLE_CHAR;
  1468. // Creates a caret container bogus element
  1469. function createCaretContainer(fill) {
  1470. var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''});
  1471. if (fill) {
  1472. caretContainer.appendChild(ed.getDoc().createTextNode(invisibleChar));
  1473. }
  1474. return caretContainer;
  1475. };
  1476. function isCaretContainerEmpty(node, nodes) {
  1477. while (node) {
  1478. if ((node.nodeType === 3 && node.nodeValue !== invisibleChar) || node.childNodes.length > 1) {
  1479. return false;
  1480. }
  1481. // Collect nodes
  1482. if (nodes && node.nodeType === 1) {
  1483. nodes.push(node);
  1484. }
  1485. node = node.firstChild;
  1486. }
  1487. return true;
  1488. };
  1489. // Returns any parent caret container element
  1490. function getParentCaretContainer(node) {
  1491. while (node) {
  1492. if (node.id === caretContainerId) {
  1493. return node;
  1494. }
  1495. node = node.parentNode;
  1496. }
  1497. };
  1498. // Finds the first text node in the specified node
  1499. function findFirstTextNode(node) {
  1500. var walker;
  1501. if (node) {
  1502. walker = new TreeWalker(node, node);
  1503. for (node = walker.current(); node; node = walker.next()) {
  1504. if (node.nodeType === 3) {
  1505. return node;
  1506. }
  1507. }
  1508. }
  1509. };
  1510. // Removes the caret container for the specified node or all on the current document
  1511. function removeCaretContainer(node, move_caret) {
  1512. var child, rng;
  1513. if (!node) {
  1514. node = getParentCaretContainer(selection.getStart());
  1515. if (!node) {
  1516. while (node = dom.get(caretContainerId)) {
  1517. removeCaretContainer(node, false);
  1518. }
  1519. }
  1520. } else {
  1521. rng = selection.getRng(true);
  1522. if (isCaretContainerEmpty(node)) {
  1523. if (move_caret !== false) {
  1524. rng.setStartBefore(node);
  1525. rng.setEndBefore(node);
  1526. }
  1527. dom.remove(node);
  1528. } else {
  1529. child = findFirstTextNode(node);
  1530. child = child.deleteData(0, 1);
  1531. dom.remove(node, 1);
  1532. }
  1533. selection.setRng(rng);
  1534. }
  1535. };
  1536. // Applies formatting to the caret postion
  1537. function applyCaretFormat() {
  1538. var rng, caretContainer, textNode, offset, bookmark, container, text;
  1539. rng = selection.getRng(true);
  1540. offset = rng.startOffset;
  1541. container = rng.startContainer;
  1542. text = container.nodeValue;
  1543. caretContainer = getParentCaretContainer(selection.getStart());
  1544. if (caretContainer) {
  1545. textNode = findFirstTextNode(caretContainer);
  1546. }
  1547. // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character
  1548. if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) {
  1549. // Get bookmark of caret position
  1550. bookmark = selection.getBookmark();
  1551. // Collapse bookmark range (WebKit)
  1552. rng.collapse(true);
  1553. // Expand the range to the closest word and split it at those points
  1554. rng = expandRng(rng, get(name));
  1555. rng = rangeUtils.split(rng);
  1556. // Apply the format to the range
  1557. apply(name, vars, rng);
  1558. // Move selection back to caret position
  1559. selection.moveToBookmark(bookmark);
  1560. } else {
  1561. if (!caretContainer || textNode.nodeValue !== invisibleChar) {
  1562. caretContainer = createCaretContainer(true);
  1563. textNode = caretContainer.firstChild;
  1564. rng.insertNode(caretContainer);
  1565. offset = 1;
  1566. apply(name, vars, caretContainer);
  1567. } else {
  1568. apply(name, vars, caretContainer);
  1569. }
  1570. // Move selection to text node
  1571. selection.setCursorLocation(textNode, offset);
  1572. }
  1573. };
  1574. function removeCaretFormat() {
  1575. var rng = selection.getRng(true), container, offset, bookmark,
  1576. hasContentAfter, node, formatNode, parents = [], i, caretContainer;
  1577. container = rng.startContainer;
  1578. offset = rng.startOffset;
  1579. node = container;
  1580. if (container.nodeType == 3) {
  1581. if (offset != container.nodeValue.length || container.nodeValue === invisibleChar) {
  1582. hasContentAfter = true;
  1583. }
  1584. node = node.parentNode;
  1585. }
  1586. while (node) {
  1587. if (matchNode(node, name, vars)) {
  1588. formatNode = node;
  1589. break;
  1590. }
  1591. if (node.nextSibling) {
  1592. hasContentAfter = true;
  1593. }
  1594. parents.push(node);
  1595. node = node.parentNode;
  1596. }
  1597. // Node doesn't have the specified format
  1598. if (!formatNode) {
  1599. return;
  1600. }
  1601. // Is there contents after the caret then remove the format on the element
  1602. if (hasContentAfter) {
  1603. // Get bookmark of caret position
  1604. bookmark = selection.getBookmark();
  1605. // Collapse bookmark range (WebKit)
  1606. rng.collapse(true);
  1607. // Expand the range to the closest word and split it at those points
  1608. rng = expandRng(rng, get(name), true);
  1609. rng = rangeUtils.split(rng);
  1610. // Remove the format from the range
  1611. remove(name, vars, rng);
  1612. // Move selection back to caret position
  1613. selection.moveToBookmark(bookmark);
  1614. } else {
  1615. caretContainer = createCaretContainer();
  1616. node = caretContainer;
  1617. for (i = parents.length - 1; i >= 0; i--) {
  1618. node.appendChild(parents[i].cloneNode(false));
  1619. node = node.firstChild;
  1620. }
  1621. // Insert invisible character into inner most format element
  1622. node.appendChild(dom.doc.createTextNode(invisibleChar));
  1623. node = node.firstChild;
  1624. // Insert caret container after the formated node
  1625. dom.insertAfter(caretContainer, formatNode);
  1626. // Move selection to text node
  1627. selection.setCursorLocation(node, 1);
  1628. }
  1629. };
  1630. // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements
  1631. ed.onBeforeGetContent.addToTop(function() {
  1632. var nodes = [], i;
  1633. if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) {
  1634. // Mark children
  1635. i = nodes.length;
  1636. while (i--) {
  1637. dom.setAttrib(nodes[i], 'data-mce-bogus', '1');
  1638. }
  1639. }
  1640. });
  1641. // Remove caret container on mouse up and on key up
  1642. tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) {
  1643. ed[name].addToTop(function() {
  1644. removeCaretContainer();
  1645. });
  1646. });
  1647. // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys
  1648. ed.onKeyDown.addToTop(function(ed, e) {
  1649. var keyCode = e.keyCode;
  1650. if (keyCode == 8 || keyCode == 37 || keyCode == 39) {
  1651. removeCaretContainer(getParentCaretContainer(selection.getStart()));
  1652. }
  1653. });
  1654. // Do apply or remove caret format
  1655. if (type == "apply") {
  1656. applyCaretFormat();
  1657. } else {
  1658. removeCaretFormat();
  1659. }
  1660. };
  1661. };
  1662. })(tinymce);