Formatter.js 71 KB

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