Quirks.js 4.85 KB
/**
 * Quirks.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/**
 * This class contains various fixes for browsers. These issues can not be feature
 * detected since we have no direct control over the clipboard. However we might be able
 * to remove some of these fixes once the browsers gets updated/fixed.
 *
 * @class tinymce.pasteplugin.Quirks
 * @private
 */
define("tinymce/pasteplugin/Quirks", [
	"tinymce/Env",
	"tinymce/util/Tools",
	"tinymce/pasteplugin/WordFilter",
	"tinymce/pasteplugin/Utils"
], function(Env, Tools, WordFilter, Utils) {
	"use strict";

	return function(editor) {
		function addPreProcessFilter(filterFunc) {
			editor.on('BeforePastePreProcess', function(e) {
				e.content = filterFunc(e.content);
			});
		}

		/**
		 * Removes BR elements after block elements. IE9 has a nasty bug where it puts a BR element after each
		 * block element when pasting from word. This removes those elements.
		 *
		 * This:
		 *  <p>a</p><br><p>b</p>
		 *
		 * Becomes:
		 *  <p>a</p><p>b</p>
		 */
		function removeExplorerBrElementsAfterBlocks(html) {
			// Only filter word specific content
			if (!WordFilter.isWordContent(html)) {
				return html;
			}

			// Produce block regexp based on the block elements in schema
			var blockElements = [];

			Tools.each(editor.schema.getBlockElements(), function(block, blockName) {
				blockElements.push(blockName);
			});

			var explorerBlocksRegExp = new RegExp(
				'(?:<br>&nbsp;[\\s\\r\\n]+|<br>)*(<\\/?(' + blockElements.join('|') + ')[^>]*>)(?:<br>&nbsp;[\\s\\r\\n]+|<br>)*',
				'g'
			);

			// Remove BR:s from: <BLOCK>X</BLOCK><BR>
			html = Utils.filter(html, [
				[explorerBlocksRegExp, '$1']
			]);

			// IE9 also adds an extra BR element for each soft-linefeed and it also adds a BR for each word wrap break
			html = Utils.filter(html, [
				[/<br><br>/g, '<BR><BR>'], // Replace multiple BR elements with uppercase BR to keep them intact
				[/<br>/g, ' '],            // Replace single br elements with space since they are word wrap BR:s
				[/<BR><BR>/g, '<br>']      // Replace back the double brs but into a single BR
			]);

			return html;
		}

		/**
		 * WebKit has a nasty bug where the all computed styles gets added to style attributes when copy/pasting contents.
		 * This fix solves that by simply removing the whole style attribute.
		 *
		 * The paste_webkit_styles option can be set to specify what to keep:
		 *  paste_webkit_styles: "none" // Keep no styles
		 *  paste_webkit_styles: "all", // Keep all of them
		 *  paste_webkit_styles: "font-weight color" // Keep specific ones
		 *
		 * @param {String} content Content that needs to be processed.
		 * @return {String} Processed contents.
		 */
		function removeWebKitStyles(content) {
			// Passthrough all styles from Word and let the WordFilter handle that junk
			if (WordFilter.isWordContent(content)) {
				return content;
			}

			// Filter away styles that isn't matching the target node
			var webKitStyles = editor.settings.paste_webkit_styles;

			if (editor.settings.paste_remove_styles_if_webkit === false || webKitStyles == "all") {
				return content;
			}

			if (webKitStyles) {
				webKitStyles = webKitStyles.split(/[, ]/);
			}

			// Keep specific styles that doesn't match the current node computed style
			if (webKitStyles) {
				var dom = editor.dom, node = editor.selection.getNode();

				content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, function(all, before, value, after) {
					var inputStyles = dom.parseStyle(value, 'span'), outputStyles = {};

					if (webKitStyles === "none") {
						return before + after;
					}

					for (var i = 0; i < webKitStyles.length; i++) {
						var inputValue = inputStyles[webKitStyles[i]], currentValue = dom.getStyle(node, webKitStyles[i], true);

						if (/color/.test(webKitStyles[i])) {
							inputValue = dom.toHex(inputValue);
							currentValue = dom.toHex(currentValue);
						}

						if (currentValue != inputValue) {
							outputStyles[webKitStyles[i]] = inputValue;
						}
					}

					outputStyles = dom.serializeStyle(outputStyles, 'span');
					if (outputStyles) {
						return before + ' style="' + outputStyles + '"' + after;
					}

					return before + after;
				});
			} else {
				// Remove all external styles
				content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, '$1$3');
			}

			// Keep internal styles
			content = content.replace(/(<[^>]+) data-mce-style="([^"]+)"([^>]*>)/gi, function(all, before, value, after) {
				return before + ' style="' + value + '"' + after;
			});

			return content;
		}

		// Sniff browsers and apply fixes since we can't feature detect
		if (Env.webkit) {
			addPreProcessFilter(removeWebKitStyles);
		}

		if (Env.ie) {
			addPreProcessFilter(removeExplorerBrElementsAfterBlocks);
		}
	};
});