made a copy
[atutor.git] / jscripts / tiny_mce / plugins / paste / editor_plugin_src.js
1 /**\r
2  * $Id: editor_plugin_src.js 1134 2009-05-21 12:48:25Z spocke $\r
3  *\r
4  * @author Moxiecode\r
5  * @copyright Copyright © 2004-2008, Moxiecode Systems AB, All rights reserved.\r
6  */\r
7 \r
8 (function() {\r
9         var each = tinymce.each;\r
10 \r
11         tinymce.create('tinymce.plugins.PastePlugin', {\r
12                 init : function(ed, url) {\r
13                         var t = this, cb;\r
14 \r
15                         t.editor = ed;\r
16                         t.url = url;\r
17 \r
18                         // Setup plugin events\r
19                         t.onPreProcess = new tinymce.util.Dispatcher(t);\r
20                         t.onPostProcess = new tinymce.util.Dispatcher(t);\r
21 \r
22                         // Register default handlers\r
23                         t.onPreProcess.add(t._preProcess);\r
24                         t.onPostProcess.add(t._postProcess);\r
25 \r
26                         // Register optional preprocess handler\r
27                         t.onPreProcess.add(function(pl, o) {\r
28                                 ed.execCallback('paste_preprocess', pl, o);\r
29                         });\r
30 \r
31                         // Register optional postprocess\r
32                         t.onPostProcess.add(function(pl, o) {\r
33                                 ed.execCallback('paste_postprocess', pl, o);\r
34                         });\r
35 \r
36                         // This function executes the process handlers and inserts the contents\r
37                         function process(h) {\r
38                                 var dom = ed.dom, o = {content : h};\r
39 \r
40                                 // Execute pre process handlers\r
41                                 t.onPreProcess.dispatch(t, o);\r
42 \r
43                                 // Create DOM structure\r
44                                 o.node = dom.create('div', 0, o.content);\r
45 \r
46                                 // Execute post process handlers\r
47                                 t.onPostProcess.dispatch(t, o);\r
48 \r
49                                 // Serialize content\r
50                                 o.content = ed.serializer.serialize(o.node, {getInner : 1});\r
51 \r
52                                 //  Insert cleaned content. We need to handle insertion of contents containing block elements separately\r
53                                 if (/<(p|h[1-6]|ul|ol)/.test(o.content))\r
54                                         t._insertBlockContent(ed, dom, o.content);\r
55                                 else\r
56                                         t._insert(o.content);\r
57                         };\r
58 \r
59                         // Add command for external usage\r
60                         ed.addCommand('mceInsertClipboardContent', function(u, v) {\r
61                                 process(v);\r
62                         });\r
63 \r
64                         // This function grabs the contents from the clipboard by adding a\r
65                         // hidden div and placing the caret inside it and after the browser paste\r
66                         // is done it grabs that contents and processes that\r
67                         function grabContent(e) {\r
68                                 var n, or, rng, sel = ed.selection, dom = ed.dom, body = ed.getBody(), posY;\r
69 \r
70                                 if (dom.get('_mcePaste'))\r
71                                         return;\r
72 \r
73                                 // Create container to paste into\r
74                                 n = dom.add(body, 'div', {id : '_mcePaste'}, '&nbsp;');\r
75 \r
76                                 // If contentEditable mode we need to find out the position of the closest element\r
77                                 if (body != ed.getDoc().body)\r
78                                         posY = dom.getPos(ed.selection.getStart(), body).y;\r
79                                 else\r
80                                         posY = body.scrollTop;\r
81 \r
82                                 // Styles needs to be applied after the element is added to the document since WebKit will otherwise remove all styles\r
83                                 dom.setStyles(n, {\r
84                                         position : 'absolute',\r
85                                         left : -10000,\r
86                                         top : posY,\r
87                                         width : 1,\r
88                                         height : 1,\r
89                                         overflow : 'hidden'\r
90                                 });\r
91 \r
92                                 if (tinymce.isIE) {\r
93                                         // Select the container\r
94                                         rng = dom.doc.body.createTextRange();\r
95                                         rng.moveToElementText(n);\r
96                                         rng.execCommand('Paste');\r
97 \r
98                                         // Remove container\r
99                                         dom.remove(n);\r
100 \r
101                                         // Process contents\r
102                                         process(n.innerHTML);\r
103 \r
104                                         return tinymce.dom.Event.cancel(e);\r
105                                 } else {\r
106                                         or = ed.selection.getRng();\r
107 \r
108                                         // Move caret into hidden div\r
109                                         n = n.firstChild;\r
110                                         rng = ed.getDoc().createRange();\r
111                                         rng.setStart(n, 0);\r
112                                         rng.setEnd(n, 1);\r
113                                         sel.setRng(rng);\r
114 \r
115                                         // Wait a while and grab the pasted contents\r
116                                         window.setTimeout(function() {\r
117                                                 var n = dom.get('_mcePaste'), h;\r
118 \r
119                                                 // Webkit clones the _mcePaste div for some odd reason so this will ensure that we get the real new div not the old empty one\r
120                                                 n.id = '_mceRemoved';\r
121                                                 dom.remove(n);\r
122                                                 n = dom.get('_mcePaste') || n;\r
123 \r
124                                                 // Grab the HTML contents\r
125                                                 // We need to look for a apple style wrapper on webkit it also adds a div wrapper if you copy/paste the body of the editor\r
126                                                 // It's amazing how strange the contentEditable mode works in WebKit\r
127                                                 h = (dom.select('> span.Apple-style-span div', n)[0] || dom.select('> span.Apple-style-span', n)[0] || n).innerHTML;\r
128 \r
129                                                 // Remove hidden div and restore selection\r
130                                                 dom.remove(n);\r
131 \r
132                                                 // Restore the old selection\r
133                                                 if (or)\r
134                                                         sel.setRng(or);\r
135 \r
136                                                 process(h);\r
137                                         }, 0);\r
138                                 }\r
139                         };\r
140 \r
141                         // Check if we should use the new auto process method                   \r
142                         if (ed.getParam('paste_auto_cleanup_on_paste', true)) {\r
143                                 // Is it's Opera or older FF use key handler\r
144                                 if (tinymce.isOpera || /Firefox\/2/.test(navigator.userAgent)) {\r
145                                         ed.onKeyDown.add(function(ed, e) {\r
146                                                 if (((tinymce.isMac ? e.metaKey : e.ctrlKey) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45))\r
147                                                         grabContent(e);\r
148                                         });\r
149                                 } else {\r
150                                         // Grab contents on paste event on Gecko and WebKit\r
151                                         ed.onPaste.addToTop(function(ed, e) {\r
152                                                 return grabContent(e);\r
153                                         });\r
154                                 }\r
155                         }\r
156 \r
157                         // Block all drag/drop events\r
158                         if (ed.getParam('paste_block_drop')) {\r
159                                 ed.onInit.add(function() {\r
160                                         ed.dom.bind(ed.getBody(), ['dragend', 'dragover', 'draggesture', 'dragdrop', 'drop', 'drag'], function(e) {\r
161                                                 e.preventDefault();\r
162                                                 e.stopPropagation();\r
163 \r
164                                                 return false;\r
165                                         });\r
166                                 });\r
167                         }\r
168 \r
169                         // Add legacy support\r
170                         t._legacySupport();\r
171                 },\r
172 \r
173                 getInfo : function() {\r
174                         return {\r
175                                 longname : 'Paste text/word',\r
176                                 author : 'Moxiecode Systems AB',\r
177                                 authorurl : 'http://tinymce.moxiecode.com',\r
178                                 infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/paste',\r
179                                 version : tinymce.majorVersion + "." + tinymce.minorVersion\r
180                         };\r
181                 },\r
182 \r
183                 _preProcess : function(pl, o) {\r
184                         var ed = this.editor, h = o.content, process, stripClass;\r
185 \r
186                         //console.log('Before preprocess:' + o.content);\r
187 \r
188                         function process(items) {\r
189                                 each(items, function(v) {\r
190                                         // Remove or replace\r
191                                         if (v.constructor == RegExp)\r
192                                                 h = h.replace(v, '');\r
193                                         else\r
194                                                 h = h.replace(v[0], v[1]);\r
195                                 });\r
196                         };\r
197 \r
198                         // Process away some basic content\r
199                         process([\r
200                                 /^\s*(&nbsp;)+/g,                                                                                       // nbsp entities at the start of contents\r
201                                 /(&nbsp;|<br[^>]*>)+\s*$/g                                                                      // nbsp entities at the end of contents\r
202                         ]);\r
203 \r
204                         // Detect Word content and process it more aggressive\r
205                         if (/(class=\"?Mso|style=\"[^\"]*\bmso\-|w:WordDocument)/.test(h)) {\r
206                                 o.wordContent = true; // Mark the pasted contents as word specific content\r
207                                 //console.log('Word contents detected.');\r
208 \r
209                                 if (ed.getParam('paste_convert_middot_lists', true)) {\r
210                                         process([\r
211                                                 [/<!--\[if !supportLists\]-->/gi, '$&__MCE_ITEM__'],                    // Convert supportLists to a list item marker\r
212                                                 [/(<span[^>]+:\s*symbol[^>]+>)/gi, '$1__MCE_ITEM__'],                           // Convert symbol spans to list items\r
213                                                 [/(<span[^>]+mso-list:[^>]+>)/gi, '$1__MCE_ITEM__']                             // Convert mso-list to item marker\r
214                                         ]);\r
215                                 }\r
216 \r
217                                 process([\r
218                                         /<!--[\s\S]+?-->/gi,                                                                                            // Word comments\r
219                                         /<\/?(img|font|meta|link|style|div|v:\w+)[^>]*>/gi,                                     // Remove some tags including VML content\r
220                                         /<\\?\?xml[^>]*>/gi,                                                                                            // XML namespace declarations\r
221                                         /<\/?o:[^>]*>/gi,                                                                                                       // MS namespaced elements <o:tag>\r
222                                         / (id|name|language|type|on\w+|v:\w+)=\"([^\"]*)\"/gi,                          // on.., class, style and language attributes with quotes\r
223                                         / (id|name|language|type|on\w+|v:\w+)=(\w+)/gi,                                         // on.., class, style and language attributes without quotes (IE)\r
224                                         [/<(\/?)s>/gi, '<$1strike>'],                                                                           // Convert <s> into <strike> for line-though\r
225                                         /<script[^>]+>[\s\S]*?<\/script>/gi,                                                            // All scripts elements for msoShowComment for example\r
226                                         [/&nbsp;/g, '\u00a0']                                                                                           // Replace nsbp entites to char since it's easier to handle\r
227                                 ]);\r
228 \r
229                                 // Remove all spans if no styles is to be retained\r
230                                 if (!ed.getParam('paste_retain_style_properties')) {\r
231                                         process([\r
232                                                 /<\/?(span)[^>]*>/gi\r
233                                         ]);\r
234                                 }\r
235                         }\r
236 \r
237                         // Allow for class names to be retained if desired; either all, or just the ones from Word\r
238                         // Note that the paste_strip_class_attributes: 'none, verify_css_classes: true is also a good variation.\r
239                         stripClass = ed.getParam('paste_strip_class_attributes', 'all');\r
240                         if (stripClass != 'none') {\r
241                                 if (stripClass == 'all') {\r
242                                         process([\r
243                                                 / class=\"([^\"]*)\"/gi,        // class attributes with quotes\r
244                                                 / class=(\w+)/gi                        // class attributes without quotes (IE)\r
245                                         ]);\r
246                                 } else { // Only strip the 'mso*' classes\r
247                                         process([\r
248                                                 / class=\"(mso[^\"]*)\"/gi,     // class attributes with quotes\r
249                                                 / class=(mso\w+)/gi                     // class attributes without quotes (IE)\r
250                                         ]);\r
251                                 }\r
252                         }\r
253 \r
254                         // Remove spans option\r
255                         if (ed.getParam('paste_remove_spans')) {\r
256                                 process([\r
257                                         /<\/?(span)[^>]*>/gi\r
258                                 ]);\r
259                         }\r
260 \r
261                         //console.log('After preprocess:' + h);\r
262 \r
263                         o.content = h;\r
264                 },\r
265 \r
266                 /**\r
267                  * Various post process items.\r
268                  */\r
269                 _postProcess : function(pl, o) {\r
270                         var t = this, ed = t.editor, dom = ed.dom, styleProps;\r
271 \r
272                         if (o.wordContent) {\r
273                                 // Remove named anchors or TOC links\r
274                                 each(dom.select('a', o.node), function(a) {\r
275                                         if (!a.href || a.href.indexOf('#_Toc') != -1)\r
276                                                 dom.remove(a, 1);\r
277                                 });\r
278 \r
279                                 if (t.editor.getParam('paste_convert_middot_lists', true))\r
280                                         t._convertLists(pl, o);\r
281 \r
282                                 // Process styles\r
283                                 styleProps = ed.getParam('paste_retain_style_properties'); // retained properties\r
284 \r
285                                 // If string property then split it\r
286                                 if (tinymce.is(styleProps, 'string'))\r
287                                         styleProps = tinymce.explode(styleProps);\r
288 \r
289                                 // Retains some style properties\r
290                                 each(dom.select('*', o.node), function(el) {\r
291                                         var newStyle = {}, npc = 0, i, sp, sv;\r
292 \r
293                                         // Store a subset of the existing styles\r
294                                         if (styleProps) {\r
295                                                 for (i = 0; i < styleProps.length; i++) {\r
296                                                         sp = styleProps[i];\r
297                                                         sv = dom.getStyle(el, sp);\r
298 \r
299                                                         if (sv) {\r
300                                                                 newStyle[sp] = sv;\r
301                                                                 npc++;\r
302                                                         }\r
303                                                 }\r
304                                         }\r
305 \r
306                                         // Remove all of the existing styles\r
307                                         dom.setAttrib(el, 'style', '');\r
308 \r
309                                         if (styleProps && npc > 0)\r
310                                                 dom.setStyles(el, newStyle); // Add back the stored subset of styles\r
311                                         else // Remove empty span tags that do not have class attributes\r
312                                                 if (el.nodeName == 'SPAN' && !el.className)\r
313                                                         dom.remove(el, true);\r
314                                 });\r
315                         }\r
316 \r
317                         // Remove all style information or only specifically on WebKit to avoid the style bug on that browser\r
318                         if (ed.getParam("paste_remove_styles") || (ed.getParam("paste_remove_styles_if_webkit") && tinymce.isWebKit)) {\r
319                                 each(dom.select('*[style]', o.node), function(el) {\r
320                                         el.removeAttribute('style');\r
321                                         el.removeAttribute('mce_style');\r
322                                 });\r
323                         } else {\r
324                                 if (tinymce.isWebKit) {\r
325                                         // 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 ..." />\r
326                                         // Removing the mce_style that contains the real value will force the Serializer engine to compress the styles\r
327                                         each(dom.select('*', o.node), function(el) {\r
328                                                 el.removeAttribute('mce_style');\r
329                                         });\r
330                                 }\r
331                         }\r
332                 },\r
333 \r
334                 /**\r
335                  * Converts the most common bullet and number formats in Office into a real semantic UL/LI list.\r
336                  */\r
337                 _convertLists : function(pl, o) {\r
338                         var dom = pl.editor.dom, listElm, li, lastMargin = -1, margin, levels = [], lastType, html;\r
339 \r
340                         // Convert middot lists into real semantic lists\r
341                         each(dom.select('p', o.node), function(p) {\r
342                                 var sib, val = '', type, html, idx, parents;\r
343 \r
344                                 // Get text node value at beginning of paragraph\r
345                                 for (sib = p.firstChild; sib && sib.nodeType == 3; sib = sib.nextSibling)\r
346                                         val += sib.nodeValue;\r
347 \r
348                                 val = p.innerHTML.replace(/<\/?\w+[^>]*>/gi, '').replace(/&nbsp;/g, '\u00a0');\r
349 \r
350                                 // Detect unordered lists look for bullets\r
351                                 if (/^(__MCE_ITEM__)+[\u2022\u00b7\u00a7\u00d8o]\s*\u00a0*/.test(val))\r
352                                         type = 'ul';\r
353 \r
354                                 // Detect ordered lists 1., a. or ixv.\r
355                                 if (/^__MCE_ITEM__\s*\w+\.\s*\u00a0{2,}/.test(val))\r
356                                         type = 'ol';\r
357 \r
358                                 // Check if node value matches the list pattern: o&nbsp;&nbsp;\r
359                                 if (type) {\r
360                                         margin = parseFloat(p.style.marginLeft || 0);\r
361 \r
362                                         if (margin > lastMargin)\r
363                                                 levels.push(margin);\r
364 \r
365                                         if (!listElm || type != lastType) {\r
366                                                 listElm = dom.create(type);\r
367                                                 dom.insertAfter(listElm, p);\r
368                                         } else {\r
369                                                 // Nested list element\r
370                                                 if (margin > lastMargin) {\r
371                                                         listElm = li.appendChild(dom.create(type));\r
372                                                 } else if (margin < lastMargin) {\r
373                                                         // Find parent level based on margin value\r
374                                                         idx = tinymce.inArray(levels, margin);\r
375                                                         parents = dom.getParents(listElm.parentNode, type);\r
376                                                         listElm = parents[parents.length - 1 - idx] || listElm;\r
377                                                 }\r
378                                         }\r
379 \r
380                                         // Remove middot or number spans if they exists\r
381                                         each(dom.select('span', p), function(span) {\r
382                                                 var html = span.innerHTML.replace(/<\/?\w+[^>]*>/gi, '');\r
383 \r
384                                                 // Remove span with the middot or the number\r
385                                                 if (type == 'ul' && /^[\u2022\u00b7\u00a7\u00d8o]/.test(html))\r
386                                                         dom.remove(span);\r
387                                                 else if (/^[\s\S]*\w+\.(&nbsp;|\u00a0)*\s*/.test(html))\r
388                                                         dom.remove(span);\r
389                                         });\r
390 \r
391                                         html = p.innerHTML;\r
392 \r
393                                         // Remove middot/list items\r
394                                         if (type == 'ul')\r
395                                                 html = p.innerHTML.replace(/__MCE_ITEM__/g, '').replace(/^[\u2022\u00b7\u00a7\u00d8o]\s*(&nbsp;|\u00a0)+\s*/, '');\r
396                                         else\r
397                                                 html = p.innerHTML.replace(/__MCE_ITEM__/g, '').replace(/^\s*\w+\.(&nbsp;|\u00a0)+\s*/, '');\r
398 \r
399                                         // Create li and add paragraph data into the new li\r
400                                         li = listElm.appendChild(dom.create('li', 0, html));\r
401                                         dom.remove(p);\r
402 \r
403                                         lastMargin = margin;\r
404                                         lastType = type;\r
405                                 } else\r
406                                         listElm = lastMargin = 0; // End list element\r
407                         });\r
408 \r
409                         // Remove any left over makers\r
410                         html = o.node.innerHTML;\r
411                         if (html.indexOf('__MCE_ITEM__') != -1)\r
412                                 o.node.innerHTML = html.replace(/__MCE_ITEM__/g, '');\r
413                 },\r
414 \r
415                 /**\r
416                  * This method will split the current block parent and insert the contents inside the split position.\r
417                  * This logic can be improved so text nodes at the start/end remain in the start/end block elements\r
418                  */\r
419                 _insertBlockContent : function(ed, dom, content) {\r
420                         var parentBlock, marker, sel = ed.selection, last, elm, vp, y, elmHeight;\r
421 \r
422                         function select(n) {\r
423                                 var r;\r
424 \r
425                                 if (tinymce.isIE) {\r
426                                         r = ed.getDoc().body.createTextRange();\r
427                                         r.moveToElementText(n);\r
428                                         r.collapse(false);\r
429                                         r.select();\r
430                                 } else {\r
431                                         sel.select(n, 1);\r
432                                         sel.collapse(false);\r
433                                 }\r
434                         };\r
435 \r
436                         // Insert a marker for the caret position\r
437                         this._insert('<span id="_marker">&nbsp;</span>', 1);\r
438                         marker = dom.get('_marker');\r
439                         parentBlock = dom.getParent(marker, 'p,h1,h2,h3,h4,h5,h6,ul,ol');\r
440 \r
441                         if (parentBlock) {\r
442                                 // Split parent block\r
443                                 marker = dom.split(parentBlock, marker);\r
444 \r
445                                 // Insert nodes before the marker\r
446                                 each(dom.create('div', 0, content).childNodes, function(n) {\r
447                                         last = marker.parentNode.insertBefore(n.cloneNode(true), marker);\r
448                                 });\r
449 \r
450                                 // Move caret after marker\r
451                                 select(last);\r
452                         } else {\r
453                                 dom.setOuterHTML(marker, content);\r
454                                 sel.select(ed.getBody(), 1);\r
455                                 sel.collapse(0);\r
456                         }\r
457 \r
458                         dom.remove('_marker'); // Remove marker if it's left\r
459 \r
460                         // Get element, position and height\r
461                         elm = sel.getStart();\r
462                         vp = dom.getViewPort(ed.getWin());\r
463                         y = ed.dom.getPos(elm).y;\r
464                         elmHeight = elm.clientHeight;\r
465 \r
466                         // Is element within viewport if not then scroll it into view\r
467                         if (y < vp.y || y + elmHeight > vp.y + vp.h)\r
468                                 ed.getDoc().body.scrollTop = y < vp.y ? y : y - vp.h + 25;\r
469                 },\r
470 \r
471                 /**\r
472                  * Inserts the specified contents at the caret position.\r
473                  */\r
474                 _insert : function(h, skip_undo) {\r
475                         var ed = this.editor;\r
476 \r
477                         // First delete the contents seems to work better on WebKit\r
478                         if (!ed.selection.isCollapsed())\r
479                                 ed.getDoc().execCommand('Delete', false, null);\r
480 \r
481                         // It's better to use the insertHTML method on Gecko since it will combine paragraphs correctly before inserting the contents\r
482                         ed.execCommand(tinymce.isGecko ? 'insertHTML' : 'mceInsertContent', false, h, {skip_undo : skip_undo});\r
483                 },\r
484 \r
485                 /**\r
486                  * This method will open the old style paste dialogs. Some users might want the old behavior but still use the new cleanup engine.\r
487                  */\r
488                 _legacySupport : function() {\r
489                         var t = this, ed = t.editor;\r
490 \r
491                         // Register commands for backwards compatibility\r
492                         each(['mcePasteText', 'mcePasteWord'], function(cmd) {\r
493                                 ed.addCommand(cmd, function() {\r
494                                         ed.windowManager.open({\r
495                                                 file : t.url + (cmd == 'mcePasteText' ? '/pastetext.htm' : '/pasteword.htm'),\r
496                                                 width : parseInt(ed.getParam("paste_dialog_width", "450")),\r
497                                                 height : parseInt(ed.getParam("paste_dialog_height", "400")),\r
498                                                 inline : 1\r
499                                         });\r
500                                 });\r
501                         });\r
502 \r
503                         // Register buttons for backwards compatibility\r
504                         ed.addButton('pastetext', {title : 'paste.paste_text_desc', cmd : 'mcePasteText'});\r
505                         ed.addButton('pasteword', {title : 'paste.paste_word_desc', cmd : 'mcePasteWord'});\r
506                         ed.addButton('selectall', {title : 'paste.selectall_desc', cmd : 'selectall'});\r
507                 }\r
508         });\r
509 \r
510         // Register plugin\r
511         tinymce.PluginManager.add('paste', tinymce.plugins.PastePlugin);\r
512 })();