editor_plugin_src.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  1. /**
  2. * editor_plugin_src.js
  3. *
  4. * Copyright 2009, Moxiecode Systems AB
  5. * Released under LGPL License.
  6. *
  7. * License: http://tinymce.moxiecode.com/license
  8. * Contributing: http://tinymce.moxiecode.com/contributing
  9. */
  10. (function() {
  11. var each = tinymce.each,
  12. defs = {
  13. paste_auto_cleanup_on_paste : true,
  14. paste_enable_default_filters : true,
  15. paste_block_drop : false,
  16. paste_retain_style_properties : "none",
  17. paste_strip_class_attributes : "mso",
  18. paste_remove_spans : false,
  19. paste_remove_styles : false,
  20. paste_remove_styles_if_webkit : true,
  21. paste_convert_middot_lists : true,
  22. paste_convert_headers_to_strong : false,
  23. paste_dialog_width : "450",
  24. paste_dialog_height : "400",
  25. paste_max_consecutive_linebreaks: 2,
  26. paste_text_use_dialog : false,
  27. paste_text_sticky : false,
  28. paste_text_sticky_default : false,
  29. paste_text_notifyalways : false,
  30. paste_text_linebreaktype : "combined",
  31. paste_text_replacements : [
  32. [/\u2026/g, "..."],
  33. [/[\x93\x94\u201c\u201d]/g, '"'],
  34. [/[\x60\x91\x92\u2018\u2019]/g, "'"]
  35. ]
  36. };
  37. function getParam(ed, name) {
  38. return ed.getParam(name, defs[name]);
  39. }
  40. tinymce.create('tinymce.plugins.PastePlugin', {
  41. init : function(ed, url) {
  42. var t = this;
  43. t.editor = ed;
  44. t.url = url;
  45. // Setup plugin events
  46. t.onPreProcess = new tinymce.util.Dispatcher(t);
  47. t.onPostProcess = new tinymce.util.Dispatcher(t);
  48. // Register default handlers
  49. t.onPreProcess.add(t._preProcess);
  50. t.onPostProcess.add(t._postProcess);
  51. // Register optional preprocess handler
  52. t.onPreProcess.add(function(pl, o) {
  53. ed.execCallback('paste_preprocess', pl, o);
  54. });
  55. // Register optional postprocess
  56. t.onPostProcess.add(function(pl, o) {
  57. ed.execCallback('paste_postprocess', pl, o);
  58. });
  59. ed.onKeyDown.addToTop(function(ed, e) {
  60. // Block ctrl+v from adding an undo level since the default logic in tinymce.Editor will add that
  61. if (((tinymce.isMac ? e.metaKey : e.ctrlKey) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45))
  62. return false; // Stop other listeners
  63. });
  64. // Initialize plain text flag
  65. ed.pasteAsPlainText = getParam(ed, 'paste_text_sticky_default');
  66. // This function executes the process handlers and inserts the contents
  67. // force_rich overrides plain text mode set by user, important for pasting with execCommand
  68. function process(o, force_rich) {
  69. var dom = ed.dom, rng;
  70. // Execute pre process handlers
  71. t.onPreProcess.dispatch(t, o);
  72. // Create DOM structure
  73. o.node = dom.create('div', 0, o.content);
  74. // If pasting inside the same element and the contents is only one block
  75. // remove the block and keep the text since Firefox will copy parts of pre and h1-h6 as a pre element
  76. if (tinymce.isGecko) {
  77. rng = ed.selection.getRng(true);
  78. if (rng.startContainer == rng.endContainer && rng.startContainer.nodeType == 3) {
  79. // Is only one block node and it doesn't contain word stuff
  80. if (o.node.childNodes.length === 1 && /^(p|h[1-6]|pre)$/i.test(o.node.firstChild.nodeName) && o.content.indexOf('__MCE_ITEM__') === -1)
  81. dom.remove(o.node.firstChild, true);
  82. }
  83. }
  84. // Execute post process handlers
  85. t.onPostProcess.dispatch(t, o);
  86. // Serialize content
  87. o.content = ed.serializer.serialize(o.node, {getInner : 1, forced_root_block : ''});
  88. // Plain text option active?
  89. if ((!force_rich) && (ed.pasteAsPlainText)) {
  90. t._insertPlainText(o.content);
  91. if (!getParam(ed, "paste_text_sticky")) {
  92. ed.pasteAsPlainText = false;
  93. ed.controlManager.setActive("pastetext", false);
  94. }
  95. } else {
  96. t._insert(o.content);
  97. }
  98. }
  99. // Add command for external usage
  100. ed.addCommand('mceInsertClipboardContent', function(u, o) {
  101. process(o, true);
  102. });
  103. if (!getParam(ed, "paste_text_use_dialog")) {
  104. ed.addCommand('mcePasteText', function(u, v) {
  105. var cookie = tinymce.util.Cookie;
  106. ed.pasteAsPlainText = !ed.pasteAsPlainText;
  107. ed.controlManager.setActive('pastetext', ed.pasteAsPlainText);
  108. if ((ed.pasteAsPlainText) && (!cookie.get("tinymcePasteText"))) {
  109. if (getParam(ed, "paste_text_sticky")) {
  110. ed.windowManager.alert(ed.translate('paste.plaintext_mode_sticky'));
  111. } else {
  112. ed.windowManager.alert(ed.translate('paste.plaintext_mode'));
  113. }
  114. if (!getParam(ed, "paste_text_notifyalways")) {
  115. cookie.set("tinymcePasteText", "1", new Date(new Date().getFullYear() + 1, 12, 31))
  116. }
  117. }
  118. });
  119. }
  120. ed.addButton('pastetext', {title: 'paste.paste_text_desc', cmd: 'mcePasteText'});
  121. ed.addButton('selectall', {title: 'paste.selectall_desc', cmd: 'selectall'});
  122. // This function grabs the contents from the clipboard by adding a
  123. // hidden div and placing the caret inside it and after the browser paste
  124. // is done it grabs that contents and processes that
  125. function grabContent(e) {
  126. var n, or, rng, oldRng, sel = ed.selection, dom = ed.dom, body = ed.getBody(), posY, textContent;
  127. // Check if browser supports direct plaintext access
  128. if (e.clipboardData || dom.doc.dataTransfer) {
  129. textContent = (e.clipboardData || dom.doc.dataTransfer).getData('Text');
  130. if (ed.pasteAsPlainText) {
  131. e.preventDefault();
  132. process({content : dom.encode(textContent).replace(/\r?\n/g, '<br />')});
  133. return;
  134. }
  135. }
  136. if (dom.get('_mcePaste'))
  137. return;
  138. // Create container to paste into
  139. n = dom.add(body, 'div', {id : '_mcePaste', 'class' : 'mcePaste', 'data-mce-bogus' : '1'}, '\uFEFF\uFEFF');
  140. // If contentEditable mode we need to find out the position of the closest element
  141. if (body != ed.getDoc().body)
  142. posY = dom.getPos(ed.selection.getStart(), body).y;
  143. else
  144. posY = body.scrollTop + dom.getViewPort(ed.getWin()).y;
  145. // Styles needs to be applied after the element is added to the document since WebKit will otherwise remove all styles
  146. // If also needs to be in view on IE or the paste would fail
  147. dom.setStyles(n, {
  148. position : 'absolute',
  149. left : tinymce.isGecko ? -40 : 0, // Need to move it out of site on Gecko since it will othewise display a ghost resize rect for the div
  150. top : posY - 25,
  151. width : 1,
  152. height : 1,
  153. overflow : 'hidden'
  154. });
  155. if (tinymce.isIE) {
  156. // Store away the old range
  157. oldRng = sel.getRng();
  158. // Select the container
  159. rng = dom.doc.body.createTextRange();
  160. rng.moveToElementText(n);
  161. rng.execCommand('Paste');
  162. // Remove container
  163. dom.remove(n);
  164. // Check if the contents was changed, if it wasn't then clipboard extraction failed probably due
  165. // to IE security settings so we pass the junk though better than nothing right
  166. if (n.innerHTML === '\uFEFF\uFEFF') {
  167. ed.execCommand('mcePasteWord');
  168. e.preventDefault();
  169. return;
  170. }
  171. // Restore the old range and clear the contents before pasting
  172. sel.setRng(oldRng);
  173. sel.setContent('');
  174. // For some odd reason we need to detach the the mceInsertContent call from the paste event
  175. // It's like IE has a reference to the parent element that you paste in and the selection gets messed up
  176. // when it tries to restore the selection
  177. setTimeout(function() {
  178. // Process contents
  179. process({content : n.innerHTML});
  180. }, 0);
  181. // Block the real paste event
  182. return tinymce.dom.Event.cancel(e);
  183. } else {
  184. function block(e) {
  185. e.preventDefault();
  186. };
  187. // Block mousedown and click to prevent selection change
  188. dom.bind(ed.getDoc(), 'mousedown', block);
  189. dom.bind(ed.getDoc(), 'keydown', block);
  190. or = ed.selection.getRng();
  191. // Move select contents inside DIV
  192. n = n.firstChild;
  193. rng = ed.getDoc().createRange();
  194. rng.setStart(n, 0);
  195. rng.setEnd(n, 2);
  196. sel.setRng(rng);
  197. // Wait a while and grab the pasted contents
  198. window.setTimeout(function() {
  199. var h = '', nl;
  200. // Paste divs duplicated in paste divs seems to happen when you paste plain text so lets first look for that broken behavior in WebKit
  201. if (!dom.select('div.mcePaste > div.mcePaste').length) {
  202. nl = dom.select('div.mcePaste');
  203. // WebKit will split the div into multiple ones so this will loop through then all and join them to get the whole HTML string
  204. each(nl, function(n) {
  205. var child = n.firstChild;
  206. // WebKit inserts a DIV container with lots of odd styles
  207. if (child && child.nodeName == 'DIV' && child.style.marginTop && child.style.backgroundColor) {
  208. dom.remove(child, 1);
  209. }
  210. // Remove apply style spans
  211. each(dom.select('span.Apple-style-span', n), function(n) {
  212. dom.remove(n, 1);
  213. });
  214. // Remove bogus br elements
  215. each(dom.select('br[data-mce-bogus]', n), function(n) {
  216. dom.remove(n);
  217. });
  218. // WebKit will make a copy of the DIV for each line of plain text pasted and insert them into the DIV
  219. if (n.parentNode.className != 'mcePaste')
  220. h += n.innerHTML;
  221. });
  222. } else {
  223. // Found WebKit weirdness so force the content into paragraphs this seems to happen when you paste plain text from Nodepad etc
  224. // So this logic will replace double enter with paragraphs and single enter with br so it kind of looks the same
  225. h = '<p>' + dom.encode(textContent).replace(/\r?\n\r?\n/g, '</p><p>').replace(/\r?\n/g, '<br />') + '</p>';
  226. }
  227. // Remove the nodes
  228. each(dom.select('div.mcePaste'), function(n) {
  229. dom.remove(n);
  230. });
  231. // Restore the old selection
  232. if (or)
  233. sel.setRng(or);
  234. process({content : h});
  235. // Unblock events ones we got the contents
  236. dom.unbind(ed.getDoc(), 'mousedown', block);
  237. dom.unbind(ed.getDoc(), 'keydown', block);
  238. }, 0);
  239. }
  240. }
  241. // Check if we should use the new auto process method
  242. if (getParam(ed, "paste_auto_cleanup_on_paste")) {
  243. // Is it's Opera or older FF use key handler
  244. if (tinymce.isOpera || /Firefox\/2/.test(navigator.userAgent)) {
  245. ed.onKeyDown.addToTop(function(ed, e) {
  246. if (((tinymce.isMac ? e.metaKey : e.ctrlKey) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45))
  247. grabContent(e);
  248. });
  249. } else {
  250. // Grab contents on paste event on Gecko and WebKit
  251. ed.onPaste.addToTop(function(ed, e) {
  252. return grabContent(e);
  253. });
  254. }
  255. }
  256. ed.onInit.add(function() {
  257. ed.controlManager.setActive("pastetext", ed.pasteAsPlainText);
  258. // Block all drag/drop events
  259. if (getParam(ed, "paste_block_drop")) {
  260. ed.dom.bind(ed.getBody(), ['dragend', 'dragover', 'draggesture', 'dragdrop', 'drop', 'drag'], function(e) {
  261. e.preventDefault();
  262. e.stopPropagation();
  263. return false;
  264. });
  265. }
  266. });
  267. // Add legacy support
  268. t._legacySupport();
  269. },
  270. getInfo : function() {
  271. return {
  272. longname : 'Paste text/word',
  273. author : 'Moxiecode Systems AB',
  274. authorurl : 'http://tinymce.moxiecode.com',
  275. infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/paste',
  276. version : tinymce.majorVersion + "." + tinymce.minorVersion
  277. };
  278. },
  279. _preProcess : function(pl, o) {
  280. var ed = this.editor,
  281. h = o.content,
  282. grep = tinymce.grep,
  283. explode = tinymce.explode,
  284. trim = tinymce.trim,
  285. len, stripClass;
  286. //console.log('Before preprocess:' + o.content);
  287. function process(items) {
  288. each(items, function(v) {
  289. // Remove or replace
  290. if (v.constructor == RegExp)
  291. h = h.replace(v, '');
  292. else
  293. h = h.replace(v[0], v[1]);
  294. });
  295. }
  296. if (ed.settings.paste_enable_default_filters == false) {
  297. return;
  298. }
  299. // IE9 adds BRs before/after block elements when contents is pasted from word or for example another browser
  300. if (tinymce.isIE && document.documentMode >= 9 && /<(h[1-6r]|p|div|address|pre|form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|caption|blockquote|center|dl|dt|dd|dir|fieldset)/.test(o.content)) {
  301. // IE9 adds BRs before/after block elements when contents is pasted from word or for example another browser
  302. process([[/(?:<br>&nbsp;[\s\r\n]+|<br>)*(<\/?(h[1-6r]|p|div|address|pre|form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|caption|blockquote|center|dl|dt|dd|dir|fieldset)[^>]*>)(?:<br>&nbsp;[\s\r\n]+|<br>)*/g, '$1']]);
  303. // IE9 also adds an extra BR element for each soft-linefeed and it also adds a BR for each word wrap break
  304. process([
  305. [/<br><br>/g, '<BR><BR>'], // Replace multiple BR elements with uppercase BR to keep them intact
  306. [/<br>/g, ' '], // Replace single br elements with space since they are word wrap BR:s
  307. [/<BR><BR>/g, '<br>'] // Replace back the double brs but into a single BR
  308. ]);
  309. }
  310. // Detect Word content and process it more aggressive
  311. if (/class="?Mso|style="[^"]*\bmso-|w:WordDocument/i.test(h) || o.wordContent) {
  312. o.wordContent = true; // Mark the pasted contents as word specific content
  313. //console.log('Word contents detected.');
  314. // Process away some basic content
  315. process([
  316. /^\s*(&nbsp;)+/gi, // &nbsp; entities at the start of contents
  317. /(&nbsp;|<br[^>]*>)+\s*$/gi // &nbsp; entities at the end of contents
  318. ]);
  319. if (getParam(ed, "paste_convert_headers_to_strong")) {
  320. h = h.replace(/<p [^>]*class="?MsoHeading"?[^>]*>(.*?)<\/p>/gi, "<p><strong>$1</strong></p>");
  321. }
  322. if (getParam(ed, "paste_convert_middot_lists")) {
  323. process([
  324. [/<!--\[if !supportLists\]-->/gi, '$&__MCE_ITEM__'], // Convert supportLists to a list item marker
  325. [/(<span[^>]+(?:mso-list:|:\s*symbol)[^>]+>)/gi, '$1__MCE_ITEM__'], // Convert mso-list and symbol spans to item markers
  326. [/(<p[^>]+(?:MsoListParagraph)[^>]+>)/gi, '$1__MCE_ITEM__'] // Convert mso-list and symbol paragraphs to item markers (FF)
  327. ]);
  328. }
  329. process([
  330. // Word comments like conditional comments etc
  331. /<!--[\s\S]+?-->/gi,
  332. // Remove comments, scripts (e.g., msoShowComment), XML tag, VML content, MS Office namespaced tags, and a few other tags
  333. /<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,
  334. // Convert <s> into <strike> for line-though
  335. [/<(\/?)s>/gi, "<$1strike>"],
  336. // Replace nsbp entites to char since it's easier to handle
  337. [/&nbsp;/gi, "\u00a0"]
  338. ]);
  339. // Remove bad attributes, with or without quotes, ensuring that attribute text is really inside a tag.
  340. // If JavaScript had a RegExp look-behind, we could have integrated this with the last process() array and got rid of the loop. But alas, it does not, so we cannot.
  341. do {
  342. len = h.length;
  343. h = h.replace(/(<[a-z][^>]*\s)(?:id|name|language|type|on\w+|\w+:\w+)=(?:"[^"]*"|\w+)\s?/gi, "$1");
  344. } while (len != h.length);
  345. // Remove all spans if no styles is to be retained
  346. if (getParam(ed, "paste_retain_style_properties").replace(/^none$/i, "").length == 0) {
  347. h = h.replace(/<\/?span[^>]*>/gi, "");
  348. } else {
  349. // We're keeping styles, so at least clean them up.
  350. // CSS Reference: http://msdn.microsoft.com/en-us/library/aa155477.aspx
  351. process([
  352. // Convert <span style="mso-spacerun:yes">___</span> to string of alternating breaking/non-breaking spaces of same length
  353. [/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi,
  354. function(str, spaces) {
  355. return (spaces.length > 0)? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : "";
  356. }
  357. ],
  358. // Examine all styles: delete junk, transform some, and keep the rest
  359. [/(<[a-z][^>]*)\sstyle="([^"]*)"/gi,
  360. function(str, tag, style) {
  361. var n = [],
  362. i = 0,
  363. s = explode(trim(style).replace(/&quot;/gi, "'"), ";");
  364. // Examine each style definition within the tag's style attribute
  365. each(s, function(v) {
  366. var name, value,
  367. parts = explode(v, ":");
  368. function ensureUnits(v) {
  369. return v + ((v !== "0") && (/\d$/.test(v)))? "px" : "";
  370. }
  371. if (parts.length == 2) {
  372. name = parts[0].toLowerCase();
  373. value = parts[1].toLowerCase();
  374. // Translate certain MS Office styles into their CSS equivalents
  375. switch (name) {
  376. case "mso-padding-alt":
  377. case "mso-padding-top-alt":
  378. case "mso-padding-right-alt":
  379. case "mso-padding-bottom-alt":
  380. case "mso-padding-left-alt":
  381. case "mso-margin-alt":
  382. case "mso-margin-top-alt":
  383. case "mso-margin-right-alt":
  384. case "mso-margin-bottom-alt":
  385. case "mso-margin-left-alt":
  386. case "mso-table-layout-alt":
  387. case "mso-height":
  388. case "mso-width":
  389. case "mso-vertical-align-alt":
  390. n[i++] = name.replace(/^mso-|-alt$/g, "") + ":" + ensureUnits(value);
  391. return;
  392. case "horiz-align":
  393. n[i++] = "text-align:" + value;
  394. return;
  395. case "vert-align":
  396. n[i++] = "vertical-align:" + value;
  397. return;
  398. case "font-color":
  399. case "mso-foreground":
  400. n[i++] = "color:" + value;
  401. return;
  402. case "mso-background":
  403. case "mso-highlight":
  404. n[i++] = "background:" + value;
  405. return;
  406. case "mso-default-height":
  407. n[i++] = "min-height:" + ensureUnits(value);
  408. return;
  409. case "mso-default-width":
  410. n[i++] = "min-width:" + ensureUnits(value);
  411. return;
  412. case "mso-padding-between-alt":
  413. n[i++] = "border-collapse:separate;border-spacing:" + ensureUnits(value);
  414. return;
  415. case "text-line-through":
  416. if ((value == "single") || (value == "double")) {
  417. n[i++] = "text-decoration:line-through";
  418. }
  419. return;
  420. case "mso-zero-height":
  421. if (value == "yes") {
  422. n[i++] = "display:none";
  423. }
  424. return;
  425. }
  426. // Eliminate all MS Office style definitions that have no CSS equivalent by examining the first characters in the name
  427. if (/^(mso|column|font-emph|lang|layout|line-break|list-image|nav|panose|punct|row|ruby|sep|size|src|tab-|table-border|text-(?!align|decor|indent|trans)|top-bar|version|vnd|word-break)/.test(name)) {
  428. return;
  429. }
  430. // If it reached this point, it must be a valid CSS style
  431. n[i++] = name + ":" + parts[1]; // Lower-case name, but keep value case
  432. }
  433. });
  434. // If style attribute contained any valid styles the re-write it; otherwise delete style attribute.
  435. if (i > 0) {
  436. return tag + ' style="' + n.join(';') + '"';
  437. } else {
  438. return tag;
  439. }
  440. }
  441. ]
  442. ]);
  443. }
  444. }
  445. // Replace headers with <strong>
  446. if (getParam(ed, "paste_convert_headers_to_strong")) {
  447. process([
  448. [/<h[1-6][^>]*>/gi, "<p><strong>"],
  449. [/<\/h[1-6][^>]*>/gi, "</strong></p>"]
  450. ]);
  451. }
  452. process([
  453. // Copy paste from Java like Open Office will produce this junk on FF
  454. [/Version:[\d.]+\nStartHTML:\d+\nEndHTML:\d+\nStartFragment:\d+\nEndFragment:\d+/gi, '']
  455. ]);
  456. // Class attribute options are: leave all as-is ("none"), remove all ("all"), or remove only those starting with mso ("mso").
  457. // Note:- paste_strip_class_attributes: "none", verify_css_classes: true is also a good variation.
  458. stripClass = getParam(ed, "paste_strip_class_attributes");
  459. if (stripClass !== "none") {
  460. function removeClasses(match, g1) {
  461. if (stripClass === "all")
  462. return '';
  463. var cls = grep(explode(g1.replace(/^(["'])(.*)\1$/, "$2"), " "),
  464. function(v) {
  465. return (/^(?!mso)/i.test(v));
  466. }
  467. );
  468. return cls.length ? ' class="' + cls.join(" ") + '"' : '';
  469. };
  470. h = h.replace(/ class="([^"]+)"/gi, removeClasses);
  471. h = h.replace(/ class=([\-\w]+)/gi, removeClasses);
  472. }
  473. // Remove spans option
  474. if (getParam(ed, "paste_remove_spans")) {
  475. h = h.replace(/<\/?span[^>]*>/gi, "");
  476. }
  477. //console.log('After preprocess:' + h);
  478. o.content = h;
  479. },
  480. /**
  481. * Various post process items.
  482. */
  483. _postProcess : function(pl, o) {
  484. var t = this, ed = t.editor, dom = ed.dom, styleProps;
  485. if (ed.settings.paste_enable_default_filters == false) {
  486. return;
  487. }
  488. if (o.wordContent) {
  489. // Remove named anchors or TOC links
  490. each(dom.select('a', o.node), function(a) {
  491. if (!a.href || a.href.indexOf('#_Toc') != -1)
  492. dom.remove(a, 1);
  493. });
  494. if (getParam(ed, "paste_convert_middot_lists")) {
  495. t._convertLists(pl, o);
  496. }
  497. // Process styles
  498. styleProps = getParam(ed, "paste_retain_style_properties"); // retained properties
  499. // Process only if a string was specified and not equal to "all" or "*"
  500. if ((tinymce.is(styleProps, "string")) && (styleProps !== "all") && (styleProps !== "*")) {
  501. styleProps = tinymce.explode(styleProps.replace(/^none$/i, ""));
  502. // Retains some style properties
  503. each(dom.select('*', o.node), function(el) {
  504. var newStyle = {}, npc = 0, i, sp, sv;
  505. // Store a subset of the existing styles
  506. if (styleProps) {
  507. for (i = 0; i < styleProps.length; i++) {
  508. sp = styleProps[i];
  509. sv = dom.getStyle(el, sp);
  510. if (sv) {
  511. newStyle[sp] = sv;
  512. npc++;
  513. }
  514. }
  515. }
  516. // Remove all of the existing styles
  517. dom.setAttrib(el, 'style', '');
  518. if (styleProps && npc > 0)
  519. dom.setStyles(el, newStyle); // Add back the stored subset of styles
  520. else // Remove empty span tags that do not have class attributes
  521. if (el.nodeName == 'SPAN' && !el.className)
  522. dom.remove(el, true);
  523. });
  524. }
  525. }
  526. // Remove all style information or only specifically on WebKit to avoid the style bug on that browser
  527. if (getParam(ed, "paste_remove_styles") || (getParam(ed, "paste_remove_styles_if_webkit") && tinymce.isWebKit)) {
  528. each(dom.select('*[style]', o.node), function(el) {
  529. el.removeAttribute('style');
  530. el.removeAttribute('data-mce-style');
  531. });
  532. } else {
  533. if (tinymce.isWebKit) {
  534. // We need to compress the styles on WebKit since if you paste <img border="0" /> it will become <img border="0" style="... lots of junk ..." />
  535. // Removing the mce_style that contains the real value will force the Serializer engine to compress the styles
  536. each(dom.select('*', o.node), function(el) {
  537. el.removeAttribute('data-mce-style');
  538. });
  539. }
  540. }
  541. },
  542. /**
  543. * Converts the most common bullet and number formats in Office into a real semantic UL/LI list.
  544. */
  545. _convertLists : function(pl, o) {
  546. var dom = pl.editor.dom, listElm, li, lastMargin = -1, margin, levels = [], lastType, html;
  547. // Convert middot lists into real semantic lists
  548. each(dom.select('p', o.node), function(p) {
  549. var sib, val = '', type, html, idx, parents;
  550. // Get text node value at beginning of paragraph
  551. for (sib = p.firstChild; sib && sib.nodeType == 3; sib = sib.nextSibling)
  552. val += sib.nodeValue;
  553. val = p.innerHTML.replace(/<\/?\w+[^>]*>/gi, '').replace(/&nbsp;/g, '\u00a0');
  554. // Detect unordered lists look for bullets
  555. if (/^(__MCE_ITEM__)+[\u2022\u00b7\u00a7\u00d8o\u25CF]\s*\u00a0*/.test(val))
  556. type = 'ul';
  557. // Detect ordered lists 1., a. or ixv.
  558. if (/^__MCE_ITEM__\s*\w+\.\s*\u00a0+/.test(val))
  559. type = 'ol';
  560. // Check if node value matches the list pattern: o&nbsp;&nbsp;
  561. if (type) {
  562. margin = parseFloat(p.style.marginLeft || 0);
  563. if (margin > lastMargin)
  564. levels.push(margin);
  565. if (!listElm || type != lastType) {
  566. listElm = dom.create(type);
  567. dom.insertAfter(listElm, p);
  568. } else {
  569. // Nested list element
  570. if (margin > lastMargin) {
  571. listElm = li.appendChild(dom.create(type));
  572. } else if (margin < lastMargin) {
  573. // Find parent level based on margin value
  574. idx = tinymce.inArray(levels, margin);
  575. parents = dom.getParents(listElm.parentNode, type);
  576. listElm = parents[parents.length - 1 - idx] || listElm;
  577. }
  578. }
  579. // Remove middot or number spans if they exists
  580. each(dom.select('span', p), function(span) {
  581. var html = span.innerHTML.replace(/<\/?\w+[^>]*>/gi, '');
  582. // Remove span with the middot or the number
  583. if (type == 'ul' && /^__MCE_ITEM__[\u2022\u00b7\u00a7\u00d8o\u25CF]/.test(html))
  584. dom.remove(span);
  585. else if (/^__MCE_ITEM__[\s\S]*\w+\.(&nbsp;|\u00a0)*\s*/.test(html))
  586. dom.remove(span);
  587. });
  588. html = p.innerHTML;
  589. // Remove middot/list items
  590. if (type == 'ul')
  591. html = p.innerHTML.replace(/__MCE_ITEM__/g, '').replace(/^[\u2022\u00b7\u00a7\u00d8o\u25CF]\s*(&nbsp;|\u00a0)+\s*/, '');
  592. else
  593. html = p.innerHTML.replace(/__MCE_ITEM__/g, '').replace(/^\s*\w+\.(&nbsp;|\u00a0)+\s*/, '');
  594. // Create li and add paragraph data into the new li
  595. li = listElm.appendChild(dom.create('li', 0, html));
  596. dom.remove(p);
  597. lastMargin = margin;
  598. lastType = type;
  599. } else
  600. listElm = lastMargin = 0; // End list element
  601. });
  602. // Remove any left over makers
  603. html = o.node.innerHTML;
  604. if (html.indexOf('__MCE_ITEM__') != -1)
  605. o.node.innerHTML = html.replace(/__MCE_ITEM__/g, '');
  606. },
  607. /**
  608. * Inserts the specified contents at the caret position.
  609. */
  610. _insert : function(h, skip_undo) {
  611. var ed = this.editor, r = ed.selection.getRng();
  612. // First delete the contents seems to work better on WebKit when the selection spans multiple list items or multiple table cells.
  613. if (!ed.selection.isCollapsed() && r.startContainer != r.endContainer)
  614. ed.getDoc().execCommand('Delete', false, null);
  615. ed.execCommand('mceInsertContent', false, h, {skip_undo : skip_undo});
  616. },
  617. /**
  618. * Instead of the old plain text method which tried to re-create a paste operation, the
  619. * new approach adds a plain text mode toggle switch that changes the behavior of paste.
  620. * This function is passed the same input that the regular paste plugin produces.
  621. * It performs additional scrubbing and produces (and inserts) the plain text.
  622. * This approach leverages all of the great existing functionality in the paste
  623. * plugin, and requires minimal changes to add the new functionality.
  624. * Speednet - June 2009
  625. */
  626. _insertPlainText : function(content) {
  627. var ed = this.editor,
  628. linebr = getParam(ed, "paste_text_linebreaktype"),
  629. rl = getParam(ed, "paste_text_replacements"),
  630. is = tinymce.is;
  631. function process(items) {
  632. each(items, function(v) {
  633. if (v.constructor == RegExp)
  634. content = content.replace(v, "");
  635. else
  636. content = content.replace(v[0], v[1]);
  637. });
  638. };
  639. if ((typeof(content) === "string") && (content.length > 0)) {
  640. // If HTML content with line-breaking tags, then remove all cr/lf chars because only tags will break a line
  641. if (/<(?:p|br|h[1-6]|ul|ol|dl|table|t[rdh]|div|blockquote|fieldset|pre|address|center)[^>]*>/i.test(content)) {
  642. process([
  643. /[\n\r]+/g
  644. ]);
  645. } else {
  646. // Otherwise just get rid of carriage returns (only need linefeeds)
  647. process([
  648. /\r+/g
  649. ]);
  650. }
  651. process([
  652. [/<\/(?:p|h[1-6]|ul|ol|dl|table|div|blockquote|fieldset|pre|address|center)>/gi, "\n\n"], // Block tags get a blank line after them
  653. [/<br[^>]*>|<\/tr>/gi, "\n"], // Single linebreak for <br /> tags and table rows
  654. [/<\/t[dh]>\s*<t[dh][^>]*>/gi, "\t"], // Table cells get tabs betweem them
  655. /<[a-z!\/?][^>]*>/gi, // Delete all remaining tags
  656. [/&nbsp;/gi, " "], // Convert non-break spaces to regular spaces (remember, *plain text*)
  657. [/(?:(?!\n)\s)*(\n+)(?:(?!\n)\s)*/gi, "$1"] // Cool little RegExp deletes whitespace around linebreak chars.
  658. ]);
  659. var maxLinebreaks = Number(getParam(ed, "paste_max_consecutive_linebreaks"));
  660. if (maxLinebreaks > -1) {
  661. var maxLinebreaksRegex = new RegExp("\n{" + (maxLinebreaks + 1) + ",}", "g");
  662. var linebreakReplacement = "";
  663. while (linebreakReplacement.length < maxLinebreaks) {
  664. linebreakReplacement += "\n";
  665. }
  666. process([
  667. [maxLinebreaksRegex, linebreakReplacement] // Limit max consecutive linebreaks
  668. ]);
  669. }
  670. content = ed.dom.decode(tinymce.html.Entities.encodeRaw(content));
  671. // Perform default or custom replacements
  672. if (is(rl, "array")) {
  673. process(rl);
  674. } else if (is(rl, "string")) {
  675. process(new RegExp(rl, "gi"));
  676. }
  677. // Treat paragraphs as specified in the config
  678. if (linebr == "none") {
  679. // Convert all line breaks to space
  680. process([
  681. [/\n+/g, " "]
  682. ]);
  683. } else if (linebr == "br") {
  684. // Convert all line breaks to <br />
  685. process([
  686. [/\n/g, "<br />"]
  687. ]);
  688. } else if (linebr == "p") {
  689. // Convert all line breaks to <p>...</p>
  690. process([
  691. [/\n+/g, "</p><p>"],
  692. [/^(.*<\/p>)(<p>)$/, '<p>$1']
  693. ]);
  694. } else {
  695. // defaults to "combined"
  696. // Convert single line breaks to <br /> and double line breaks to <p>...</p>
  697. process([
  698. [/\n\n/g, "</p><p>"],
  699. [/^(.*<\/p>)(<p>)$/, '<p>$1'],
  700. [/\n/g, "<br />"]
  701. ]);
  702. }
  703. ed.execCommand('mceInsertContent', false, content);
  704. }
  705. },
  706. /**
  707. * This method will open the old style paste dialogs. Some users might want the old behavior but still use the new cleanup engine.
  708. */
  709. _legacySupport : function() {
  710. var t = this, ed = t.editor;
  711. // Register command(s) for backwards compatibility
  712. ed.addCommand("mcePasteWord", function() {
  713. ed.windowManager.open({
  714. file: t.url + "/pasteword.htm",
  715. width: parseInt(getParam(ed, "paste_dialog_width")),
  716. height: parseInt(getParam(ed, "paste_dialog_height")),
  717. inline: 1
  718. });
  719. });
  720. if (getParam(ed, "paste_text_use_dialog")) {
  721. ed.addCommand("mcePasteText", function() {
  722. ed.windowManager.open({
  723. file : t.url + "/pastetext.htm",
  724. width: parseInt(getParam(ed, "paste_dialog_width")),
  725. height: parseInt(getParam(ed, "paste_dialog_height")),
  726. inline : 1
  727. });
  728. });
  729. }
  730. // Register button for backwards compatibility
  731. ed.addButton("pasteword", {title : "paste.paste_word_desc", cmd : "mcePasteWord"});
  732. }
  733. });
  734. // Register plugin
  735. tinymce.PluginManager.add("paste", tinymce.plugins.PastePlugin);
  736. })();