2 * Compiled inline version. (Library mode)
5 /*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */
8 (function(exports, undefined) {
13 function require(ids, callback) {
14 var module, defs = [];
16 for (var i = 0; i < ids.length; ++i) {
17 module = modules[ids[i]] || resolve(ids[i]);
19 throw 'module definition dependecy not found: ' + ids[i];
25 callback.apply(null, defs);
28 function define(id, dependencies, definition) {
29 if (typeof id !== 'string') {
30 throw 'invalid module definition, module id must be defined and be a string';
33 if (dependencies === undefined) {
34 throw 'invalid module definition, dependencies must be specified';
37 if (definition === undefined) {
38 throw 'invalid module definition, definition function must be specified';
41 require(dependencies, function() {
42 modules[id] = definition.apply(null, arguments);
46 function defined(id) {
50 function resolve(id) {
52 var fragments = id.split(/[.\/]/);
54 for (var fi = 0; fi < fragments.length; ++fi) {
55 if (!target[fragments[fi]]) {
59 target = target[fragments[fi]];
65 function expose(ids) {
66 for (var i = 0; i < ids.length; i++) {
69 var fragments = id.split(/[.\/]/);
71 for (var fi = 0; fi < fragments.length - 1; ++fi) {
72 if (target[fragments[fi]] === undefined) {
73 target[fragments[fi]] = {};
76 target = target[fragments[fi]];
79 target[fragments[fragments.length - 1]] = modules[id];
83 // Included from: js/tinymce/plugins/paste/classes/Clipboard.js
88 * Copyright, Moxiecode Systems AB
89 * Released under LGPL License.
91 * License: http://www.tinymce.com/license
92 * Contributing: http://www.tinymce.com/contributing
96 * This class contains logic for getting HTML contents out of the clipboard.
98 * @class tinymce.pasteplugin.Clipboard
101 define("tinymce/pasteplugin/Clipboard", [
103 "tinymce/util/Tools",
105 ], function(Env, Tools, VK) {
106 function hasClipboardData() {
107 // Gecko is excluded until the fix: https://bugzilla.mozilla.org/show_bug.cgi?id=850663
108 return !Env.gecko && (("ClipboardEvent" in window) || (Env.webkit && "FocusEvent" in window));
111 return function(editor) {
112 var self = this, plainTextPasteTime;
115 return new Date().getTime();
118 function isPasteKeyEvent(e) {
119 // Ctrl+V or Shift+Insert
120 return (VK.metaKeyPressed(e) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45);
123 function innerText(elm) {
124 return elm.innerText || elm.textContent;
127 function shouldPasteAsPlainText() {
128 return now() - plainTextPasteTime < 100 || self.pasteFormat == "text";
131 // TODO: Move this to a class?
132 function process(content, items) {
133 Tools.each(items, function(v) {
134 if (v.constructor == RegExp) {
135 content = content.replace(v, '');
137 content = content.replace(v[0], v[1]);
144 function processHtml(html) {
145 var args = editor.fire('PastePreProcess', {content: html});
149 // Remove all data images from paste for example from Gecko
150 if (!editor.settings.paste_data_images) {
151 html = html.replace(/<img src=\"data:image[^>]+>/g, '');
154 if (editor.settings.paste_remove_styles || (editor.settings.paste_remove_styles_if_webkit !== false && Env.webkit)) {
155 html = html.replace(/ style=\"[^\"]+\"/g, '');
158 if (!args.isDefaultPrevented()) {
159 editor.insertContent(html);
163 function processText(text) {
164 text = editor.dom.encode(text).replace(/\r\n/g, '\n');
166 var startBlock = editor.dom.getParent(editor.selection.getStart(), editor.dom.isBlock);
168 if ((startBlock && /^(PRE|DIV)$/.test(startBlock.nodeName)) || !editor.settings.forced_root_block) {
169 text = process(text, [
173 text = process(text, [
174 [/\n\n/g, "</p><p>"],
175 [/^(.*<\/p>)(<p>)$/, '<p>$1'],
180 var args = editor.fire('PastePreProcess', {content: text});
182 if (!args.isDefaultPrevented()) {
183 editor.insertContent(args.content);
187 function createPasteBin() {
188 var scrollTop = editor.dom.getViewPort().y;
190 // Create a pastebin and move the selection into the bin
191 var pastebinElm = editor.dom.add(editor.getBody(), 'div', {
192 contentEditable: false,
193 "data-mce-bogus": "1",
194 style: 'position: absolute; top: ' + scrollTop + 'px; left: 0; width: 1px; height: 1px; overflow: hidden'
195 }, '<div contentEditable="true" data-mce-bogus="1">X</div>');
197 editor.dom.bind(pastebinElm, 'beforedeactivate focusin focusout', function(e) {
204 function removePasteBin(pastebinElm) {
205 editor.dom.unbind(pastebinElm);
206 editor.dom.remove(pastebinElm);
209 editor.on('keydown', function(e) {
211 if (VK.metaKeyPressed(e) && e.shiftKey && e.keyCode == 86) {
212 plainTextPasteTime = now();
216 // Use Clipboard API if it's available
217 if (hasClipboardData()) {
218 editor.on('paste', function(e) {
219 var clipboardData = e.clipboardData;
221 function processByContentType(contentType, processFunc) {
222 for (var ti = 0; ti < clipboardData.types.length; ti++) {
223 if (clipboardData.types[ti] == contentType) {
224 processFunc(clipboardData.getData(contentType));
225 //clipboardData.items[ti].getAsString(processFunc);
234 if (shouldPasteAsPlainText()) {
235 // First look for HTML then look for plain text
236 if (!processByContentType('text/plain', processText)) {
237 processByContentType('text/html', processHtml);
240 // First look for HTML then look for plain text
241 if (!processByContentType('text/html', processHtml)) {
242 processByContentType('text/plain', processText);
249 var keyPasteTime = 0;
251 editor.on('keydown', function(e) {
252 if (isPasteKeyEvent(e) && !e.isDefaultPrevented()) {
253 // Prevent undoManager keydown handler from making an undo level with the pastebin in it
254 e.stopImmediatePropagation();
256 var pastebinElm = createPasteBin();
257 keyPasteTime = now();
259 editor.dom.bind(pastebinElm, 'paste', function() {
260 setTimeout(function() {
261 editor.selection.setRng(lastRng);
262 removePasteBin(pastebinElm);
264 if (shouldPasteAsPlainText()) {
265 processText(innerText(pastebinElm.firstChild));
267 processHtml(pastebinElm.firstChild.innerHTML);
272 var lastRng = editor.selection.getRng();
273 pastebinElm.firstChild.focus();
274 pastebinElm.firstChild.innerText = '';
279 editor.on('init', function() {
280 var dom = editor.dom;
282 // Use a different method if the paste was made without using the keyboard
283 // for example using the browser menu items
284 editor.dom.bind(editor.getBody(), 'paste', function(e) {
285 if (now() - keyPasteTime > 100) {
286 var gotPasteEvent, pastebinElm = createPasteBin();
290 dom.bind(pastebinElm, 'paste', function(e) {
292 gotPasteEvent = true;
295 var lastRng = editor.selection.getRng();
297 // Select the container
298 var rng = dom.doc.body.createTextRange();
299 rng.moveToElementText(pastebinElm.firstChild);
300 rng.execCommand('Paste');
301 removePasteBin(pastebinElm);
303 if (!gotPasteEvent) {
304 editor.windowManager.alert('Please use Ctrl+V/Cmd+V keyboard shortcuts to paste contents.');
308 editor.selection.setRng(lastRng);
310 if (shouldPasteAsPlainText()) {
311 processText(innerText(pastebinElm.firstChild));
313 processHtml(pastebinElm.firstChild.innerHTML);
319 editor.on('init', function() {
320 editor.dom.bind(editor.getBody(), 'paste', function(e) {
322 editor.windowManager.alert('Please use Ctrl+V/Cmd+V keyboard shortcuts to paste contents.');
326 // Old Gecko/WebKit/Opera fallback
327 editor.on('keydown', function(e) {
328 if (isPasteKeyEvent(e) && !e.isDefaultPrevented()) {
329 // Prevent undoManager keydown handler from making an undo level with the pastebin in it
330 e.stopImmediatePropagation();
332 var pastebinElm = createPasteBin();
333 var lastRng = editor.selection.getRng();
335 editor.selection.select(pastebinElm, true);
337 editor.dom.bind(pastebinElm, 'paste', function(e) {
340 setTimeout(function() {
341 removePasteBin(pastebinElm);
342 editor.lastRng = lastRng;
343 editor.selection.setRng(lastRng);
345 var pastebinContents = pastebinElm.firstChild;
347 // Remove last BR Safari on Mac adds trailing BR
348 if (pastebinContents.lastChild && pastebinContents.lastChild.nodeName == 'BR') {
349 pastebinContents.removeChild(pastebinContents.lastChild);
352 if (shouldPasteAsPlainText()) {
353 processText(innerText(pastebinContents));
355 processHtml(pastebinContents.innerHTML);
363 // Prevent users from dropping data images on Gecko
364 if (!editor.settings.paste_data_images) {
365 editor.on('drop', function(e) {
366 var dataTransfer = e.dataTransfer;
368 if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
375 // Block all drag/drop events
376 if (editor.paste_block_drop) {
377 editor.on('dragend dragover draggesture dragdrop drop drag', function(e) {
383 this.paste = processHtml;
384 this.pasteText = processText;
388 // Included from: js/tinymce/plugins/paste/classes/WordFilter.js
393 * Copyright, Moxiecode Systems AB
394 * Released under LGPL License.
396 * License: http://www.tinymce.com/license
397 * Contributing: http://www.tinymce.com/contributing
401 * This class parses word HTML into proper TinyMCE markup.
403 * @class tinymce.pasteplugin.Quirks
406 define("tinymce/pasteplugin/WordFilter", [
407 "tinymce/util/Tools",
408 "tinymce/html/DomParser",
409 "tinymce/html/Schema",
410 "tinymce/html/Serializer",
412 ], function(Tools, DomParser, Schema, Serializer, Node) {
413 return function(editor) {
414 var each = Tools.each;
416 editor.on('PastePreProcess', function(e) {
417 var content = e.content, retainStyleProperties, validStyles;
419 retainStyleProperties = editor.settings.paste_retain_style_properties;
420 if (retainStyleProperties) {
421 validStyles = Tools.makeMap(retainStyleProperties);
424 function process(items) {
425 each(items, function(v) {
426 if (v.constructor == RegExp) {
427 content = content.replace(v, '');
429 content = content.replace(v[0], v[1]);
435 * Converts fake bullet and numbered lists to real semantic OL/UL.
437 * @param {tinymce.html.Node} node Root node to convert children of.
439 function convertFakeListsToProperLists(node) {
440 var currentListNode, prevListNode, lastLevel = 1;
442 function convertParagraphToLi(paragraphNode, listStartTextNode, listName, start) {
443 var level = paragraphNode._listLevel || lastLevel;
445 // Handle list nesting
446 if (level != lastLevel) {
447 if (level < lastLevel) {
448 // Move to parent list
449 if (currentListNode) {
450 currentListNode = currentListNode.parent.parent;
454 prevListNode = currentListNode;
455 currentListNode = null;
459 if (!currentListNode || currentListNode.name != listName) {
460 prevListNode = prevListNode || currentListNode;
461 currentListNode = new Node(listName, 1);
464 currentListNode.attr('start', '' + start);
467 paragraphNode.wrap(currentListNode);
469 currentListNode.append(paragraphNode);
472 paragraphNode.name = 'li';
473 listStartTextNode.value = '';
475 var nextNode = listStartTextNode.next;
476 if (nextNode && nextNode.type == 3) {
477 nextNode.value = nextNode.value.replace(/^\u00a0+/, '');
480 // Append list to previous list if it exists
481 if (level > lastLevel && prevListNode) {
482 prevListNode.lastChild.append(currentListNode);
488 var paragraphs = node.getAll('p');
490 for (var i = 0; i < paragraphs.length; i++) {
491 node = paragraphs[i];
493 if (node.name == 'p' && node.firstChild) {
494 // Find first text node in paragraph
496 var listStartTextNode = node.firstChild;
498 while (listStartTextNode) {
499 nodeText = listStartTextNode.value;
504 listStartTextNode = listStartTextNode.firstChild;
507 // Detect unordered lists look for bullets
508 if (/^\s*[\u2022\u00b7\u00a7\u00d8o\u25CF]\s*$/.test(nodeText)) {
509 convertParagraphToLi(node, listStartTextNode, 'ul');
513 // Detect ordered lists 1., a. or ixv.
514 if (/^\s*\w+\./.test(nodeText)) {
515 // Parse OL start number
516 var matches = /([0-9])\./.exec(nodeText);
519 start = parseInt(matches[1], 10);
522 convertParagraphToLi(node, listStartTextNode, 'ol', start);
526 currentListNode = null;
531 function filterStyles(node, styleValue) {
532 // Parse out list indent level for lists
533 if (node.name === 'p') {
534 var matches = /mso-list:\w+ \w+([0-9]+)/.exec(styleValue);
537 node._listLevel = parseInt(matches[1], 10);
541 if (editor.getParam("paste_retain_style_properties", "none")) {
542 var outputStyle = "";
544 Tools.each(editor.dom.parseStyle(styleValue), function(value, name) {
545 // Convert various MS styles to W3C styles
552 name = "vertical-align";
556 case "mso-foreground":
560 case "mso-background":
561 case "mso-highlight":
566 // Output only valid styles
567 if (retainStyleProperties == "all" || (validStyles && validStyles[name])) {
568 outputStyle += name + ':' + value + ';';
580 if (editor.settings.paste_enable_default_filters === false) {
584 // Detect is the contents is Word junk HTML
585 if (/class="?Mso|style="[^"]*\bmso-|style='[^'']*\bmso-|w:WordDocument/i.test(e.content)) {
586 e.wordContent = true; // Mark it for other processors
588 // Remove basic Word junk
590 // Word comments like conditional comments etc
593 // Remove comments, scripts (e.g., msoShowComment), XML tag, VML content,
594 // MS Office namespaced tags, and a few other tags
595 /<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,
597 // Convert <s> into <strike> for line-though
598 [/<(\/?)s>/gi, "<$1strike>"],
600 // Replace nsbp entites to char since it's easier to handle
601 [/ /gi, "\u00a0"],
603 // Convert <span style="mso-spacerun:yes">___</span> to string of alternating
604 // breaking/non-breaking spaces of same length
605 [/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi,
606 function(str, spaces) {
607 return (spaces.length > 0) ?
608 spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : "";
613 // Setup strict schema
614 var schema = new Schema({
615 valid_elements: '@[style],-strong/b,-em/i,-span,-p,-ol,-ul,-li,-h1,-h2,-h3,-h4,-h5,-h6,-table,' +
616 '-tr,-td[colspan|rowspan],-th,-thead,-tfoot,-tbody,-a[!href],sub,sup,strike'
619 // Parse HTML into DOM structure
620 var domParser = new DomParser({}, schema);
622 // Filte element style attributes
623 domParser.addAttributeFilter('style', function(nodes) {
624 var i = nodes.length, node;
628 node.attr('style', filterStyles(node, node.attr('style')));
630 // Remove pointess spans
631 if (node.name == 'span' && !node.attributes.length) {
637 // Parse into DOM structure
638 var rootNode = domParser.parse(content);
641 convertFakeListsToProperLists(rootNode);
643 // Serialize DOM back to HTML
644 e.content = new Serializer({}, schema).serialize(rootNode);
650 // Included from: js/tinymce/plugins/paste/classes/Quirks.js
655 * Copyright, Moxiecode Systems AB
656 * Released under LGPL License.
658 * License: http://www.tinymce.com/license
659 * Contributing: http://www.tinymce.com/contributing
663 * This class contains various fixes for browsers. These issues can not be feature
664 * detected since we have no direct control over the clipboard. However we might be able
665 * to remove some of these fixes once the browsers gets updated/fixed.
667 * @class tinymce.pasteplugin.Quirks
670 define("tinymce/pasteplugin/Quirks", [
673 ], function(Env, Tools) {
676 return function(editor) {
677 var explorerBlocksRegExp;
679 function addPreProcessFilter(filterFunc) {
680 editor.on('PastePreProcess', function(e) {
681 e.content = filterFunc(e.content);
685 function process(content, items) {
686 Tools.each(items, function(v) {
687 if (v.constructor == RegExp) {
688 content = content.replace(v, '');
690 content = content.replace(v[0], v[1]);
698 * Removes WebKit fragment comments and converted-space spans.
701 * <!--StartFragment-->a<span class="Apple-converted-space"> </span>b<!--EndFragment-->
706 function removeWebKitFragments(html) {
707 html = process(html, [
708 /^[\s\S]*<!--StartFragment-->|<!--EndFragment-->[\s\S]*$/g, // WebKit fragment
709 [/<span class="Apple-converted-space">\u00a0<\/span>/g, '\u00a0'], // WebKit
710 /<br>$/ // Traling BR elements
717 * Removes BR elements after block elements. IE9 has a nasty bug where it puts a BR element after each
718 * block element when pasting from word. This removes those elements.
721 * <p>a</p><br><p>b</p>
726 function removeExplorerBrElementsAfterBlocks(html) {
727 // Produce block regexp based on the block elements in schema
728 if (!explorerBlocksRegExp) {
729 var blockElements = [];
731 Tools.each(editor.schema.getBlockElements(), function(block, blockName) {
732 blockElements.push(blockName);
735 explorerBlocksRegExp = new RegExp(
736 '(?:<br> [\\s\\r\\n]+|<br>)*(<\\/?(' + blockElements.join('|') + ')[^>]*>)(?:<br> [\\s\\r\\n]+|<br>)*',
741 // Remove BR:s from: <BLOCK>X</BLOCK><BR>
742 html = process(html, [
743 [explorerBlocksRegExp, '$1']
746 // IE9 also adds an extra BR element for each soft-linefeed and it also adds a BR for each word wrap break
747 html = process(html, [
748 [/<br><br>/g, '<BR><BR>'], // Replace multiple BR elements with uppercase BR to keep them intact
749 [/<br>/g, ' '], // Replace single br elements with space since they are word wrap BR:s
750 [/<BR><BR>/g, '<br>'] // Replace back the double brs but into a single BR
756 // Sniff browsers and apply fixes since we can't feature detect
758 addPreProcessFilter(removeWebKitFragments);
762 addPreProcessFilter(removeExplorerBrElementsAfterBlocks);
767 // Included from: js/tinymce/plugins/paste/classes/Plugin.js
772 * Copyright, Moxiecode Systems AB
773 * Released under LGPL License.
775 * License: http://www.tinymce.com/license
776 * Contributing: http://www.tinymce.com/contributing
780 * This class contains the tinymce plugin logic for the paste plugin.
782 * @class tinymce.pasteplugin.Plugin
785 define("tinymce/pasteplugin/Plugin", [
786 "tinymce/PluginManager",
787 "tinymce/pasteplugin/Clipboard",
788 "tinymce/pasteplugin/WordFilter",
789 "tinymce/pasteplugin/Quirks"
790 ], function(PluginManager, Clipboard, WordFilter, Quirks) {
793 PluginManager.add('paste', function(editor) {
794 var self = this, clipboard;
796 function togglePlainTextPaste() {
797 if (clipboard.pasteFormat == "text") {
799 clipboard.pasteFormat = "html";
801 clipboard.pasteFormat = "text";
804 if (!userIsInformed) {
805 editor.windowManager.alert(
806 'Paste is now in plain text mode. Contents will now ' +
807 'be pasted as plain text until you toggle this option off.'
810 userIsInformed = true;
815 self.clipboard = clipboard = new Clipboard(editor);
816 self.quirks = new Quirks(editor);
817 self.wordFilter = new WordFilter(editor);
819 if (editor.settings.paste_as_text) {
820 self.clipboard.pasteFormat = "text";
823 editor.addCommand('mceInsertClipboardContent', function(ui, value) {
825 self.clipboard.paste(value.content);
829 self.clipboard.pasteText(value.text);
833 editor.addButton('pastetext', {
835 tooltip: 'Paste as text',
836 onclick: togglePlainTextPaste,
837 active: self.clipboard.pasteFormat == "text"
840 editor.addMenuItem('pastetext', {
841 text: 'Paste as text',
843 active: clipboard.pasteFormat,
844 onclick: togglePlainTextPaste
849 expose(["tinymce/pasteplugin/Clipboard","tinymce/pasteplugin/WordFilter","tinymce/pasteplugin/Quirks","tinymce/pasteplugin/Plugin"]);