NormalizeStyle.js 16 KB

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