/**
* editor_plugin_src.js
*
* Copyright 2009, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://tinymce.moxiecode.com/license
* Contributing: http://tinymce.moxiecode.com/contributing
*/
(function() {
var each = tinymce.each,
defs = {
paste_auto_cleanup_on_paste : true,
paste_enable_default_filters : true,
paste_block_drop : false,
paste_retain_style_properties : "none",
paste_strip_class_attributes : "mso",
paste_remove_spans : false,
paste_remove_styles : false,
paste_remove_styles_if_webkit : true,
paste_convert_middot_lists : true,
paste_convert_headers_to_strong : false,
paste_dialog_width : "450",
paste_dialog_height : "400",
paste_text_use_dialog : false,
paste_text_sticky : false,
paste_text_notifyalways : false,
paste_text_linebreaktype : "p",
paste_text_replacements : [
[/\u2026/g, "..."],
[/[\x93\x94\u201c\u201d]/g, '"'],
[/[\x60\x91\x92\u2018\u2019]/g, "'"]
]
};
function getParam(ed, name) {
return ed.getParam(name, defs[name]);
}
tinymce.create('tinymce.plugins.PastePlugin', {
init : function(ed, url) {
var t = this;
t.editor = ed;
t.url = url;
// Setup plugin events
t.onPreProcess = new tinymce.util.Dispatcher(t);
t.onPostProcess = new tinymce.util.Dispatcher(t);
// Register default handlers
t.onPreProcess.add(t._preProcess);
t.onPostProcess.add(t._postProcess);
// Register optional preprocess handler
t.onPreProcess.add(function(pl, o) {
ed.execCallback('paste_preprocess', pl, o);
});
// Register optional postprocess
t.onPostProcess.add(function(pl, o) {
ed.execCallback('paste_postprocess', pl, o);
});
ed.onKeyDown.addToTop(function(ed, e) {
// Block ctrl+v from adding an undo level since the default logic in tinymce.Editor will add that
if (((tinymce.isMac ? e.metaKey : e.ctrlKey) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45))
return false; // Stop other listeners
});
// Initialize plain text flag
ed.pasteAsPlainText = false;
// This function executes the process handlers and inserts the contents
// force_rich overrides plain text mode set by user, important for pasting with execCommand
function process(o, force_rich) {
var dom = ed.dom, rng, nodes;
// Execute pre process handlers
t.onPreProcess.dispatch(t, o);
// Create DOM structure
o.node = dom.create('div', 0, o.content);
// If pasting inside the same element and the contents is only one block
// remove the block and keep the text since Firefox will copy parts of pre and h1-h6 as a pre element
if (tinymce.isGecko) {
rng = ed.selection.getRng(true);
if (rng.startContainer == rng.endContainer && rng.startContainer.nodeType == 3) {
nodes = dom.select('p,h1,h2,h3,h4,h5,h6,pre', o.node);
// Is only one block node and it doesn't contain word stuff
if (nodes.length == 1 && o.content.indexOf('__MCE_ITEM__') === -1)
dom.remove(nodes.reverse(), true);
}
}
// Execute post process handlers
t.onPostProcess.dispatch(t, o);
// Serialize content
o.content = ed.serializer.serialize(o.node, {getInner : 1});
// Plain text option active?
if ((!force_rich) && (ed.pasteAsPlainText)) {
t._insertPlainText(ed, dom, o.content);
if (!getParam(ed, "paste_text_sticky")) {
ed.pasteAsPlainText = false;
ed.controlManager.setActive("pastetext", false);
}
} else {
t._insert(o.content);
}
}
// Add command for external usage
ed.addCommand('mceInsertClipboardContent', function(u, o) {
process(o, true);
});
if (!getParam(ed, "paste_text_use_dialog")) {
ed.addCommand('mcePasteText', function(u, v) {
var cookie = tinymce.util.Cookie;
ed.pasteAsPlainText = !ed.pasteAsPlainText;
ed.controlManager.setActive('pastetext', ed.pasteAsPlainText);
if ((ed.pasteAsPlainText) && (!cookie.get("tinymcePasteText"))) {
if (getParam(ed, "paste_text_sticky")) {
ed.windowManager.alert(ed.translate('paste.plaintext_mode_sticky'));
} else {
ed.windowManager.alert(ed.translate('paste.plaintext_mode_sticky'));
}
if (!getParam(ed, "paste_text_notifyalways")) {
cookie.set("tinymcePasteText", "1", new Date(new Date().getFullYear() + 1, 12, 31))
}
}
});
}
ed.addButton('pastetext', {title: 'paste.paste_text_desc', cmd: 'mcePasteText'});
ed.addButton('selectall', {title: 'paste.selectall_desc', cmd: 'selectall'});
// This function grabs the contents from the clipboard by adding a
// hidden div and placing the caret inside it and after the browser paste
// is done it grabs that contents and processes that
function grabContent(e) {
var n, or, rng, oldRng, sel = ed.selection, dom = ed.dom, body = ed.getBody(), posY, textContent;
// Check if browser supports direct plaintext access
if (e.clipboardData || dom.doc.dataTransfer) {
textContent = (e.clipboardData || dom.doc.dataTransfer).getData('Text');
if (ed.pasteAsPlainText) {
e.preventDefault();
process({content : textContent.replace(/\r?\n/g, '
')});
return;
}
}
if (dom.get('_mcePaste'))
return;
// Create container to paste into
n = dom.add(body, 'div', {id : '_mcePaste', 'class' : 'mcePaste', 'data-mce-bogus' : '1'}, '\uFEFF\uFEFF
');
// If contentEditable mode we need to find out the position of the closest element
if (body != ed.getDoc().body)
posY = dom.getPos(ed.selection.getStart(), body).y;
else
posY = body.scrollTop + dom.getViewPort().y;
// Styles needs to be applied after the element is added to the document since WebKit will otherwise remove all styles
dom.setStyles(n, {
position : 'absolute',
left : -10000,
top : posY,
width : 1,
height : 1,
overflow : 'hidden'
});
if (tinymce.isIE) {
// Store away the old range
oldRng = sel.getRng();
// Select the container
rng = dom.doc.body.createTextRange();
rng.moveToElementText(n);
rng.execCommand('Paste');
// Remove container
dom.remove(n);
// Check if the contents was changed, if it wasn't then clipboard extraction failed probably due
// to IE security settings so we pass the junk though better than nothing right
if (n.innerHTML === '\uFEFF') {
ed.execCommand('mcePasteWord');
e.preventDefault();
return;
}
// Restore the old range and clear the contents before pasting
sel.setRng(oldRng);
sel.setContent('');
// For some odd reason we need to detach the the mceInsertContent call from the paste event
// It's like IE has a reference to the parent element that you paste in and the selection gets messed up
// when it tries to restore the selection
setTimeout(function() {
// Process contents
process({content : n.innerHTML});
}, 0);
// Block the real paste event
return tinymce.dom.Event.cancel(e);
} else {
function block(e) {
e.preventDefault();
};
// Block mousedown and click to prevent selection change
dom.bind(ed.getDoc(), 'mousedown', block);
dom.bind(ed.getDoc(), 'keydown', block);
or = ed.selection.getRng();
// Move caret into hidden div
n = n.firstChild;
rng = ed.getDoc().createRange();
rng.setStart(n, 0);
rng.setEnd(n, 1);
sel.setRng(rng);
// Wait a while and grab the pasted contents
window.setTimeout(function() {
var h = '', nl;
// Paste divs duplicated in paste divs seems to happen when you paste plain text so lets first look for that broken behavior in WebKit
if (!dom.select('div.mcePaste > div.mcePaste').length) {
nl = dom.select('div.mcePaste');
// WebKit will split the div into multiple ones so this will loop through then all and join them to get the whole HTML string
each(nl, function(n) {
var child = n.firstChild;
// WebKit inserts a DIV container with lots of odd styles
if (child && child.nodeName == 'DIV' && child.style.marginTop && child.style.backgroundColor) {
dom.remove(child, 1);
}
// Remove apply style spans
each(dom.select('span.Apple-style-span', n), function(n) {
dom.remove(n, 1);
});
// Remove bogus br elements
each(dom.select('br[data-mce-bogus]', n), function(n) {
dom.remove(n);
});
// WebKit will make a copy of the DIV for each line of plain text pasted and insert them into the DIV
if (n.parentNode.className != 'mcePaste')
h += n.innerHTML;
});
} else {
// Found WebKit weirdness so force the content into plain text mode
h = '
' + dom.encode(textContent).replace(/\r?\n/g, ''; } // Remove the nodes each(dom.select('div.mcePaste'), function(n) { dom.remove(n); }); // Restore the old selection if (or) sel.setRng(or); process({content : h}); // Unblock events ones we got the contents dom.unbind(ed.getDoc(), 'mousedown', block); dom.unbind(ed.getDoc(), 'keydown', block); }, 0); } } // Check if we should use the new auto process method if (getParam(ed, "paste_auto_cleanup_on_paste")) { // Is it's Opera or older FF use key handler if (tinymce.isOpera || /Firefox\/2/.test(navigator.userAgent)) { ed.onKeyDown.addToTop(function(ed, e) { if (((tinymce.isMac ? e.metaKey : e.ctrlKey) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45)) grabContent(e); }); } else { // Grab contents on paste event on Gecko and WebKit ed.onPaste.addToTop(function(ed, e) { return grabContent(e); }); } } // Block all drag/drop events if (getParam(ed, "paste_block_drop")) { ed.onInit.add(function() { ed.dom.bind(ed.getBody(), ['dragend', 'dragover', 'draggesture', 'dragdrop', 'drop', 'drag'], function(e) { e.preventDefault(); e.stopPropagation(); return false; }); }); } // Add legacy support t._legacySupport(); }, getInfo : function() { return { longname : 'Paste text/word', author : 'Moxiecode Systems AB', authorurl : 'http://tinymce.moxiecode.com', infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/paste', version : tinymce.majorVersion + "." + tinymce.minorVersion }; }, _preProcess : function(pl, o) { var ed = this.editor, h = o.content, grep = tinymce.grep, explode = tinymce.explode, trim = tinymce.trim, len, stripClass; //console.log('Before preprocess:' + o.content); function process(items) { each(items, function(v) { // Remove or replace if (v.constructor == RegExp) h = h.replace(v, ''); else h = h.replace(v[0], v[1]); }); } if (ed.settings.paste_enable_default_filters == false) { return; } // IE9 adds BRs before/after block elements when contents is pasted from word or for example another browser if (tinymce.isIE && document.documentMode >= 9) process([[/(?:
') + '
]*class="?MsoHeading"?[^>]*>(.*?)<\/p>/gi, "
$1
"); } if (getParam(ed, "paste_convert_middot_lists")) { process([ [//gi, '$&__MCE_ITEM__'], // Convert supportLists to a list item marker [/(]+(?:mso-list:|:\s*symbol)[^>]+>)/gi, '$1__MCE_ITEM__'], // Convert mso-list and symbol spans to item markers [/(]+(?:MsoListParagraph)[^>]+>)/gi, '$1__MCE_ITEM__'] // Convert mso-list and symbol paragraphs to item markers (FF)
]);
}
process([
// Word comments like conditional comments etc
//gi,
// Remove comments, scripts (e.g., msoShowComment), XML tag, VML content, MS Office namespaced tags, and a few other tags
/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,
// Convert "],
[/<\/h[1-6][^>]*>/gi, " into for line-though
[/<(\/?)s>/gi, "<$1strike>"],
// Replace nsbp entites to char since it's easier to handle
[/ /gi, "\u00a0"]
]);
// Remove bad attributes, with or without quotes, ensuring that attribute text is really inside a tag.
// 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.
do {
len = h.length;
h = h.replace(/(<[a-z][^>]*\s)(?:id|name|language|type|on\w+|\w+:\w+)=(?:"[^"]*"|\w+)\s?/gi, "$1");
} while (len != h.length);
// Remove all spans if no styles is to be retained
if (getParam(ed, "paste_retain_style_properties").replace(/^none$/i, "").length == 0) {
h = h.replace(/<\/?span[^>]*>/gi, "");
} else {
// We're keeping styles, so at least clean them up.
// CSS Reference: http://msdn.microsoft.com/en-us/library/aa155477.aspx
process([
// Convert ___ to string of alternating breaking/non-breaking spaces of same length
[/([\s\u00a0]*)<\/span>/gi,
function(str, spaces) {
return (spaces.length > 0)? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : "";
}
],
// Examine all styles: delete junk, transform some, and keep the rest
[/(<[a-z][^>]*)\sstyle="([^"]*)"/gi,
function(str, tag, style) {
var n = [],
i = 0,
s = explode(trim(style).replace(/"/gi, "'"), ";");
// Examine each style definition within the tag's style attribute
each(s, function(v) {
var name, value,
parts = explode(v, ":");
function ensureUnits(v) {
return v + ((v !== "0") && (/\d$/.test(v)))? "px" : "";
}
if (parts.length == 2) {
name = parts[0].toLowerCase();
value = parts[1].toLowerCase();
// Translate certain MS Office styles into their CSS equivalents
switch (name) {
case "mso-padding-alt":
case "mso-padding-top-alt":
case "mso-padding-right-alt":
case "mso-padding-bottom-alt":
case "mso-padding-left-alt":
case "mso-margin-alt":
case "mso-margin-top-alt":
case "mso-margin-right-alt":
case "mso-margin-bottom-alt":
case "mso-margin-left-alt":
case "mso-table-layout-alt":
case "mso-height":
case "mso-width":
case "mso-vertical-align-alt":
n[i++] = name.replace(/^mso-|-alt$/g, "") + ":" + ensureUnits(value);
return;
case "horiz-align":
n[i++] = "text-align:" + value;
return;
case "vert-align":
n[i++] = "vertical-align:" + value;
return;
case "font-color":
case "mso-foreground":
n[i++] = "color:" + value;
return;
case "mso-background":
case "mso-highlight":
n[i++] = "background:" + value;
return;
case "mso-default-height":
n[i++] = "min-height:" + ensureUnits(value);
return;
case "mso-default-width":
n[i++] = "min-width:" + ensureUnits(value);
return;
case "mso-padding-between-alt":
n[i++] = "border-collapse:separate;border-spacing:" + ensureUnits(value);
return;
case "text-line-through":
if ((value == "single") || (value == "double")) {
n[i++] = "text-decoration:line-through";
}
return;
case "mso-zero-height":
if (value == "yes") {
n[i++] = "display:none";
}
return;
}
// Eliminate all MS Office style definitions that have no CSS equivalent by examining the first characters in the name
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)) {
return;
}
// If it reached this point, it must be a valid CSS style
n[i++] = name + ":" + parts[1]; // Lower-case name, but keep value case
}
});
// If style attribute contained any valid styles the re-write it; otherwise delete style attribute.
if (i > 0) {
return tag + ' style="' + n.join(';') + '"';
} else {
return tag;
}
}
]
]);
}
}
// Replace headers with
if (getParam(ed, "paste_convert_headers_to_strong")) {
process([
[/
]*>|<\/tr>/gi, "\n"], // Single linebreak for
tags and table rows
[/<\/t[dh]>\s*
"]
]);
}
else {
process([
/^\s+|\s+$/g,
[/\n\n/g, "
"],
[/\n/g, "
"]
]);
}
// This next piece of code handles the situation where we're pasting more than one paragraph of plain
// text, and we are pasting the content into the middle of a block node in the editor. The block
// node gets split at the selection point into "Para A" and "Para B" (for the purposes of explaining).
// The first paragraph of the pasted text is appended to "Para A", and the last paragraph of the
// pasted text is prepended to "Para B". Any other paragraphs of pasted text are placed between
// "Para A" and "Para B". This code solves a host of problems with the original plain text plugin and
// now handles styles correctly. (Pasting plain text into a styled paragraph is supposed to make the
// plain text take the same style as the existing paragraph.)
if ((pos = h.indexOf("
")) != -1) { rpos = h.lastIndexOf("
");
node = sel.getNode();
breakElms = []; // Get list of elements to break
do {
if (node.nodeType == 1) {
// Don't break tables and break at body
if (node.nodeName == "TD" || node.nodeName == "BODY") {
break;
}
breakElms[breakElms.length] = node;
}
} while (node = node.parentNode);
// Are we in the middle of a block node?
if (breakElms.length > 0) {
before = h.substring(0, pos);
after = "";
for (i=0, len=breakElms.length; i