NormalizeStyle.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dojox.editor.plugins.NormalizeStyle"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojox.editor.plugins.NormalizeStyle"] = true;
  8. dojo.provide("dojox.editor.plugins.NormalizeStyle");
  9. dojo.require("dijit._editor.html");
  10. dojo.require("dijit._editor._Plugin");
  11. dojo.declare("dojox.editor.plugins.NormalizeStyle",dijit._editor._Plugin,{
  12. // summary:
  13. // This plugin provides NormalizeStyle cabability to the editor. It is
  14. // a headless plugin that tries to normalize how content is styled when
  15. // it comes out of th editor ('b' or css). It also auto-converts
  16. // incoming content to the proper one expected by the browser as well so
  17. // that the native styling buttons work.
  18. // mode [public] String
  19. // A String variable indicating if it should use semantic tags 'b', 'i', etc, or
  20. // CSS styling. The default is semantic.
  21. mode: "semantic",
  22. // condenseSpans [public] Boolean
  23. // A boolean variable indicating if it should try to condense
  24. // 'span''span''span' styles when in css mode
  25. // The default is true, it will try to combine where it can.
  26. condenseSpans: true,
  27. setEditor: function(editor){
  28. // summary:
  29. // Over-ride for the setting of the editor.
  30. // editor: Object
  31. // The editor to configure for this plugin to use.
  32. this.editor = editor;
  33. editor.customUndo = true;
  34. if(this.mode === "semantic"){
  35. this.editor.contentDomPostFilters.push(dojo.hitch(this, this._convertToSemantic));
  36. }else if(this.mode === "css"){
  37. this.editor.contentDomPostFilters.push(dojo.hitch(this, this._convertToCss));
  38. }
  39. // Pre DOM filters are usually based on what browser, as they all use different ways to
  40. // apply styles with actions and modify them.
  41. if(dojo.isIE){
  42. // IE still uses semantic tags most of the time, so convert to that.
  43. this.editor.contentDomPreFilters.push(dojo.hitch(this, this._convertToSemantic));
  44. this._browserFilter = this._convertToSemantic;
  45. }else if(dojo.isWebKit){
  46. this.editor.contentDomPreFilters.push(dojo.hitch(this, this._convertToCss));
  47. this._browserFilter = this._convertToCss;
  48. }else if(dojo.isMoz){
  49. //Editor currently forces Moz into semantic mode, so we need to match. Ideally
  50. //editor could get rid of that and just use CSS mode, which would work cleaner
  51. //That's why this is split out, to make it easy to change later.
  52. this.editor.contentDomPreFilters.push(dojo.hitch(this, this._convertToSemantic));
  53. this._browserFilter = this._convertToSemantic;
  54. }else{
  55. this.editor.contentDomPreFilters.push(dojo.hitch(this, this._convertToSemantic));
  56. this._browserFilter = this._convertToSemantic;
  57. }
  58. // Set up the inserthtml impl over-ride. This catches semi-paste events and
  59. // tries to normalize them too.
  60. if(this.editor._inserthtmlImpl){
  61. this.editor._oldInsertHtmlImpl = this.editor._inserthtmlImpl;
  62. }
  63. this.editor._inserthtmlImpl = dojo.hitch(this, this._inserthtmlImpl);
  64. },
  65. _convertToSemantic: function(node){
  66. // summary:
  67. // A function to convert the HTML structure of 'node' into
  68. // semantic tags where possible.
  69. // node: DOMNode
  70. // The node to process.
  71. // tags:
  72. // private
  73. if(node){
  74. var w = this.editor.window;
  75. var self = this;
  76. var convertNode = function(cNode){
  77. if(cNode.nodeType == 1){
  78. if(cNode.id !== "dijitEditorBody"){
  79. var style = cNode.style;
  80. var tag = cNode.tagName?cNode.tagName.toLowerCase():"";
  81. var sTag;
  82. if(style && tag != "table" && tag != "ul" && tag != "ol"){
  83. // Avoid wrapper blocks that have specific underlying structure, as injecting
  84. // spans/etc there is invalid.
  85. // Lets check and convert certain node/style types.
  86. var fw = style.fontWeight? style.fontWeight.toLowerCase() : "";
  87. var fs = style.fontStyle? style.fontStyle.toLowerCase() : "";
  88. var td = style.textDecoration? style.textDecoration.toLowerCase() : "";
  89. var s = style.fontSize?style.fontSize.toLowerCase() : "";
  90. var bc = style.backgroundColor?style.backgroundColor.toLowerCase() : "";
  91. var c = style.color?style.color.toLowerCase() : "";
  92. var wrapNodes = function(wrap, pNode){
  93. if(wrap){
  94. while(pNode.firstChild){
  95. wrap.appendChild(pNode.firstChild);
  96. }
  97. if(tag == "span" && !pNode.style.cssText){
  98. // A styler tag with nothing extra in it, so lets remove it.
  99. dojo.place(wrap, pNode, "before");
  100. pNode.parentNode.removeChild(pNode);
  101. pNode = wrap;
  102. }else{
  103. pNode.appendChild(wrap);
  104. }
  105. }
  106. return pNode;
  107. };
  108. switch(fw){
  109. case "bold":
  110. case "bolder":
  111. case "700":
  112. case "800":
  113. case "900":
  114. sTag = dojo.withGlobal(w, "create", dojo, ["b", {}] );
  115. cNode.style.fontWeight = "";
  116. break;
  117. }
  118. cNode = wrapNodes(sTag, cNode);
  119. sTag = null;
  120. if(fs == "italic"){
  121. sTag = dojo.withGlobal(w, "create", dojo, ["i", {}] );
  122. cNode.style.fontStyle = "";
  123. }
  124. cNode = wrapNodes(sTag, cNode);
  125. sTag = null;
  126. if(td){
  127. var da = td.split(" ");
  128. var count = 0;
  129. dojo.forEach(da, function(s){
  130. switch(s){
  131. case "underline":
  132. sTag = dojo.withGlobal(w, "create", dojo, ["u", {}] );
  133. break;
  134. case "line-through":
  135. sTag = dojo.withGlobal(w, "create", dojo, ["strike", {}] );
  136. break;
  137. }
  138. count++;
  139. if(count == da.length){
  140. // Last one, clear the decor and see if we can span strip on wrap.
  141. cNode.style.textDecoration = "";
  142. }
  143. cNode = wrapNodes(sTag, cNode);
  144. sTag = null;
  145. });
  146. }
  147. if(s){
  148. var sizeMap = {
  149. "xx-small": 1,
  150. "x-small": 2,
  151. "small": 3,
  152. "medium": 4,
  153. "large": 5,
  154. "x-large": 6,
  155. "xx-large": 7,
  156. "-webkit-xxx-large": 7
  157. };
  158. // Convert point or px size to size
  159. // to something roughly mappable.
  160. if(s.indexOf("pt") > 0){
  161. s = s.substring(0,s.indexOf("pt"));
  162. s = parseInt(s);
  163. if(s < 5){
  164. s = "xx-small";
  165. }else if(s < 10){
  166. s = "x-small";
  167. }else if(s < 15){
  168. s = "small";
  169. }else if(s < 20){
  170. s = "medium";
  171. }else if(s < 25){
  172. s = "large";
  173. }else if(s < 30){
  174. s = "x-large";
  175. }else if(s > 30){
  176. s = "xx-large";
  177. }
  178. }else if(s.indexOf("px") > 0){
  179. s = s.substring(0,s.indexOf("px"));
  180. s = parseInt(s);
  181. if(s < 5){
  182. s = "xx-small";
  183. }else if(s < 10){
  184. s = "x-small";
  185. }else if(s < 15){
  186. s = "small";
  187. }else if(s < 20){
  188. s = "medium";
  189. }else if(s < 25){
  190. s = "large";
  191. }else if(s < 30){
  192. s = "x-large";
  193. }else if(s > 30){
  194. s = "xx-large";
  195. }
  196. }
  197. var size = sizeMap[s];
  198. if(!size){
  199. size = 3;
  200. }
  201. sTag = dojo.withGlobal(w, "create", dojo, ["font", {size: size}] );
  202. cNode.style.fontSize = "";
  203. }
  204. cNode = wrapNodes(sTag, cNode);
  205. sTag = null;
  206. if(bc && tag !== "font" && self._isInline(tag)){
  207. // IE doesn't like non-font background color crud.
  208. // Also, don't move it in if the background color is set on a block style node,
  209. // as it won't color properly once put on inline font.
  210. bc = new dojo.Color(bc).toHex();
  211. sTag = dojo.withGlobal(w, "create", dojo, ["font", {style: {backgroundColor: bc}}] );
  212. cNode.style.backgroundColor = "";
  213. }
  214. if(c && tag !== "font"){
  215. // IE doesn't like non-font background color crud.
  216. c = new dojo.Color(c).toHex();
  217. sTag = dojo.withGlobal(w, "create", dojo, ["font", {color: c}] );
  218. cNode.style.color = "";
  219. }
  220. cNode = wrapNodes(sTag, cNode);
  221. sTag = null;
  222. }
  223. }
  224. if(cNode.childNodes){
  225. // Clone it, since we may alter its position
  226. var nodes = [];
  227. dojo.forEach(cNode.childNodes, function(n){ nodes.push(n);});
  228. dojo.forEach(nodes, convertNode);
  229. }
  230. }
  231. return cNode;
  232. };
  233. return this._normalizeTags(convertNode(node));
  234. }
  235. return node;
  236. },
  237. _normalizeTags: function(node){
  238. // summary:
  239. // A function to handle normalizing certain tag types contained under 'node'
  240. // node:
  241. // The node to search from.
  242. // tags:
  243. // Protected.
  244. var w = this.editor.window;
  245. var nodes = dojo.withGlobal(w, function() {
  246. return dojo.query("em,s,strong", node);
  247. });
  248. if(nodes && nodes.length){
  249. dojo.forEach(nodes, function(n){
  250. if(n){
  251. var tag = n.tagName?n.tagName.toLowerCase():"";
  252. var tTag;
  253. switch(tag){
  254. case "s":
  255. tTag = "strike";
  256. break;
  257. case "em":
  258. tTag = "i";
  259. break;
  260. case "strong":
  261. tTag = "b";
  262. break;
  263. }
  264. if(tTag){
  265. var nNode = dojo.withGlobal(w, "create", dojo, [tTag, null, n, "before"] );
  266. while(n.firstChild){
  267. nNode.appendChild(n.firstChild);
  268. }
  269. n.parentNode.removeChild(n);
  270. }
  271. }
  272. });
  273. }
  274. return node;
  275. },
  276. _convertToCss: function(node){
  277. // summary:
  278. // A function to convert the HTML structure of 'node' into
  279. // css span styles around text instead of semantic tags.
  280. // Note: It does not do compression of spans together.
  281. // node: DOMNode
  282. // The node to process
  283. // tags:
  284. // private
  285. if(node){
  286. var w = this.editor.window;
  287. var convertNode = function(cNode) {
  288. if(cNode.nodeType == 1){
  289. if(cNode.id !== "dijitEditorBody"){
  290. var tag = cNode.tagName?cNode.tagName.toLowerCase():"";
  291. if(tag){
  292. var span;
  293. switch(tag){
  294. case "b":
  295. case "strong": // Mainly IE
  296. span = dojo.withGlobal(w, "create", dojo, ["span", {style: {"fontWeight": "bold"}}] );
  297. break;
  298. case "i":
  299. case "em": // Mainly IE
  300. span = dojo.withGlobal(w, "create", dojo, ["span", {style: {"fontStyle": "italic"}}] );
  301. break;
  302. case "u":
  303. span = dojo.withGlobal(w, "create", dojo, ["span", {style: {"textDecoration": "underline"}}] );
  304. break;
  305. case "strike":
  306. case "s": // Mainly WebKit.
  307. span = dojo.withGlobal(w, "create", dojo, ["span", {style: {"textDecoration": "line-through"}}] );
  308. break;
  309. case "font": // Try to deal with colors
  310. var styles = {};
  311. if(dojo.attr(cNode, "color")){
  312. styles.color = dojo.attr(cNode, "color");
  313. }
  314. if(dojo.attr(cNode, "face")){
  315. styles.fontFace = dojo.attr(cNode, "face");
  316. }
  317. if(cNode.style && cNode.style.backgroundColor){
  318. styles.backgroundColor = cNode.style.backgroundColor;
  319. }
  320. if(cNode.style && cNode.style.color){
  321. styles.color = cNode.style.color;
  322. }
  323. var sizeMap = {
  324. 1: "xx-small",
  325. 2: "x-small",
  326. 3: "small",
  327. 4: "medium",
  328. 5: "large",
  329. 6: "x-large",
  330. 7: "xx-large"
  331. };
  332. if(dojo.attr(cNode, "size")){
  333. styles.fontSize = sizeMap[dojo.attr(cNode, "size")];
  334. }
  335. span = dojo.withGlobal(w, "create", dojo, ["span", {style: styles}] );
  336. break;
  337. }
  338. if(span){
  339. while(cNode.firstChild){
  340. span.appendChild(cNode.firstChild);
  341. }
  342. dojo.place(span, cNode, "before");
  343. cNode.parentNode.removeChild(cNode);
  344. cNode = span;
  345. }
  346. }
  347. }
  348. if(cNode.childNodes){
  349. // Clone it, since we may alter its position
  350. var nodes = [];
  351. dojo.forEach(cNode.childNodes, function(n){ nodes.push(n);});
  352. dojo.forEach(nodes, convertNode);
  353. }
  354. }
  355. return cNode;
  356. };
  357. node = convertNode(node);
  358. if(this.condenseSpans){
  359. this._condenseSpans(node);
  360. }
  361. }
  362. return node;
  363. },
  364. _condenseSpans: function(node){
  365. // summary:
  366. // Method to condense spans if you end up with multi-wrapping from
  367. // from converting b, i, u, to span nodes.
  368. // node:
  369. // The node (and its children), to process.
  370. // tags:
  371. // private
  372. var compressSpans = function(node){
  373. // Okay, span with no class or id and it has styles.
  374. // So, merge the styles, then collapse. Merge requires determining
  375. // all the common/different styles and anything that overlaps the style,
  376. // but a different value can't be merged.
  377. var genStyleMap = function(styleText){
  378. var m;
  379. if(styleText){
  380. m = {};
  381. var styles = styleText.toLowerCase().split(";");
  382. dojo.forEach(styles, function(s){
  383. if(s){
  384. var ss = s.split(":");
  385. var key = ss[0] ? dojo.trim(ss[0]): "";
  386. var val = ss[1] ? dojo.trim(ss[1]): "";
  387. if(key && val){
  388. var i;
  389. var nKey = "";
  390. for(i = 0; i < key.length; i++){
  391. var ch = key.charAt(i);
  392. if(ch == "-"){
  393. i++;
  394. ch = key.charAt(i);
  395. nKey += ch.toUpperCase();
  396. }else{
  397. nKey += ch;
  398. }
  399. }
  400. m[nKey] = val;
  401. }
  402. }
  403. });
  404. }
  405. return m;
  406. };
  407. if(node && node.nodeType == 1){
  408. var tag = node.tagName? node.tagName.toLowerCase() : "";
  409. if(tag === "span" && node.childNodes && node.childNodes.length === 1){
  410. // Okay, a possibly compressible span
  411. var c = node.firstChild;
  412. while(c && c.nodeType == 1 && c.tagName && c.tagName.toLowerCase() == "span"){
  413. if(!dojo.attr(c, "class") && !dojo.attr(c, "id") && c.style){
  414. var s1 = genStyleMap(node.style.cssText);
  415. var s2 = genStyleMap(c.style.cssText);
  416. if(s1 && s2){
  417. // Maps, so lets see if we can combine them.
  418. var combinedMap = {};
  419. var i;
  420. for(i in s1){
  421. if(!s1[i] || !s2[i] || s1[i] == s2[i]){
  422. combinedMap[i] = s1[i];
  423. delete s2[i];
  424. }else if(s1[i] != s2[i]){
  425. // Collision, cannot merge.
  426. // IE does not handle combined underline strikethrough text
  427. // decorations on a single span.
  428. if(i == "textDecoration"){
  429. combinedMap[i] = s1[i] + " " + s2[i];
  430. delete s2[i];
  431. }else{
  432. combinedMap = null;
  433. }
  434. break;
  435. }else{
  436. combinedMap = null;
  437. break;
  438. }
  439. }
  440. if(combinedMap){
  441. for(i in s2){
  442. combinedMap[i] = s2[i];
  443. }
  444. dojo.style(node, combinedMap);
  445. while(c.firstChild){
  446. node.appendChild(c.firstChild);
  447. }
  448. var t = c.nextSibling;
  449. c.parentNode.removeChild(c);
  450. c = t;
  451. }else{
  452. c = c.nextSibling;
  453. }
  454. }else{
  455. c = c.nextSibling;
  456. }
  457. }else{
  458. c = c.nextSibling;
  459. }
  460. }
  461. }
  462. }
  463. if(node.childNodes && node.childNodes.length){
  464. dojo.forEach(node.childNodes, compressSpans);
  465. }
  466. };
  467. compressSpans(node);
  468. },
  469. _isInline: function(tag){
  470. // summary:
  471. // Function to determine if the current tag is an inline
  472. // element that does formatting, as we don't want to
  473. // try to combine inlines with divs on styles.
  474. // tag:
  475. // The tag to examine
  476. // tags:
  477. // private
  478. switch(tag){
  479. case "a":
  480. case "b":
  481. case "strong":
  482. case "s":
  483. case "strike":
  484. case "i":
  485. case "u":
  486. case "em":
  487. case "sup":
  488. case "sub":
  489. case "span":
  490. case "font":
  491. case "big":
  492. case "cite":
  493. case "q":
  494. case "img":
  495. case "small":
  496. return true;
  497. default:
  498. return false;
  499. }
  500. },
  501. _inserthtmlImpl: function(html){
  502. // summary:
  503. // Function to trap and over-ride the editor inserthtml implementation
  504. // to try and filter it to match the editor's internal styling mode.
  505. // Helpful for plugins like PasteFromWord, in that it extra-filters
  506. // and normalizes the input if it can.
  507. // html:
  508. // The HTML string to insert.
  509. // tags:
  510. // private
  511. if(html){
  512. var div = this.editor.document.createElement("div");
  513. div.innerHTML = html;
  514. div = this._browserFilter(div);
  515. html = dijit._editor.getChildrenHtml(div);
  516. div.innerHTML = "";
  517. // Call the over-ride, or if not available, just execute it.
  518. if(this.editor._oldInsertHtmlImpl){
  519. return this.editor._oldInsertHtmlImpl(html);
  520. }else{
  521. return this.editor.execCommand("inserthtml", html);
  522. }
  523. }
  524. return false;
  525. }
  526. });
  527. // Register this plugin.
  528. dojo.subscribe(dijit._scopeName + ".Editor.getPlugin",null,function(o){
  529. if(o.plugin){ return; }
  530. var name = o.args.name.toLowerCase();
  531. if(name === "normalizestyle"){
  532. o.plugin = new dojox.editor.plugins.NormalizeStyle({
  533. mode: ("mode" in o.args)?o.args.mode:"semantic",
  534. condenseSpans: ("condenseSpans" in o.args)?o.args.condenseSpans:true
  535. });
  536. }
  537. });
  538. }