Mercurial > public > sg101
comparison static/js/tiny_mce/plugins/paste/editor_plugin_src.js @ 312:88b2b9cb8c1f
Fixing #142; cut over to the django.contrib.staticfiles app.
author | Brian Neal <bgneal@gmail.com> |
---|---|
date | Thu, 27 Jan 2011 02:56:10 +0000 |
parents | |
children | 6c182ceb7147 |
comparison
equal
deleted
inserted
replaced
311:b1c39788e511 | 312:88b2b9cb8c1f |
---|---|
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 | |
11 (function() { | |
12 var each = tinymce.each, | |
13 entities = null, | |
14 defs = { | |
15 paste_auto_cleanup_on_paste : true, | |
16 paste_block_drop : false, | |
17 paste_retain_style_properties : "none", | |
18 paste_strip_class_attributes : "mso", | |
19 paste_remove_spans : false, | |
20 paste_remove_styles : false, | |
21 paste_remove_styles_if_webkit : true, | |
22 paste_convert_middot_lists : true, | |
23 paste_convert_headers_to_strong : false, | |
24 paste_dialog_width : "450", | |
25 paste_dialog_height : "400", | |
26 paste_text_use_dialog : false, | |
27 paste_text_sticky : false, | |
28 paste_text_notifyalways : false, | |
29 paste_text_linebreaktype : "p", | |
30 paste_text_replacements : [ | |
31 [/\u2026/g, "..."], | |
32 [/[\x93\x94\u201c\u201d]/g, '"'], | |
33 [/[\x60\x91\x92\u2018\u2019]/g, "'"] | |
34 ] | |
35 }; | |
36 | |
37 function getParam(ed, name) { | |
38 return ed.getParam(name, defs[name]); | |
39 } | |
40 | |
41 tinymce.create('tinymce.plugins.PastePlugin', { | |
42 init : function(ed, url) { | |
43 var t = this; | |
44 | |
45 t.editor = ed; | |
46 t.url = url; | |
47 | |
48 // Setup plugin events | |
49 t.onPreProcess = new tinymce.util.Dispatcher(t); | |
50 t.onPostProcess = new tinymce.util.Dispatcher(t); | |
51 | |
52 // Register default handlers | |
53 t.onPreProcess.add(t._preProcess); | |
54 t.onPostProcess.add(t._postProcess); | |
55 | |
56 // Register optional preprocess handler | |
57 t.onPreProcess.add(function(pl, o) { | |
58 ed.execCallback('paste_preprocess', pl, o); | |
59 }); | |
60 | |
61 // Register optional postprocess | |
62 t.onPostProcess.add(function(pl, o) { | |
63 ed.execCallback('paste_postprocess', pl, o); | |
64 }); | |
65 | |
66 // Initialize plain text flag | |
67 ed.pasteAsPlainText = false; | |
68 | |
69 // This function executes the process handlers and inserts the contents | |
70 // force_rich overrides plain text mode set by user, important for pasting with execCommand | |
71 function process(o, force_rich) { | |
72 var dom = ed.dom; | |
73 | |
74 // Execute pre process handlers | |
75 t.onPreProcess.dispatch(t, o); | |
76 | |
77 // Create DOM structure | |
78 o.node = dom.create('div', 0, o.content); | |
79 | |
80 // Execute post process handlers | |
81 t.onPostProcess.dispatch(t, o); | |
82 | |
83 // Serialize content | |
84 o.content = ed.serializer.serialize(o.node, {getInner : 1}); | |
85 | |
86 // Plain text option active? | |
87 if ((!force_rich) && (ed.pasteAsPlainText)) { | |
88 t._insertPlainText(ed, dom, o.content); | |
89 | |
90 if (!getParam(ed, "paste_text_sticky")) { | |
91 ed.pasteAsPlainText = false; | |
92 ed.controlManager.setActive("pastetext", false); | |
93 } | |
94 } else if (/<(p|h[1-6]|ul|ol)/.test(o.content)) { | |
95 // Handle insertion of contents containing block elements separately | |
96 t._insertBlockContent(ed, dom, o.content); | |
97 } else { | |
98 t._insert(o.content); | |
99 } | |
100 } | |
101 | |
102 // Add command for external usage | |
103 ed.addCommand('mceInsertClipboardContent', function(u, o) { | |
104 process(o, true); | |
105 }); | |
106 | |
107 if (!getParam(ed, "paste_text_use_dialog")) { | |
108 ed.addCommand('mcePasteText', function(u, v) { | |
109 var cookie = tinymce.util.Cookie; | |
110 | |
111 ed.pasteAsPlainText = !ed.pasteAsPlainText; | |
112 ed.controlManager.setActive('pastetext', ed.pasteAsPlainText); | |
113 | |
114 if ((ed.pasteAsPlainText) && (!cookie.get("tinymcePasteText"))) { | |
115 if (getParam(ed, "paste_text_sticky")) { | |
116 ed.windowManager.alert(ed.translate('paste.plaintext_mode_sticky')); | |
117 } else { | |
118 ed.windowManager.alert(ed.translate('paste.plaintext_mode_sticky')); | |
119 } | |
120 | |
121 if (!getParam(ed, "paste_text_notifyalways")) { | |
122 cookie.set("tinymcePasteText", "1", new Date(new Date().getFullYear() + 1, 12, 31)) | |
123 } | |
124 } | |
125 }); | |
126 } | |
127 | |
128 ed.addButton('pastetext', {title: 'paste.paste_text_desc', cmd: 'mcePasteText'}); | |
129 ed.addButton('selectall', {title: 'paste.selectall_desc', cmd: 'selectall'}); | |
130 | |
131 // This function grabs the contents from the clipboard by adding a | |
132 // hidden div and placing the caret inside it and after the browser paste | |
133 // is done it grabs that contents and processes that | |
134 function grabContent(e) { | |
135 var n, or, rng, sel = ed.selection, dom = ed.dom, body = ed.getBody(), posY; | |
136 | |
137 // Check if browser supports direct plaintext access | |
138 if (ed.pasteAsPlainText && (e.clipboardData || dom.doc.dataTransfer)) { | |
139 e.preventDefault(); | |
140 process({content : (e.clipboardData || dom.doc.dataTransfer).getData('Text')}, true); | |
141 return; | |
142 } | |
143 | |
144 if (dom.get('_mcePaste')) | |
145 return; | |
146 | |
147 // Create container to paste into | |
148 n = dom.add(body, 'div', {id : '_mcePaste', 'class' : 'mcePaste'}, '\uFEFF<br _mce_bogus="1">'); | |
149 | |
150 // If contentEditable mode we need to find out the position of the closest element | |
151 if (body != ed.getDoc().body) | |
152 posY = dom.getPos(ed.selection.getStart(), body).y; | |
153 else | |
154 posY = body.scrollTop; | |
155 | |
156 // Styles needs to be applied after the element is added to the document since WebKit will otherwise remove all styles | |
157 dom.setStyles(n, { | |
158 position : 'absolute', | |
159 left : -10000, | |
160 top : posY, | |
161 width : 1, | |
162 height : 1, | |
163 overflow : 'hidden' | |
164 }); | |
165 | |
166 if (tinymce.isIE) { | |
167 // Select the container | |
168 rng = dom.doc.body.createTextRange(); | |
169 rng.moveToElementText(n); | |
170 rng.execCommand('Paste'); | |
171 | |
172 // Remove container | |
173 dom.remove(n); | |
174 | |
175 // Check if the contents was changed, if it wasn't then clipboard extraction failed probably due | |
176 // to IE security settings so we pass the junk though better than nothing right | |
177 if (n.innerHTML === '\uFEFF') { | |
178 ed.execCommand('mcePasteWord'); | |
179 e.preventDefault(); | |
180 return; | |
181 } | |
182 | |
183 // Process contents | |
184 process({content : n.innerHTML}); | |
185 | |
186 // Block the real paste event | |
187 return tinymce.dom.Event.cancel(e); | |
188 } else { | |
189 function block(e) { | |
190 e.preventDefault(); | |
191 }; | |
192 | |
193 // Block mousedown and click to prevent selection change | |
194 dom.bind(ed.getDoc(), 'mousedown', block); | |
195 dom.bind(ed.getDoc(), 'keydown', block); | |
196 | |
197 or = ed.selection.getRng(); | |
198 | |
199 // Move caret into hidden div | |
200 n = n.firstChild; | |
201 rng = ed.getDoc().createRange(); | |
202 rng.setStart(n, 0); | |
203 rng.setEnd(n, 1); | |
204 sel.setRng(rng); | |
205 | |
206 // Wait a while and grab the pasted contents | |
207 window.setTimeout(function() { | |
208 var h = '', nl = dom.select('div.mcePaste'); | |
209 | |
210 // WebKit will split the div into multiple ones so this will loop through then all and join them to get the whole HTML string | |
211 each(nl, function(n) { | |
212 var child = n.firstChild; | |
213 | |
214 // WebKit inserts a DIV container with lots of odd styles | |
215 if (child && child.nodeName == 'DIV' && child.style.marginTop && child.style.backgroundColor) { | |
216 dom.remove(child, 1); | |
217 } | |
218 | |
219 // WebKit duplicates the divs so we need to remove them | |
220 each(dom.select('div.mcePaste', n), function(n) { | |
221 dom.remove(n, 1); | |
222 }); | |
223 | |
224 // Remove apply style spans | |
225 each(dom.select('span.Apple-style-span', n), function(n) { | |
226 dom.remove(n, 1); | |
227 }); | |
228 | |
229 // Remove bogus br elements | |
230 each(dom.select('br[_mce_bogus]', n), function(n) { | |
231 dom.remove(n); | |
232 }); | |
233 | |
234 h += n.innerHTML; | |
235 }); | |
236 | |
237 // Remove the nodes | |
238 each(nl, function(n) { | |
239 dom.remove(n); | |
240 }); | |
241 | |
242 // Restore the old selection | |
243 if (or) | |
244 sel.setRng(or); | |
245 | |
246 process({content : h}); | |
247 | |
248 // Unblock events ones we got the contents | |
249 dom.unbind(ed.getDoc(), 'mousedown', block); | |
250 dom.unbind(ed.getDoc(), 'keydown', block); | |
251 }, 0); | |
252 } | |
253 } | |
254 | |
255 // Check if we should use the new auto process method | |
256 if (getParam(ed, "paste_auto_cleanup_on_paste")) { | |
257 // Is it's Opera or older FF use key handler | |
258 if (tinymce.isOpera || /Firefox\/2/.test(navigator.userAgent)) { | |
259 ed.onKeyDown.add(function(ed, e) { | |
260 if (((tinymce.isMac ? e.metaKey : e.ctrlKey) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45)) | |
261 grabContent(e); | |
262 }); | |
263 } else { | |
264 // Grab contents on paste event on Gecko and WebKit | |
265 ed.onPaste.addToTop(function(ed, e) { | |
266 return grabContent(e); | |
267 }); | |
268 } | |
269 } | |
270 | |
271 // Block all drag/drop events | |
272 if (getParam(ed, "paste_block_drop")) { | |
273 ed.onInit.add(function() { | |
274 ed.dom.bind(ed.getBody(), ['dragend', 'dragover', 'draggesture', 'dragdrop', 'drop', 'drag'], function(e) { | |
275 e.preventDefault(); | |
276 e.stopPropagation(); | |
277 | |
278 return false; | |
279 }); | |
280 }); | |
281 } | |
282 | |
283 // Add legacy support | |
284 t._legacySupport(); | |
285 }, | |
286 | |
287 getInfo : function() { | |
288 return { | |
289 longname : 'Paste text/word', | |
290 author : 'Moxiecode Systems AB', | |
291 authorurl : 'http://tinymce.moxiecode.com', | |
292 infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/paste', | |
293 version : tinymce.majorVersion + "." + tinymce.minorVersion | |
294 }; | |
295 }, | |
296 | |
297 _preProcess : function(pl, o) { | |
298 //console.log('Before preprocess:' + o.content); | |
299 | |
300 var ed = this.editor, | |
301 h = o.content, | |
302 grep = tinymce.grep, | |
303 explode = tinymce.explode, | |
304 trim = tinymce.trim, | |
305 len, stripClass; | |
306 | |
307 function process(items) { | |
308 each(items, function(v) { | |
309 // Remove or replace | |
310 if (v.constructor == RegExp) | |
311 h = h.replace(v, ''); | |
312 else | |
313 h = h.replace(v[0], v[1]); | |
314 }); | |
315 } | |
316 | |
317 // Detect Word content and process it more aggressive | |
318 if (/class="?Mso|style="[^"]*\bmso-|w:WordDocument/i.test(h) || o.wordContent) { | |
319 o.wordContent = true; // Mark the pasted contents as word specific content | |
320 //console.log('Word contents detected.'); | |
321 | |
322 // Process away some basic content | |
323 process([ | |
324 /^\s*( )+/gi, // entities at the start of contents | |
325 /( |<br[^>]*>)+\s*$/gi // entities at the end of contents | |
326 ]); | |
327 | |
328 if (getParam(ed, "paste_convert_headers_to_strong")) { | |
329 h = h.replace(/<p [^>]*class="?MsoHeading"?[^>]*>(.*?)<\/p>/gi, "<p><strong>$1</strong></p>"); | |
330 } | |
331 | |
332 if (getParam(ed, "paste_convert_middot_lists")) { | |
333 process([ | |
334 [/<!--\[if !supportLists\]-->/gi, '$&__MCE_ITEM__'], // Convert supportLists to a list item marker | |
335 [/(<span[^>]+(?:mso-list:|:\s*symbol)[^>]+>)/gi, '$1__MCE_ITEM__'] // Convert mso-list and symbol spans to item markers | |
336 ]); | |
337 } | |
338 | |
339 process([ | |
340 // Word comments like conditional comments etc | |
341 /<!--[\s\S]+?-->/gi, | |
342 | |
343 // Remove comments, scripts (e.g., msoShowComment), XML tag, VML content, MS Office namespaced tags, and a few other tags | |
344 /<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, | |
345 | |
346 // Convert <s> into <strike> for line-though | |
347 [/<(\/?)s>/gi, "<$1strike>"], | |
348 | |
349 // Replace nsbp entites to char since it's easier to handle | |
350 [/ /gi, "\u00a0"] | |
351 ]); | |
352 | |
353 // Remove bad attributes, with or without quotes, ensuring that attribute text is really inside a tag. | |
354 // 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. | |
355 do { | |
356 len = h.length; | |
357 h = h.replace(/(<[a-z][^>]*\s)(?:id|name|language|type|on\w+|\w+:\w+)=(?:"[^"]*"|\w+)\s?/gi, "$1"); | |
358 } while (len != h.length); | |
359 | |
360 // Remove all spans if no styles is to be retained | |
361 if (getParam(ed, "paste_retain_style_properties").replace(/^none$/i, "").length == 0) { | |
362 h = h.replace(/<\/?span[^>]*>/gi, ""); | |
363 } else { | |
364 // We're keeping styles, so at least clean them up. | |
365 // CSS Reference: http://msdn.microsoft.com/en-us/library/aa155477.aspx | |
366 | |
367 process([ | |
368 // Convert <span style="mso-spacerun:yes">___</span> to string of alternating breaking/non-breaking spaces of same length | |
369 [/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi, | |
370 function(str, spaces) { | |
371 return (spaces.length > 0)? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : ""; | |
372 } | |
373 ], | |
374 | |
375 // Examine all styles: delete junk, transform some, and keep the rest | |
376 [/(<[a-z][^>]*)\sstyle="([^"]*)"/gi, | |
377 function(str, tag, style) { | |
378 var n = [], | |
379 i = 0, | |
380 s = explode(trim(style).replace(/"/gi, "'"), ";"); | |
381 | |
382 // Examine each style definition within the tag's style attribute | |
383 each(s, function(v) { | |
384 var name, value, | |
385 parts = explode(v, ":"); | |
386 | |
387 function ensureUnits(v) { | |
388 return v + ((v !== "0") && (/\d$/.test(v)))? "px" : ""; | |
389 } | |
390 | |
391 if (parts.length == 2) { | |
392 name = parts[0].toLowerCase(); | |
393 value = parts[1].toLowerCase(); | |
394 | |
395 // Translate certain MS Office styles into their CSS equivalents | |
396 switch (name) { | |
397 case "mso-padding-alt": | |
398 case "mso-padding-top-alt": | |
399 case "mso-padding-right-alt": | |
400 case "mso-padding-bottom-alt": | |
401 case "mso-padding-left-alt": | |
402 case "mso-margin-alt": | |
403 case "mso-margin-top-alt": | |
404 case "mso-margin-right-alt": | |
405 case "mso-margin-bottom-alt": | |
406 case "mso-margin-left-alt": | |
407 case "mso-table-layout-alt": | |
408 case "mso-height": | |
409 case "mso-width": | |
410 case "mso-vertical-align-alt": | |
411 n[i++] = name.replace(/^mso-|-alt$/g, "") + ":" + ensureUnits(value); | |
412 return; | |
413 | |
414 case "horiz-align": | |
415 n[i++] = "text-align:" + value; | |
416 return; | |
417 | |
418 case "vert-align": | |
419 n[i++] = "vertical-align:" + value; | |
420 return; | |
421 | |
422 case "font-color": | |
423 case "mso-foreground": | |
424 n[i++] = "color:" + value; | |
425 return; | |
426 | |
427 case "mso-background": | |
428 case "mso-highlight": | |
429 n[i++] = "background:" + value; | |
430 return; | |
431 | |
432 case "mso-default-height": | |
433 n[i++] = "min-height:" + ensureUnits(value); | |
434 return; | |
435 | |
436 case "mso-default-width": | |
437 n[i++] = "min-width:" + ensureUnits(value); | |
438 return; | |
439 | |
440 case "mso-padding-between-alt": | |
441 n[i++] = "border-collapse:separate;border-spacing:" + ensureUnits(value); | |
442 return; | |
443 | |
444 case "text-line-through": | |
445 if ((value == "single") || (value == "double")) { | |
446 n[i++] = "text-decoration:line-through"; | |
447 } | |
448 return; | |
449 | |
450 case "mso-zero-height": | |
451 if (value == "yes") { | |
452 n[i++] = "display:none"; | |
453 } | |
454 return; | |
455 } | |
456 | |
457 // Eliminate all MS Office style definitions that have no CSS equivalent by examining the first characters in the name | |
458 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)) { | |
459 return; | |
460 } | |
461 | |
462 // If it reached this point, it must be a valid CSS style | |
463 n[i++] = name + ":" + parts[1]; // Lower-case name, but keep value case | |
464 } | |
465 }); | |
466 | |
467 // If style attribute contained any valid styles the re-write it; otherwise delete style attribute. | |
468 if (i > 0) { | |
469 return tag + ' style="' + n.join(';') + '"'; | |
470 } else { | |
471 return tag; | |
472 } | |
473 } | |
474 ] | |
475 ]); | |
476 } | |
477 } | |
478 | |
479 // Replace headers with <strong> | |
480 if (getParam(ed, "paste_convert_headers_to_strong")) { | |
481 process([ | |
482 [/<h[1-6][^>]*>/gi, "<p><strong>"], | |
483 [/<\/h[1-6][^>]*>/gi, "</strong></p>"] | |
484 ]); | |
485 } | |
486 | |
487 // Class attribute options are: leave all as-is ("none"), remove all ("all"), or remove only those starting with mso ("mso"). | |
488 // Note:- paste_strip_class_attributes: "none", verify_css_classes: true is also a good variation. | |
489 stripClass = getParam(ed, "paste_strip_class_attributes"); | |
490 | |
491 if (stripClass !== "none") { | |
492 function removeClasses(match, g1) { | |
493 if (stripClass === "all") | |
494 return ''; | |
495 | |
496 var cls = grep(explode(g1.replace(/^(["'])(.*)\1$/, "$2"), " "), | |
497 function(v) { | |
498 return (/^(?!mso)/i.test(v)); | |
499 } | |
500 ); | |
501 | |
502 return cls.length ? ' class="' + cls.join(" ") + '"' : ''; | |
503 }; | |
504 | |
505 h = h.replace(/ class="([^"]+)"/gi, removeClasses); | |
506 h = h.replace(/ class=(\w+)/gi, removeClasses); | |
507 } | |
508 | |
509 // Remove spans option | |
510 if (getParam(ed, "paste_remove_spans")) { | |
511 h = h.replace(/<\/?span[^>]*>/gi, ""); | |
512 } | |
513 | |
514 //console.log('After preprocess:' + h); | |
515 | |
516 o.content = h; | |
517 }, | |
518 | |
519 /** | |
520 * Various post process items. | |
521 */ | |
522 _postProcess : function(pl, o) { | |
523 var t = this, ed = t.editor, dom = ed.dom, styleProps; | |
524 | |
525 if (o.wordContent) { | |
526 // Remove named anchors or TOC links | |
527 each(dom.select('a', o.node), function(a) { | |
528 if (!a.href || a.href.indexOf('#_Toc') != -1) | |
529 dom.remove(a, 1); | |
530 }); | |
531 | |
532 if (getParam(ed, "paste_convert_middot_lists")) { | |
533 t._convertLists(pl, o); | |
534 } | |
535 | |
536 // Process styles | |
537 styleProps = getParam(ed, "paste_retain_style_properties"); // retained properties | |
538 | |
539 // Process only if a string was specified and not equal to "all" or "*" | |
540 if ((tinymce.is(styleProps, "string")) && (styleProps !== "all") && (styleProps !== "*")) { | |
541 styleProps = tinymce.explode(styleProps.replace(/^none$/i, "")); | |
542 | |
543 // Retains some style properties | |
544 each(dom.select('*', o.node), function(el) { | |
545 var newStyle = {}, npc = 0, i, sp, sv; | |
546 | |
547 // Store a subset of the existing styles | |
548 if (styleProps) { | |
549 for (i = 0; i < styleProps.length; i++) { | |
550 sp = styleProps[i]; | |
551 sv = dom.getStyle(el, sp); | |
552 | |
553 if (sv) { | |
554 newStyle[sp] = sv; | |
555 npc++; | |
556 } | |
557 } | |
558 } | |
559 | |
560 // Remove all of the existing styles | |
561 dom.setAttrib(el, 'style', ''); | |
562 | |
563 if (styleProps && npc > 0) | |
564 dom.setStyles(el, newStyle); // Add back the stored subset of styles | |
565 else // Remove empty span tags that do not have class attributes | |
566 if (el.nodeName == 'SPAN' && !el.className) | |
567 dom.remove(el, true); | |
568 }); | |
569 } | |
570 } | |
571 | |
572 // Remove all style information or only specifically on WebKit to avoid the style bug on that browser | |
573 if (getParam(ed, "paste_remove_styles") || (getParam(ed, "paste_remove_styles_if_webkit") && tinymce.isWebKit)) { | |
574 each(dom.select('*[style]', o.node), function(el) { | |
575 el.removeAttribute('style'); | |
576 el.removeAttribute('_mce_style'); | |
577 }); | |
578 } else { | |
579 if (tinymce.isWebKit) { | |
580 // 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 ..." /> | |
581 // Removing the mce_style that contains the real value will force the Serializer engine to compress the styles | |
582 each(dom.select('*', o.node), function(el) { | |
583 el.removeAttribute('_mce_style'); | |
584 }); | |
585 } | |
586 } | |
587 }, | |
588 | |
589 /** | |
590 * Converts the most common bullet and number formats in Office into a real semantic UL/LI list. | |
591 */ | |
592 _convertLists : function(pl, o) { | |
593 var dom = pl.editor.dom, listElm, li, lastMargin = -1, margin, levels = [], lastType, html; | |
594 | |
595 // Convert middot lists into real semantic lists | |
596 each(dom.select('p', o.node), function(p) { | |
597 var sib, val = '', type, html, idx, parents; | |
598 | |
599 // Get text node value at beginning of paragraph | |
600 for (sib = p.firstChild; sib && sib.nodeType == 3; sib = sib.nextSibling) | |
601 val += sib.nodeValue; | |
602 | |
603 val = p.innerHTML.replace(/<\/?\w+[^>]*>/gi, '').replace(/ /g, '\u00a0'); | |
604 | |
605 // Detect unordered lists look for bullets | |
606 if (/^(__MCE_ITEM__)+[\u2022\u00b7\u00a7\u00d8o]\s*\u00a0*/.test(val)) | |
607 type = 'ul'; | |
608 | |
609 // Detect ordered lists 1., a. or ixv. | |
610 if (/^__MCE_ITEM__\s*\w+\.\s*\u00a0{2,}/.test(val)) | |
611 type = 'ol'; | |
612 | |
613 // Check if node value matches the list pattern: o | |
614 if (type) { | |
615 margin = parseFloat(p.style.marginLeft || 0); | |
616 | |
617 if (margin > lastMargin) | |
618 levels.push(margin); | |
619 | |
620 if (!listElm || type != lastType) { | |
621 listElm = dom.create(type); | |
622 dom.insertAfter(listElm, p); | |
623 } else { | |
624 // Nested list element | |
625 if (margin > lastMargin) { | |
626 listElm = li.appendChild(dom.create(type)); | |
627 } else if (margin < lastMargin) { | |
628 // Find parent level based on margin value | |
629 idx = tinymce.inArray(levels, margin); | |
630 parents = dom.getParents(listElm.parentNode, type); | |
631 listElm = parents[parents.length - 1 - idx] || listElm; | |
632 } | |
633 } | |
634 | |
635 // Remove middot or number spans if they exists | |
636 each(dom.select('span', p), function(span) { | |
637 var html = span.innerHTML.replace(/<\/?\w+[^>]*>/gi, ''); | |
638 | |
639 // Remove span with the middot or the number | |
640 if (type == 'ul' && /^[\u2022\u00b7\u00a7\u00d8o]/.test(html)) | |
641 dom.remove(span); | |
642 else if (/^[\s\S]*\w+\.( |\u00a0)*\s*/.test(html)) | |
643 dom.remove(span); | |
644 }); | |
645 | |
646 html = p.innerHTML; | |
647 | |
648 // Remove middot/list items | |
649 if (type == 'ul') | |
650 html = p.innerHTML.replace(/__MCE_ITEM__/g, '').replace(/^[\u2022\u00b7\u00a7\u00d8o]\s*( |\u00a0)+\s*/, ''); | |
651 else | |
652 html = p.innerHTML.replace(/__MCE_ITEM__/g, '').replace(/^\s*\w+\.( |\u00a0)+\s*/, ''); | |
653 | |
654 // Create li and add paragraph data into the new li | |
655 li = listElm.appendChild(dom.create('li', 0, html)); | |
656 dom.remove(p); | |
657 | |
658 lastMargin = margin; | |
659 lastType = type; | |
660 } else | |
661 listElm = lastMargin = 0; // End list element | |
662 }); | |
663 | |
664 // Remove any left over makers | |
665 html = o.node.innerHTML; | |
666 if (html.indexOf('__MCE_ITEM__') != -1) | |
667 o.node.innerHTML = html.replace(/__MCE_ITEM__/g, ''); | |
668 }, | |
669 | |
670 /** | |
671 * This method will split the current block parent and insert the contents inside the split position. | |
672 * This logic can be improved so text nodes at the start/end remain in the start/end block elements | |
673 */ | |
674 _insertBlockContent : function(ed, dom, content) { | |
675 var parentBlock, marker, sel = ed.selection, last, elm, vp, y, elmHeight, markerId = 'mce_marker'; | |
676 | |
677 function select(n) { | |
678 var r; | |
679 | |
680 if (tinymce.isIE) { | |
681 r = ed.getDoc().body.createTextRange(); | |
682 r.moveToElementText(n); | |
683 r.collapse(false); | |
684 r.select(); | |
685 } else { | |
686 sel.select(n, 1); | |
687 sel.collapse(false); | |
688 } | |
689 } | |
690 | |
691 // Insert a marker for the caret position | |
692 this._insert('<span id="' + markerId + '"></span>', 1); | |
693 marker = dom.get(markerId); | |
694 parentBlock = dom.getParent(marker, 'p,h1,h2,h3,h4,h5,h6,ul,ol,th,td'); | |
695 | |
696 // If it's a parent block but not a table cell | |
697 if (parentBlock && !/TD|TH/.test(parentBlock.nodeName)) { | |
698 // Split parent block | |
699 marker = dom.split(parentBlock, marker); | |
700 | |
701 // Insert nodes before the marker | |
702 each(dom.create('div', 0, content).childNodes, function(n) { | |
703 last = marker.parentNode.insertBefore(n.cloneNode(true), marker); | |
704 }); | |
705 | |
706 // Move caret after marker | |
707 select(last); | |
708 } else { | |
709 dom.setOuterHTML(marker, content); | |
710 sel.select(ed.getBody(), 1); | |
711 sel.collapse(0); | |
712 } | |
713 | |
714 // Remove marker if it's left | |
715 while (elm = dom.get(markerId)) | |
716 dom.remove(elm); | |
717 | |
718 // Get element, position and height | |
719 elm = sel.getStart(); | |
720 vp = dom.getViewPort(ed.getWin()); | |
721 y = ed.dom.getPos(elm).y; | |
722 elmHeight = elm.clientHeight; | |
723 | |
724 // Is element within viewport if not then scroll it into view | |
725 if (y < vp.y || y + elmHeight > vp.y + vp.h) | |
726 ed.getDoc().body.scrollTop = y < vp.y ? y : y - vp.h + 25; | |
727 }, | |
728 | |
729 /** | |
730 * Inserts the specified contents at the caret position. | |
731 */ | |
732 _insert : function(h, skip_undo) { | |
733 var ed = this.editor, r = ed.selection.getRng(); | |
734 | |
735 // First delete the contents seems to work better on WebKit when the selection spans multiple list items or multiple table cells. | |
736 if (!ed.selection.isCollapsed() && r.startContainer != r.endContainer) | |
737 ed.getDoc().execCommand('Delete', false, null); | |
738 | |
739 // It's better to use the insertHTML method on Gecko since it will combine paragraphs correctly before inserting the contents | |
740 ed.execCommand(tinymce.isGecko ? 'insertHTML' : 'mceInsertContent', false, h, {skip_undo : skip_undo}); | |
741 }, | |
742 | |
743 /** | |
744 * Instead of the old plain text method which tried to re-create a paste operation, the | |
745 * new approach adds a plain text mode toggle switch that changes the behavior of paste. | |
746 * This function is passed the same input that the regular paste plugin produces. | |
747 * It performs additional scrubbing and produces (and inserts) the plain text. | |
748 * This approach leverages all of the great existing functionality in the paste | |
749 * plugin, and requires minimal changes to add the new functionality. | |
750 * Speednet - June 2009 | |
751 */ | |
752 _insertPlainText : function(ed, dom, h) { | |
753 var i, len, pos, rpos, node, breakElms, before, after, | |
754 w = ed.getWin(), | |
755 d = ed.getDoc(), | |
756 sel = ed.selection, | |
757 is = tinymce.is, | |
758 inArray = tinymce.inArray, | |
759 linebr = getParam(ed, "paste_text_linebreaktype"), | |
760 rl = getParam(ed, "paste_text_replacements"); | |
761 | |
762 function process(items) { | |
763 each(items, function(v) { | |
764 if (v.constructor == RegExp) | |
765 h = h.replace(v, ""); | |
766 else | |
767 h = h.replace(v[0], v[1]); | |
768 }); | |
769 }; | |
770 | |
771 if ((typeof(h) === "string") && (h.length > 0)) { | |
772 if (!entities) | |
773 entities = ("34,quot,38,amp,39,apos,60,lt,62,gt," + ed.serializer.settings.entities).split(","); | |
774 | |
775 // If HTML content with line-breaking tags, then remove all cr/lf chars because only tags will break a line | |
776 if (/<(?:p|br|h[1-6]|ul|ol|dl|table|t[rdh]|div|blockquote|fieldset|pre|address|center)[^>]*>/i.test(h)) { | |
777 process([ | |
778 /[\n\r]+/g | |
779 ]); | |
780 } else { | |
781 // Otherwise just get rid of carriage returns (only need linefeeds) | |
782 process([ | |
783 /\r+/g | |
784 ]); | |
785 } | |
786 | |
787 process([ | |
788 [/<\/(?: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 | |
789 [/<br[^>]*>|<\/tr>/gi, "\n"], // Single linebreak for <br /> tags and table rows | |
790 [/<\/t[dh]>\s*<t[dh][^>]*>/gi, "\t"], // Table cells get tabs betweem them | |
791 /<[a-z!\/?][^>]*>/gi, // Delete all remaining tags | |
792 [/ /gi, " "], // Convert non-break spaces to regular spaces (remember, *plain text*) | |
793 [ | |
794 // HTML entity | |
795 /&(#\d+|[a-z0-9]{1,10});/gi, | |
796 | |
797 // Replace with actual character | |
798 function(e, s) { | |
799 if (s.charAt(0) === "#") { | |
800 return String.fromCharCode(s.slice(1)); | |
801 } | |
802 else { | |
803 return ((e = inArray(entities, s)) > 0)? String.fromCharCode(entities[e-1]) : " "; | |
804 } | |
805 } | |
806 ], | |
807 [/(?:(?!\n)\s)*(\n+)(?:(?!\n)\s)*/gi, "$1"], // Cool little RegExp deletes whitespace around linebreak chars. | |
808 [/\n{3,}/g, "\n\n"], // Max. 2 consecutive linebreaks | |
809 /^\s+|\s+$/g // Trim the front & back | |
810 ]); | |
811 | |
812 h = dom.encode(h); | |
813 | |
814 // Delete any highlighted text before pasting | |
815 if (!sel.isCollapsed()) { | |
816 d.execCommand("Delete", false, null); | |
817 } | |
818 | |
819 // Perform default or custom replacements | |
820 if (is(rl, "array") || (is(rl, "array"))) { | |
821 process(rl); | |
822 } | |
823 else if (is(rl, "string")) { | |
824 process(new RegExp(rl, "gi")); | |
825 } | |
826 | |
827 // Treat paragraphs as specified in the config | |
828 if (linebr == "none") { | |
829 process([ | |
830 [/\n+/g, " "] | |
831 ]); | |
832 } | |
833 else if (linebr == "br") { | |
834 process([ | |
835 [/\n/g, "<br />"] | |
836 ]); | |
837 } | |
838 else { | |
839 process([ | |
840 /^\s+|\s+$/g, | |
841 [/\n\n/g, "</p><p>"], | |
842 [/\n/g, "<br />"] | |
843 ]); | |
844 } | |
845 | |
846 // This next piece of code handles the situation where we're pasting more than one paragraph of plain | |
847 // text, and we are pasting the content into the middle of a block node in the editor. The block | |
848 // node gets split at the selection point into "Para A" and "Para B" (for the purposes of explaining). | |
849 // The first paragraph of the pasted text is appended to "Para A", and the last paragraph of the | |
850 // pasted text is prepended to "Para B". Any other paragraphs of pasted text are placed between | |
851 // "Para A" and "Para B". This code solves a host of problems with the original plain text plugin and | |
852 // now handles styles correctly. (Pasting plain text into a styled paragraph is supposed to make the | |
853 // plain text take the same style as the existing paragraph.) | |
854 if ((pos = h.indexOf("</p><p>")) != -1) { | |
855 rpos = h.lastIndexOf("</p><p>"); | |
856 node = sel.getNode(); | |
857 breakElms = []; // Get list of elements to break | |
858 | |
859 do { | |
860 if (node.nodeType == 1) { | |
861 // Don't break tables and break at body | |
862 if (node.nodeName == "TD" || node.nodeName == "BODY") { | |
863 break; | |
864 } | |
865 | |
866 breakElms[breakElms.length] = node; | |
867 } | |
868 } while (node = node.parentNode); | |
869 | |
870 // Are we in the middle of a block node? | |
871 if (breakElms.length > 0) { | |
872 before = h.substring(0, pos); | |
873 after = ""; | |
874 | |
875 for (i=0, len=breakElms.length; i<len; i++) { | |
876 before += "</" + breakElms[i].nodeName.toLowerCase() + ">"; | |
877 after += "<" + breakElms[breakElms.length-i-1].nodeName.toLowerCase() + ">"; | |
878 } | |
879 | |
880 if (pos == rpos) { | |
881 h = before + after + h.substring(pos+7); | |
882 } | |
883 else { | |
884 h = before + h.substring(pos+4, rpos+4) + after + h.substring(rpos+7); | |
885 } | |
886 } | |
887 } | |
888 | |
889 // Insert content at the caret, plus add a marker for repositioning the caret | |
890 ed.execCommand("mceInsertRawHTML", false, h + '<span id="_plain_text_marker"> </span>'); | |
891 | |
892 // Reposition the caret to the marker, which was placed immediately after the inserted content. | |
893 // Needs to be done asynchronously (in window.setTimeout) or else it doesn't work in all browsers. | |
894 // The second part of the code scrolls the content up if the caret is positioned off-screen. | |
895 // This is only necessary for WebKit browsers, but it doesn't hurt to use for all. | |
896 window.setTimeout(function() { | |
897 var marker = dom.get('_plain_text_marker'), | |
898 elm, vp, y, elmHeight; | |
899 | |
900 sel.select(marker, false); | |
901 d.execCommand("Delete", false, null); | |
902 marker = null; | |
903 | |
904 // Get element, position and height | |
905 elm = sel.getStart(); | |
906 vp = dom.getViewPort(w); | |
907 y = dom.getPos(elm).y; | |
908 elmHeight = elm.clientHeight; | |
909 | |
910 // Is element within viewport if not then scroll it into view | |
911 if ((y < vp.y) || (y + elmHeight > vp.y + vp.h)) { | |
912 d.body.scrollTop = y < vp.y ? y : y - vp.h + 25; | |
913 } | |
914 }, 0); | |
915 } | |
916 }, | |
917 | |
918 /** | |
919 * This method will open the old style paste dialogs. Some users might want the old behavior but still use the new cleanup engine. | |
920 */ | |
921 _legacySupport : function() { | |
922 var t = this, ed = t.editor; | |
923 | |
924 // Register command(s) for backwards compatibility | |
925 ed.addCommand("mcePasteWord", function() { | |
926 ed.windowManager.open({ | |
927 file: t.url + "/pasteword.htm", | |
928 width: parseInt(getParam(ed, "paste_dialog_width")), | |
929 height: parseInt(getParam(ed, "paste_dialog_height")), | |
930 inline: 1 | |
931 }); | |
932 }); | |
933 | |
934 if (getParam(ed, "paste_text_use_dialog")) { | |
935 ed.addCommand("mcePasteText", function() { | |
936 ed.windowManager.open({ | |
937 file : t.url + "/pastetext.htm", | |
938 width: parseInt(getParam(ed, "paste_dialog_width")), | |
939 height: parseInt(getParam(ed, "paste_dialog_height")), | |
940 inline : 1 | |
941 }); | |
942 }); | |
943 } | |
944 | |
945 // Register button for backwards compatibility | |
946 ed.addButton("pasteword", {title : "paste.paste_word_desc", cmd : "mcePasteWord"}); | |
947 } | |
948 }); | |
949 | |
950 // Register plugin | |
951 tinymce.PluginManager.add("paste", tinymce.plugins.PastePlugin); | |
952 })(); |