This commit is contained in:
2025-10-22 15:39:40 +08:00
commit b0b510fac1
2720 changed files with 415933 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Plugin: paste Demo Page</title>
</head>
<body>
<h2>Plugin: paste Demo Page</h2>
<div id="ephox-ui">
<textarea name="" id="" cols="30" rows="10" class="tinymce"></textarea>
</div>
<script src="../../../../../js/tinymce/tinymce.js"></script>
<script src="../../../../../scratch/demos/plugins/paste/demo.js"></script>
</body>
</html>

View File

@@ -0,0 +1,33 @@
/**
* Demo.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
// tslint:disable:no-console
declare let tinymce: any;
tinymce.init({
selector: 'textarea.tinymce',
theme: 'modern',
skin_url: '../../../../../js/tinymce/skins/lightgray',
plugins: 'paste code',
toolbar: 'undo redo | pastetext code',
init_instance_callback (editor) {
editor.on('PastePreProcess', function (evt) {
console.log(evt);
});
editor.on('PastePostProcess', function (evt) {
console.log(evt);
});
},
height: 600
});
export {};

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import { Cell } from '@ephox/katamari';
import PluginManager from 'tinymce/core/api/PluginManager';
import DetectProPlugin from './alien/DetectProPlugin';
import Api from './api/Api';
import Commands from './api/Commands';
import { Clipboard } from './api/Clipboard';
import CutCopy from './core/CutCopy';
import DragDrop from './core/DragDrop';
import PrePostProcess from './core/PrePostProcess';
import Quirks from './core/Quirks';
import Buttons from './ui/Buttons';
import { Editor } from 'tinymce/core/api/Editor';
import Settings from 'tinymce/plugins/paste/api/Settings';
PluginManager.add('paste', function (editor: Editor) {
if (DetectProPlugin.hasProPlugin(editor) === false) {
const userIsInformedState = Cell(false);
const draggingInternallyState = Cell(false);
const pasteFormat = Cell(Settings.isPasteAsTextEnabled(editor) ? 'text' : 'html');
const clipboard = Clipboard(editor, pasteFormat);
const quirks = Quirks.setup(editor);
Buttons.register(editor, clipboard);
Commands.register(editor, clipboard, userIsInformedState);
PrePostProcess.setup(editor);
CutCopy.register(editor);
DragDrop.setup(editor, clipboard, draggingInternallyState);
return Api.get(clipboard, quirks);
}
});
export default function () { }

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import PluginManager from 'tinymce/core/api/PluginManager';
import { Editor } from 'tinymce/core/api/Editor';
import { window } from '@ephox/dom-globals';
const hasProPlugin = function (editor: Editor) {
// draw back if power version is requested and registered
if (/(^|[ ,])powerpaste([, ]|$)/.test(editor.settings.plugins) && PluginManager.get('powerpaste')) {
if (typeof window.console !== 'undefined' && window.console.log) {
window.console.log('PowerPaste is incompatible with Paste plugin! Remove \'paste\' from the \'plugins\' option.');
}
return true;
} else {
return false;
}
};
export default {
hasProPlugin
};

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import { Clipboard } from '../api/Clipboard';
const get = function (clipboard: Clipboard, quirks) {
return {
clipboard,
quirks
};
};
export default {
get
};

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import { ClipboardContents, registerEventsAndFilters, pasteHtml, pasteText, pasteImageData, getDataTransferItems, hasContentType, hasHtmlOrText } from '../core/Clipboard';
import { PasteBin } from '../core/PasteBin';
import { Cell } from '@ephox/katamari';
import { Editor } from 'tinymce/core/api/Editor';
import { ClipboardEvent, DragEvent, Range, DataTransfer } from '@ephox/dom-globals';
export interface Clipboard {
pasteFormat: Cell<string>;
pasteHtml: (html: string, internalFlag: boolean) => void;
pasteText: (text: string) => void;
pasteImageData: (e: ClipboardEvent | DragEvent, rng: Range) => boolean;
getDataTransferItems: (dataTransfer: DataTransfer) => ClipboardContents;
hasHtmlOrText: (content: ClipboardContents) => boolean;
hasContentType: (clipboardContent: ClipboardContents, mimeType: string) => boolean;
}
export const Clipboard = (editor: Editor, pasteFormat: Cell<string>): Clipboard => {
const pasteBin = PasteBin(editor);
editor.on('preInit', () => registerEventsAndFilters(editor, pasteBin, pasteFormat));
return {
pasteFormat,
pasteHtml: (html: string, internalFlag: boolean) => pasteHtml(editor, html, internalFlag),
pasteText: (text: string) => pasteText(editor, text),
pasteImageData: (e: ClipboardEvent | DragEvent, rng: Range) => pasteImageData(editor, e, rng),
getDataTransferItems,
hasHtmlOrText,
hasContentType
};
};

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import Actions from '../core/Actions';
import { Editor } from 'tinymce/core/api/Editor';
import { Clipboard } from '../api/Clipboard';
const register = function (editor: Editor, clipboard: Clipboard, userIsInformedState) {
editor.addCommand('mceTogglePlainTextPaste', function () {
Actions.togglePlainTextPaste(editor, clipboard, userIsInformedState);
});
editor.addCommand('mceInsertClipboardContent', function (ui, value) {
if (value.content) {
clipboard.pasteHtml(value.content, value.internal);
}
if (value.text) {
clipboard.pasteText(value.text);
}
});
};
export default {
register
};

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import { Editor } from 'tinymce/core/api/Editor';
import { HTMLElement } from '@ephox/dom-globals';
const firePastePreProcess = function (editor: Editor, html: string, internal: boolean, isWordHtml: boolean) {
return editor.fire('PastePreProcess', { content: html, internal, wordContent: isWordHtml });
};
const firePastePostProcess = function (editor: Editor, node: HTMLElement, internal: boolean, isWordHtml: boolean) {
return editor.fire('PastePostProcess', { node, internal, wordContent: isWordHtml });
};
const firePastePlainTextToggle = function (editor: Editor, state: boolean) {
return editor.fire('PastePlainTextToggle', { state });
};
const firePaste = function (editor: Editor, ieFake: boolean) {
return editor.fire('paste', { ieFake });
};
export default {
firePastePreProcess,
firePastePostProcess,
firePastePlainTextToggle,
firePaste
};

View File

@@ -0,0 +1,94 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import { Editor } from 'tinymce/core/api/Editor';
const shouldPlainTextInform = (editor: Editor): boolean => {
return editor.getParam('paste_plaintext_inform', true);
};
const shouldBlockDrop = (editor: Editor): boolean => {
return editor.getParam('paste_block_drop', false);
};
const shouldPasteDataImages = (editor: Editor): boolean => {
return editor.getParam('paste_data_images', false);
};
const shouldFilterDrop = (editor: Editor): boolean => {
return editor.getParam('paste_filter_drop', true);
};
type ProcessFn = (plugin, args) => void;
const getPreProcess = (editor: Editor): ProcessFn => {
return editor.getParam('paste_preprocess');
};
const getPostProcess = (editor: Editor): ProcessFn => {
return editor.getParam('paste_postprocess');
};
const getWebkitStyles = (editor: Editor): string => {
return editor.getParam('paste_webkit_styles');
};
const shouldRemoveWebKitStyles = (editor: Editor): boolean => {
return editor.getParam('paste_remove_styles_if_webkit', true);
};
const shouldMergeFormats = (editor: Editor): boolean => {
return editor.getParam('paste_merge_formats', true);
};
const isSmartPasteEnabled = (editor: Editor): boolean => {
return editor.getParam('smart_paste', true);
};
const isPasteAsTextEnabled = (editor: Editor): boolean => {
return editor.getParam('paste_as_text', false);
};
const getRetainStyleProps = (editor: Editor): string => {
return editor.getParam('paste_retain_style_properties');
};
const getWordValidElements = (editor: Editor): string => {
const defaultValidElements = (
'-strong/b,-em/i,-u,-span,-p,-ol,-ul,-li,-h1,-h2,-h3,-h4,-h5,-h6,' +
'-p/div,-a[href|name],sub,sup,strike,br,del,table[width],tr,' +
'td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody'
);
return editor.getParam('paste_word_valid_elements', defaultValidElements);
};
const shouldConvertWordFakeLists = (editor: Editor): boolean => {
return editor.getParam('paste_convert_word_fake_lists', true);
};
const shouldUseDefaultFilters = (editor: Editor): boolean => {
return editor.getParam('paste_enable_default_filters', true);
};
export default {
shouldPlainTextInform,
shouldBlockDrop,
shouldPasteDataImages,
shouldFilterDrop,
getPreProcess,
getPostProcess,
getWebkitStyles,
shouldRemoveWebKitStyles,
shouldMergeFormats,
isSmartPasteEnabled,
isPasteAsTextEnabled,
getRetainStyleProps,
getWordValidElements,
shouldConvertWordFakeLists,
shouldUseDefaultFilters
};

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import Events from '../api/Events';
import Settings from '../api/Settings';
import { Editor } from 'tinymce/core/api/Editor';
import { Clipboard } from '../api/Clipboard';
const shouldInformUserAboutPlainText = function (editor: Editor, userIsInformedState) {
return userIsInformedState.get() === false && Settings.shouldPlainTextInform(editor);
};
const displayNotification = function (editor: Editor, message: string) {
editor.notificationManager.open({
text: editor.translate(message),
type: 'info'
});
};
const togglePlainTextPaste = function (editor: Editor, clipboard: Clipboard, userIsInformedState) {
if (clipboard.pasteFormat.get() === 'text') {
clipboard.pasteFormat.set('html');
Events.firePastePlainTextToggle(editor, false);
} else {
clipboard.pasteFormat.set('text');
Events.firePastePlainTextToggle(editor, true);
if (shouldInformUserAboutPlainText(editor, userIsInformedState)) {
displayNotification(editor, 'Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.');
userIsInformedState.set(true);
}
}
editor.focus();
};
export default {
togglePlainTextPaste
};

View File

@@ -0,0 +1,484 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import Env from 'tinymce/core/api/Env';
import Delay from 'tinymce/core/api/util/Delay';
import Tools from 'tinymce/core/api/util/Tools';
import VK from 'tinymce/core/api/util/VK';
import Events from '../api/Events';
import InternalHtml from './InternalHtml';
import Newlines from './Newlines';
import { PasteBin } from './PasteBin';
import ProcessFilters from './ProcessFilters';
import SmartPaste from './SmartPaste';
import Utils from './Utils';
import { Editor } from 'tinymce/core/api/Editor';
import { Cell, Futures, Future, Arr } from '@ephox/katamari';
import { DataTransfer, ClipboardEvent, HTMLImageElement, Range, Image, Event, DragEvent, navigator, KeyboardEvent, File } from '@ephox/dom-globals';
declare let window: any;
/**
* Pastes the specified HTML. This means that the HTML is filtered and then
* inserted at the current selection in the editor. It will also fire paste events
* for custom user filtering.
*
* @param {String} html HTML code to paste into the current selection.
* @param {Boolean?} internalFlag Optional true/false flag if the contents is internal or external.
*/
const pasteHtml = (editor: Editor, html: string, internalFlag: boolean) => {
const internal = internalFlag ? internalFlag : InternalHtml.isMarked(html);
const args = ProcessFilters.process(editor, InternalHtml.unmark(html), internal);
if (args.cancelled === false) {
SmartPaste.insertContent(editor, args.content);
}
};
/**
* Pastes the specified text. This means that the plain text is processed
* and converted into BR and P elements. It will fire paste events for custom filtering.
*
* @param {String} text Text to paste as the current selection location.
*/
const pasteText = (editor, text: string) => {
text = editor.dom.encode(text).replace(/\r\n/g, '\n');
text = Newlines.convert(text, editor.settings.forced_root_block, editor.settings.forced_root_block_attrs);
pasteHtml(editor, text, false);
};
export interface ClipboardContents {
[key: string]: string;
}
/**
* Gets various content types out of a datatransfer object.
*
* @param {DataTransfer} dataTransfer Event fired on paste.
* @return {Object} Object with mime types and data for those mime types.
*/
const getDataTransferItems = (dataTransfer: DataTransfer): ClipboardContents => {
const items = {};
const mceInternalUrlPrefix = 'data:text/mce-internal,';
if (dataTransfer) {
// Use old WebKit/IE API
if (dataTransfer.getData) {
const legacyText = dataTransfer.getData('Text');
if (legacyText && legacyText.length > 0) {
if (legacyText.indexOf(mceInternalUrlPrefix) === -1) {
items['text/plain'] = legacyText;
}
}
}
if (dataTransfer.types) {
for (let i = 0; i < dataTransfer.types.length; i++) {
const contentType = dataTransfer.types[i];
try { // IE11 throws exception when contentType is Files (type is present but data cannot be retrieved via getData())
items[contentType] = dataTransfer.getData(contentType);
} catch (ex) {
items[contentType] = ''; // useless in general, but for consistency across browsers
}
}
}
}
return items;
};
/**
* Gets various content types out of the Clipboard API. It will also get the
* plain text using older IE and WebKit API:s.
*
* @param {ClipboardEvent} clipboardEvent Event fired on paste.
* @return {Object} Object with mime types and data for those mime types.
*/
const getClipboardContent = (editor: Editor, clipboardEvent: ClipboardEvent) => {
const content = getDataTransferItems(clipboardEvent.clipboardData || (editor.getDoc() as any).dataTransfer);
// Edge 15 has a broken HTML Clipboard API see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/11877517/
return Utils.isMsEdge() ? Tools.extend(content, { 'text/html': '' }) : content;
};
const hasContentType = (clipboardContent: ClipboardContents, mimeType: string) => {
return mimeType in clipboardContent && clipboardContent[mimeType].length > 0;
};
const hasHtmlOrText = (content: ClipboardContents) => {
return hasContentType(content, 'text/html') || hasContentType(content, 'text/plain');
};
const getBase64FromUri = (uri: string) => {
let idx;
idx = uri.indexOf(',');
if (idx !== -1) {
return uri.substr(idx + 1);
}
return null;
};
const isValidDataUriImage = (settings, imgElm: HTMLImageElement) => {
return settings.images_dataimg_filter ? settings.images_dataimg_filter(imgElm) : true;
};
const extractFilename = (editor: Editor, str: string) => {
const m = str.match(/([\s\S]+?)\.(?:jpeg|jpg|png|gif)$/i);
return m ? editor.dom.encode(m[1]) : null;
};
const uniqueId = Utils.createIdGenerator('mceclip');
const pasteImage = (editor: Editor, imageItem) => {
const base64 = getBase64FromUri(imageItem.uri);
const id = uniqueId();
const name = editor.settings.images_reuse_filename && imageItem.blob.name ? extractFilename(editor, imageItem.blob.name) : id;
const img = new Image();
img.src = imageItem.uri;
// TODO: Move the bulk of the cache logic to EditorUpload
if (isValidDataUriImage(editor.settings, img)) {
const blobCache = editor.editorUpload.blobCache;
let blobInfo, existingBlobInfo;
existingBlobInfo = blobCache.findFirst(function (cachedBlobInfo) {
return cachedBlobInfo.base64() === base64;
});
if (!existingBlobInfo) {
blobInfo = blobCache.create(id, imageItem.blob, base64, name);
blobCache.add(blobInfo);
} else {
blobInfo = existingBlobInfo;
}
pasteHtml(editor, '<img src="' + blobInfo.blobUri() + '">', false);
} else {
pasteHtml(editor, '<img src="' + imageItem.uri + '">', false);
}
};
const isClipboardEvent = (event: Event): event is ClipboardEvent => event.type === 'paste';
const readBlobsAsDataUris = (items: File[]) => {
return Futures.mapM(items, (item: any) => {
return Future.nu((resolve) => {
const blob = item.getAsFile ? item.getAsFile() : item;
const reader = new window.FileReader();
reader.onload = () => {
resolve({
blob,
uri: reader.result
});
};
reader.readAsDataURL(blob);
});
});
};
const getImagesFromDataTransfer = (dataTransfer: DataTransfer) => {
const items = dataTransfer.items ? Arr.map(Arr.from(dataTransfer.items), (item) => item.getAsFile()) : [];
const files = dataTransfer.files ? Arr.from(dataTransfer.files) : [];
const images = Arr.filter(items.length > 0 ? items : files, (file) => /^image\/(jpeg|png|gif|bmp)$/.test(file.type));
return images;
};
/**
* Checks if the clipboard contains image data if it does it will take that data
* and convert it into a data url image and paste that image at the caret location.
*
* @param {ClipboardEvent} e Paste/drop event object.
* @param {DOMRange} rng Rng object to move selection to.
* @return {Boolean} true/false if the image data was found or not.
*/
const pasteImageData = (editor, e: ClipboardEvent | DragEvent, rng: Range) => {
const dataTransfer = isClipboardEvent(e) ? e.clipboardData : e.dataTransfer;
if (editor.settings.paste_data_images && dataTransfer) {
const images = getImagesFromDataTransfer(dataTransfer);
if (images.length > 0) {
e.preventDefault();
readBlobsAsDataUris(images).get((blobResults) => {
if (rng) {
editor.selection.setRng(rng);
}
Arr.each(blobResults, (result) => {
pasteImage(editor, result);
});
});
return true;
}
}
return false;
};
/**
* Chrome on Android doesn't support proper clipboard access so we have no choice but to allow the browser default behavior.
*
* @param {Event} e Paste event object to check if it contains any data.
* @return {Boolean} true/false if the clipboard is empty or not.
*/
const isBrokenAndroidClipboardEvent = (e: ClipboardEvent) => {
const clipboardData = e.clipboardData;
return navigator.userAgent.indexOf('Android') !== -1 && clipboardData && clipboardData.items && clipboardData.items.length === 0;
};
const isKeyboardPasteEvent = (e: KeyboardEvent) => {
return (VK.metaKeyPressed(e) && e.keyCode === 86) || (e.shiftKey && e.keyCode === 45);
};
const registerEventHandlers = (editor: Editor, pasteBin: PasteBin, pasteFormat: Cell<string>) => {
let keyboardPasteTimeStamp = 0;
let keyboardPastePlainTextState;
editor.on('keydown', function (e) {
function removePasteBinOnKeyUp(e) {
// Ctrl+V or Shift+Insert
if (isKeyboardPasteEvent(e) && !e.isDefaultPrevented()) {
pasteBin.remove();
}
}
// Ctrl+V or Shift+Insert
if (isKeyboardPasteEvent(e) && !e.isDefaultPrevented()) {
keyboardPastePlainTextState = e.shiftKey && e.keyCode === 86;
// Edge case on Safari on Mac where it doesn't handle Cmd+Shift+V correctly
// it fires the keydown but no paste or keyup so we are left with a paste bin
if (keyboardPastePlainTextState && Env.webkit && navigator.userAgent.indexOf('Version/') !== -1) {
return;
}
// Prevent undoManager keydown handler from making an undo level with the pastebin in it
e.stopImmediatePropagation();
keyboardPasteTimeStamp = new Date().getTime();
// IE doesn't support Ctrl+Shift+V and it doesn't even produce a paste event
// so lets fake a paste event and let IE use the execCommand/dataTransfer methods
if (Env.ie && keyboardPastePlainTextState) {
e.preventDefault();
Events.firePaste(editor, true);
return;
}
pasteBin.remove();
pasteBin.create();
// Remove pastebin if we get a keyup and no paste event
// For example pasting a file in IE 11 will not produce a paste event
editor.once('keyup', removePasteBinOnKeyUp);
editor.once('paste', function () {
editor.off('keyup', removePasteBinOnKeyUp);
});
}
});
function insertClipboardContent(clipboardContent, isKeyBoardPaste, plainTextMode, internal) {
let content, isPlainTextHtml;
// Grab HTML from Clipboard API or paste bin as a fallback
if (hasContentType(clipboardContent, 'text/html')) {
content = clipboardContent['text/html'];
} else {
content = pasteBin.getHtml();
internal = internal ? internal : InternalHtml.isMarked(content);
// If paste bin is empty try using plain text mode
// since that is better than nothing right
if (pasteBin.isDefaultContent(content)) {
plainTextMode = true;
}
}
content = Utils.trimHtml(content);
pasteBin.remove();
isPlainTextHtml = (internal === false && Newlines.isPlainText(content));
// If we got nothing from clipboard API and pastebin or the content is a plain text (with only
// some BRs, Ps or DIVs as newlines) then we fallback to plain/text
if (!content.length || isPlainTextHtml) {
plainTextMode = true;
}
// Grab plain text from Clipboard API or convert existing HTML to plain text
if (plainTextMode) {
// Use plain text contents from Clipboard API unless the HTML contains paragraphs then
// we should convert the HTML to plain text since works better when pasting HTML/Word contents as plain text
if (hasContentType(clipboardContent, 'text/plain') && isPlainTextHtml) {
content = clipboardContent['text/plain'];
} else {
content = Utils.innerText(content);
}
}
// If the content is the paste bin default HTML then it was
// impossible to get the cliboard data out.
if (pasteBin.isDefaultContent(content)) {
if (!isKeyBoardPaste) {
editor.windowManager.alert('Please use Ctrl+V/Cmd+V keyboard shortcuts to paste contents.');
}
return;
}
if (plainTextMode) {
pasteText(editor, content);
} else {
pasteHtml(editor, content, internal);
}
}
const getLastRng = function () {
return pasteBin.getLastRng() || editor.selection.getRng();
};
editor.on('paste', function (e) {
// Getting content from the Clipboard can take some time
const clipboardTimer = new Date().getTime();
const clipboardContent = getClipboardContent(editor, e);
const clipboardDelay = new Date().getTime() - clipboardTimer;
const isKeyBoardPaste = (new Date().getTime() - keyboardPasteTimeStamp - clipboardDelay) < 1000;
const plainTextMode = pasteFormat.get() === 'text' || keyboardPastePlainTextState;
let internal = hasContentType(clipboardContent, InternalHtml.internalHtmlMime());
keyboardPastePlainTextState = false;
if (e.isDefaultPrevented() || isBrokenAndroidClipboardEvent(e)) {
pasteBin.remove();
return;
}
if (!hasHtmlOrText(clipboardContent) && pasteImageData(editor, e, getLastRng())) {
pasteBin.remove();
return;
}
// Not a keyboard paste prevent default paste and try to grab the clipboard contents using different APIs
if (!isKeyBoardPaste) {
e.preventDefault();
}
// Try IE only method if paste isn't a keyboard paste
if (Env.ie && (!isKeyBoardPaste || e.ieFake) && !hasContentType(clipboardContent, 'text/html')) {
pasteBin.create();
editor.dom.bind(pasteBin.getEl(), 'paste', function (e) {
e.stopPropagation();
});
editor.getDoc().execCommand('Paste', false, null);
clipboardContent['text/html'] = pasteBin.getHtml();
}
// If clipboard API has HTML then use that directly
if (hasContentType(clipboardContent, 'text/html')) {
e.preventDefault();
// if clipboard lacks internal mime type, inspect html for internal markings
if (!internal) {
internal = InternalHtml.isMarked(clipboardContent['text/html']);
}
insertClipboardContent(clipboardContent, isKeyBoardPaste, plainTextMode, internal);
} else {
Delay.setEditorTimeout(editor, function () {
insertClipboardContent(clipboardContent, isKeyBoardPaste, plainTextMode, internal);
}, 0);
}
});
};
/**
* This class contains logic for getting HTML contents out of the clipboard.
*
* We need to make a lot of ugly hacks to get the contents out of the clipboard since
* the W3C Clipboard API is broken in all browsers that have it: Gecko/WebKit/Blink.
* We might rewrite this the way those API:s stabilize. Browsers doesn't handle pasting
* from applications like Word the same way as it does when pasting into a contentEditable area
* so we need to do lots of extra work to try to get to this clipboard data.
*
* Current implementation steps:
* 1. On keydown with paste keys Ctrl+V or Shift+Insert create
* a paste bin element and move focus to that element.
* 2. Wait for the browser to fire a "paste" event and get the contents out of the paste bin.
* 3. Check if the paste was successful if true, process the HTML.
* (4). If the paste was unsuccessful use IE execCommand, Clipboard API, document.dataTransfer old WebKit API etc.
*
* @class tinymce.pasteplugin.Clipboard
* @private
*/
const registerEventsAndFilters = (editor: Editor, pasteBin: PasteBin, pasteFormat: Cell<string>) => {
registerEventHandlers(editor, pasteBin, pasteFormat);
let src;
// Remove all data images from paste for example from Gecko
// except internal images like video elements
editor.parser.addNodeFilter('img', (nodes, name, args) => {
const isPasteInsert = (args) => {
return args.data && args.data.paste === true;
};
const remove = (node) => {
if (!node.attr('data-mce-object') && src !== Env.transparentSrc) {
node.remove();
}
};
const isWebKitFakeUrl = (src) => {
return src.indexOf('webkit-fake-url') === 0;
};
const isDataUri = (src) => {
return src.indexOf('data:') === 0;
};
if (!editor.settings.paste_data_images && isPasteInsert(args)) {
let i = nodes.length;
while (i--) {
src = nodes[i].attributes.map.src;
if (!src) {
continue;
}
// Safari on Mac produces webkit-fake-url see: https://bugs.webkit.org/show_bug.cgi?id=49141
if (isWebKitFakeUrl(src)) {
remove(nodes[i]);
} else if (!editor.settings.allow_html_data_urls && isDataUri(src)) {
remove(nodes[i]);
}
}
}
});
};
export {
registerEventsAndFilters,
pasteHtml,
pasteText,
pasteImageData,
getDataTransferItems,
hasHtmlOrText,
hasContentType
};

View File

@@ -0,0 +1,127 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import Env from 'tinymce/core/api/Env';
import InternalHtml from './InternalHtml';
import Utils from './Utils';
import { Editor } from 'tinymce/core/api/Editor';
import { DataTransfer, ClipboardEvent, Range } from '@ephox/dom-globals';
const noop = function () {
};
interface SelectionContentData {
html: string;
text: string;
}
const hasWorkingClipboardApi = (clipboardData: DataTransfer) => {
// iOS supports the clipboardData API but it doesn't do anything for cut operations
// Edge 15 has a broken HTML Clipboard API see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/11780845/
return Env.iOS === false && clipboardData !== undefined && typeof clipboardData.setData === 'function' && Utils.isMsEdge() !== true;
};
const setHtml5Clipboard = (clipboardData: DataTransfer, html: string, text: string) => {
if (hasWorkingClipboardApi(clipboardData)) {
try {
clipboardData.clearData();
clipboardData.setData('text/html', html);
clipboardData.setData('text/plain', text);
clipboardData.setData(InternalHtml.internalHtmlMime(), html);
return true;
} catch (e) {
return false;
}
} else {
return false;
}
};
type DoneFn = () => void;
type FallbackFn = (html: string, done: DoneFn) => void;
const setClipboardData = (evt: ClipboardEvent, data: SelectionContentData, fallback: FallbackFn, done: DoneFn) => {
if (setHtml5Clipboard(evt.clipboardData, data.html, data.text)) {
evt.preventDefault();
done();
} else {
fallback(data.html, done);
}
};
const fallback = (editor: Editor): FallbackFn => (html, done) => {
const markedHtml = InternalHtml.mark(html);
const outer = editor.dom.create('div', {
'contenteditable': 'false',
'data-mce-bogus': 'all'
});
const inner = editor.dom.create('div', { contenteditable: 'true' }, markedHtml);
editor.dom.setStyles(outer, {
position: 'fixed',
top: '0',
left: '-3000px',
width: '1000px',
overflow: 'hidden'
});
outer.appendChild(inner);
editor.dom.add(editor.getBody(), outer);
const range = editor.selection.getRng();
inner.focus();
const offscreenRange: Range = editor.dom.createRng();
offscreenRange.selectNodeContents(inner);
editor.selection.setRng(offscreenRange);
setTimeout(() => {
editor.selection.setRng(range);
outer.parentNode.removeChild(outer);
done();
}, 0);
};
const getData = (editor: Editor): SelectionContentData => (
{
html: editor.selection.getContent({ contextual: true }),
text: editor.selection.getContent({ format: 'text' })
}
);
const isTableSelection = (editor: Editor): boolean => {
return !!editor.dom.getParent(editor.selection.getStart(), 'td[data-mce-selected],th[data-mce-selected]', editor.getBody());
};
const hasSelectedContent = (editor: Editor): boolean => {
return !editor.selection.isCollapsed() || isTableSelection(editor);
};
const cut = (editor: Editor) => (evt: ClipboardEvent) => {
if (hasSelectedContent(editor)) {
setClipboardData(evt, getData(editor), fallback(editor), () => {
// Chrome fails to execCommand from another execCommand with this message:
// "We don't execute document.execCommand() this time, because it is called recursively.""
setTimeout(() => { // detach
editor.execCommand('Delete');
}, 0);
});
}
};
const copy = (editor: Editor) => (evt: ClipboardEvent) => {
if (hasSelectedContent(editor)) {
setClipboardData(evt, getData(editor), fallback(editor), noop);
}
};
const register = (editor: Editor) => {
editor.on('cut', cut(editor));
editor.on('copy', copy(editor));
};
export default {
register
};

View File

@@ -0,0 +1,113 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import RangeUtils from 'tinymce/core/api/dom/RangeUtils';
import Delay from 'tinymce/core/api/util/Delay';
import Settings from '../api/Settings';
import InternalHtml from './InternalHtml';
import Utils from './Utils';
import { Editor } from 'tinymce/core/api/Editor';
import { Clipboard } from '../api/Clipboard';
import { MouseEvent, DataTransfer, Range } from '@ephox/dom-globals';
const getCaretRangeFromEvent = function (editor: Editor, e: MouseEvent) {
return RangeUtils.getCaretRangeFromPoint(e.clientX, e.clientY, editor.getDoc());
};
const isPlainTextFileUrl = function (content: DataTransfer) {
const plainTextContent = content['text/plain'];
return plainTextContent ? plainTextContent.indexOf('file://') === 0 : false;
};
const setFocusedRange = function (editor: Editor, rng: Range) {
editor.focus();
editor.selection.setRng(rng);
};
const setup = function (editor: Editor, clipboard: Clipboard, draggingInternallyState) {
// Block all drag/drop events
if (Settings.shouldBlockDrop(editor)) {
editor.on('dragend dragover draggesture dragdrop drop drag', function (e) {
e.preventDefault();
e.stopPropagation();
});
}
// Prevent users from dropping data images on Gecko
if (!Settings.shouldPasteDataImages(editor)) {
editor.on('drop', function (e) {
const dataTransfer = e.dataTransfer;
if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
e.preventDefault();
}
});
}
editor.on('drop', function (e) {
let dropContent, rng;
rng = getCaretRangeFromEvent(editor, e);
if (e.isDefaultPrevented() || draggingInternallyState.get()) {
return;
}
dropContent = clipboard.getDataTransferItems(e.dataTransfer);
const internal = clipboard.hasContentType(dropContent, InternalHtml.internalHtmlMime());
if ((!clipboard.hasHtmlOrText(dropContent) || isPlainTextFileUrl(dropContent)) && clipboard.pasteImageData(e, rng)) {
return;
}
if (rng && Settings.shouldFilterDrop(editor)) {
let content = dropContent['mce-internal'] || dropContent['text/html'] || dropContent['text/plain'];
if (content) {
e.preventDefault();
// FF 45 doesn't paint a caret when dragging in text in due to focus call by execCommand
Delay.setEditorTimeout(editor, function () {
editor.undoManager.transact(function () {
if (dropContent['mce-internal']) {
editor.execCommand('Delete');
}
setFocusedRange(editor, rng);
content = Utils.trimHtml(content);
if (!dropContent['text/html']) {
clipboard.pasteText(content);
} else {
clipboard.pasteHtml(content, internal);
}
});
});
}
}
});
editor.on('dragstart', function (e) {
draggingInternallyState.set(true);
});
editor.on('dragover dragend', function (e) {
if (Settings.shouldPasteDataImages(editor) && draggingInternallyState.get() === false) {
e.preventDefault();
setFocusedRange(editor, getCaretRangeFromEvent(editor, e));
}
if (e.type === 'dragend') {
draggingInternallyState.set(false);
}
});
};
export default {
setup
};

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
const validContext = /^(p|h[1-6]|li)$/;
const findStartTokenIndex = function (regexp: RegExp, html: string) {
const matches = regexp.exec(html);
return matches ? matches.index + matches[0].length : -1;
};
const findEndTokenIndex = function (regexp: RegExp, html: string) {
const matches = regexp.exec(html);
return matches ? matches.index : -1;
};
const unwrap = function (startRe: RegExp, endRe: RegExp, html: string) {
const startIndex = findStartTokenIndex(startRe, html);
const endIndex = findEndTokenIndex(endRe, html);
return startIndex !== -1 && endIndex !== -1 ? html.substring(startIndex, endIndex) : html;
};
const parseContext = function (html: string) {
const matches = /<\/([^>]+)>/g.exec(html);
return matches ? matches[1].toLowerCase() : 'body';
};
const getFragmentInfo = function (html: string) {
const startIndex = findStartTokenIndex(/<!--\s*StartFragment\s*-->/g, html);
const endIndex = findEndTokenIndex(/<!--\s*EndFragment\s*-->/g, html);
if (startIndex !== -1 && endIndex !== -1) {
return {
html: html.substring(startIndex, endIndex),
context: parseContext(html.substr(endIndex))
};
} else {
return { html, context: 'body' };
}
};
const unwrapHtml = function (html: string) {
return unwrap(/<body[^>]*>/gi, /<\/body>/gi,
unwrap(/<!--\s*StartFragment\s*-->/g, /<!--\s*EndFragment\s*-->/g, html)
);
};
const getFragmentHtml = function (html: string) {
const fragmentInfo = getFragmentInfo(html);
return validContext.test(fragmentInfo.context) ? unwrapHtml(fragmentInfo.html) : unwrapHtml(html);
};
export default {
getFragmentInfo,
getFragmentHtml
};

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
const internalMimeType = 'x-tinymce/html';
const internalMark = '<!-- ' + internalMimeType + ' -->';
const mark = function (html: string) {
return internalMark + html;
};
const unmark = function (html: string) {
return html.replace(internalMark, '');
};
const isMarked = function (html: string) {
return html.indexOf(internalMark) !== -1;
};
export default {
mark,
unmark,
isMarked,
internalHtmlMime () {
return internalMimeType;
}
};

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import Tools from 'tinymce/core/api/util/Tools';
import Entities from 'tinymce/core/api/html/Entities';
export interface RootAttrs {[key: string]: string; }
/**
* Newlines class contains utilities to convert newlines (\n or \r\n) tp BRs or to a combination of the specified block element and BRs
*
* @class tinymce.Newlines
* @private
*/
const isPlainText = function (text: string) {
// so basically any tag that is not one of the "p, div, span, br", or is one of them, but is followed
// by some additional characters qualifies the text as not a plain text (having some HTML tags)
// <span style="white-space:pre"> and <br /> are added as separate exceptions to the rule
return !/<(?:\/?(?!(?:div|p|br|span)>)\w+|(?:(?!(?:span style="white-space:\s?pre;?">)|br\s?\/>))\w+\s[^>]+)>/i.test(text);
};
const toBRs = function (text: string) {
return text.replace(/\r?\n/g, '<br>');
};
const openContainer = function (rootTag: string, rootAttrs: RootAttrs) {
let key;
const attrs = [];
let tag = '<' + rootTag;
if (typeof rootAttrs === 'object') {
for (key in rootAttrs) {
if (rootAttrs.hasOwnProperty(key)) {
attrs.push(key + '="' + Entities.encodeAllRaw(rootAttrs[key]) + '"');
}
}
if (attrs.length) {
tag += ' ' + attrs.join(' ');
}
}
return tag + '>';
};
const toBlockElements = function (text: string, rootTag: string, rootAttrs: RootAttrs) {
const blocks = text.split(/\n\n/);
const tagOpen = openContainer(rootTag, rootAttrs);
const tagClose = '</' + rootTag + '>';
const paragraphs = Tools.map(blocks, function (p) {
return p.split(/\n/).join('<br />');
});
const stitch = function (p) {
return tagOpen + p + tagClose;
};
return paragraphs.length === 1 ? paragraphs[0] : Tools.map(paragraphs, stitch).join('');
};
const convert = function (text: string, rootTag: string, rootAttrs: RootAttrs) {
return rootTag ? toBlockElements(text, rootTag, rootAttrs) : toBRs(text);
};
export default {
isPlainText,
convert,
toBRs,
toBlockElements
};

View File

@@ -0,0 +1,188 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import Tools from 'tinymce/core/api/util/Tools';
import Env from 'tinymce/core/api/Env';
import { Editor } from 'tinymce/core/api/Editor';
import { Cell } from '@ephox/katamari';
import { Element, document, HTMLElement, Range } from '@ephox/dom-globals';
// We can't attach the pastebin to a H1 inline element on IE since it won't allow H1 or other
// non valid parents to be pasted into the pastebin so we need to attach it to the body
const getPasteBinParent = (editor: Editor): Element => {
return Env.ie && editor.inline ? document.body : editor.getBody();
};
const isExternalPasteBin = (editor: Editor) => getPasteBinParent(editor) !== editor.getBody();
const delegatePasteEvents = (editor: Editor, pasteBinElm: Element, pasteBinDefaultContent: string) => {
if (isExternalPasteBin(editor)) {
editor.dom.bind(pasteBinElm, 'paste keyup', function (e) {
if (!isDefault(editor, pasteBinDefaultContent)) {
editor.fire('paste');
}
});
}
};
/**
* Creates a paste bin element as close as possible to the current caret location and places the focus inside that element
* so that when the real paste event occurs the contents gets inserted into this element
* instead of the current editor selection element.
*/
const create = (editor: Editor, lastRngCell, pasteBinDefaultContent: string) => {
const dom = editor.dom, body = editor.getBody();
let pasteBinElm;
lastRngCell.set(editor.selection.getRng());
// Create a pastebin
pasteBinElm = editor.dom.add(getPasteBinParent(editor), 'div', {
'id': 'mcepastebin',
'class': 'mce-pastebin',
'contentEditable': true,
'data-mce-bogus': 'all',
'style': 'position: fixed; top: 50%; width: 10px; height: 10px; overflow: hidden; opacity: 0'
}, pasteBinDefaultContent);
// Move paste bin out of sight since the controlSelection rect gets displayed otherwise on IE and Gecko
if (Env.ie || Env.gecko) {
dom.setStyle(pasteBinElm, 'left', dom.getStyle(body, 'direction', true) === 'rtl' ? 0xFFFF : -0xFFFF);
}
// Prevent focus events from bubbeling fixed FocusManager issues
dom.bind(pasteBinElm, 'beforedeactivate focusin focusout', function (e) {
e.stopPropagation();
});
delegatePasteEvents(editor, pasteBinElm, pasteBinDefaultContent);
pasteBinElm.focus();
editor.selection.select(pasteBinElm, true);
};
/**
* Removes the paste bin if it exists.
*/
const remove = (editor, lastRngCell) => {
if (getEl(editor)) {
let pasteBinClone;
const lastRng = lastRngCell.get();
// WebKit/Blink might clone the div so
// lets make sure we remove all clones
// TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
while ((pasteBinClone = editor.dom.get('mcepastebin'))) {
editor.dom.remove(pasteBinClone);
editor.dom.unbind(pasteBinClone);
}
if (lastRng) {
editor.selection.setRng(lastRng);
}
}
lastRngCell.set(null);
};
const getEl = (editor: Editor) => {
return editor.dom.get('mcepastebin');
};
/**
* Returns the contents of the paste bin as a HTML string.
*
* @return {String} Get the contents of the paste bin.
*/
const getHtml = (editor: Editor) => {
let pasteBinElm, pasteBinClones, i, dirtyWrappers, cleanWrapper;
// Since WebKit/Chrome might clone the paste bin when pasting
// for example: <img style="float: right"> we need to check if any of them contains some useful html.
// TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
const copyAndRemove = function (toElm: HTMLElement, fromElm: HTMLElement) {
toElm.appendChild(fromElm);
editor.dom.remove(fromElm, true); // remove, but keep children
};
// find only top level elements (there might be more nested inside them as well, see TINY-1162)
pasteBinClones = Tools.grep(getPasteBinParent(editor).childNodes, function (elm) {
return elm.id === 'mcepastebin';
});
pasteBinElm = pasteBinClones.shift();
// if clones were found, move their content into the first bin
Tools.each(pasteBinClones, function (pasteBinClone) {
copyAndRemove(pasteBinElm, pasteBinClone);
});
// TINY-1162: when copying plain text (from notepad for example) WebKit clones
// paste bin (with styles and attributes) and uses it as a default wrapper for
// the chunks of the content, here we cycle over the whole paste bin and replace
// those wrappers with a basic div
dirtyWrappers = editor.dom.select('div[id=mcepastebin]', pasteBinElm);
for (i = dirtyWrappers.length - 1; i >= 0; i--) {
cleanWrapper = editor.dom.create('div');
pasteBinElm.insertBefore(cleanWrapper, dirtyWrappers[i]);
copyAndRemove(cleanWrapper, dirtyWrappers[i]);
}
return pasteBinElm ? pasteBinElm.innerHTML : '';
};
const getLastRng = (lastRng) => {
return lastRng.get();
};
const isDefaultContent = (pasteBinDefaultContent: string, content: string) => {
return content === pasteBinDefaultContent;
};
const isPasteBin = (elm) => {
return elm && elm.id === 'mcepastebin';
};
const isDefault = (editor, pasteBinDefaultContent) => {
const pasteBinElm = getEl(editor);
return isPasteBin(pasteBinElm) && isDefaultContent(pasteBinDefaultContent, pasteBinElm.innerHTML);
};
interface PasteBin {
create: () => void;
remove: () => void;
getEl: () => HTMLElement;
getHtml: () => string;
getLastRng: () => Range;
isDefault: () => boolean;
isDefaultContent: (content: any) => boolean;
}
/**
* @class tinymce.pasteplugin.PasteBin
* @private
*/
const PasteBin = (editor): PasteBin => {
const lastRng = Cell(null);
const pasteBinDefaultContent = '%MCEPASTEBIN%';
return {
create: () => create(editor, lastRng, pasteBinDefaultContent),
remove: () => remove(editor, lastRng),
getEl: () => getEl(editor),
getHtml: () => getHtml(editor),
getLastRng: () => getLastRng(lastRng),
isDefault: () => isDefault(editor, pasteBinDefaultContent),
isDefaultContent: (content) => isDefaultContent(pasteBinDefaultContent, content)
};
};
export {
PasteBin,
getPasteBinParent
};

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import Settings from '../api/Settings';
import { Editor } from 'tinymce/core/api/Editor';
const setup = function (editor: Editor) {
const plugin = editor.plugins.paste;
const preProcess = Settings.getPreProcess(editor);
if (preProcess) {
editor.on('PastePreProcess', function (e) {
preProcess.call(plugin, plugin, e);
});
}
const postProcess = Settings.getPostProcess(editor);
if (postProcess) {
editor.on('PastePostProcess', function (e) {
postProcess.call(plugin, plugin, e);
});
}
};
export default {
setup
};

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import Events from '../api/Events';
import WordFilter from './WordFilter';
import { Editor } from 'tinymce/core/api/Editor';
const processResult = function (content, cancelled) {
return { content, cancelled };
};
const postProcessFilter = function (editor: Editor, html: string, internal: boolean, isWordHtml: boolean) {
const tempBody = editor.dom.create('div', { style: 'display:none' }, html);
const postProcessArgs = Events.firePastePostProcess(editor, tempBody, internal, isWordHtml);
return processResult(postProcessArgs.node.innerHTML, postProcessArgs.isDefaultPrevented());
};
const filterContent = function (editor: Editor, content: string, internal: boolean, isWordHtml: boolean) {
const preProcessArgs = Events.firePastePreProcess(editor, content, internal, isWordHtml);
if (editor.hasEventListeners('PastePostProcess') && !preProcessArgs.isDefaultPrevented()) {
return postProcessFilter(editor, preProcessArgs.content, internal, isWordHtml);
} else {
return processResult(preProcessArgs.content, preProcessArgs.isDefaultPrevented());
}
};
const process = function (editor: Editor, html: string, internal: boolean) {
const isWordHtml = WordFilter.isWordContent(html);
const content = isWordHtml ? WordFilter.preProcess(editor, html) : html;
return filterContent(editor, content, internal, isWordHtml);
};
export default {
process
};

View File

@@ -0,0 +1,171 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import Env from 'tinymce/core/api/Env';
import Tools from 'tinymce/core/api/util/Tools';
import Settings from '../api/Settings';
import Utils from './Utils';
import WordFilter from './WordFilter';
import { Editor } from 'tinymce/core/api/Editor';
import { Element } from '@ephox/dom-globals';
/**
* 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
*/
function addPreProcessFilter(editor: Editor, filterFunc) {
editor.on('PastePreProcess', function (e) {
e.content = filterFunc(editor, e.content, e.internal, e.wordContent);
});
}
function addPostProcessFilter(editor: Editor, filterFunc) {
editor.on('PastePostProcess', function (e) {
filterFunc(editor, e.node);
});
}
/**
* 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(editor: Editor, html: string) {
// Only filter word specific content
if (!WordFilter.isWordContent(html)) {
return html;
}
// Produce block regexp based on the block elements in schema
const blockElements = [];
Tools.each(editor.schema.getBlockElements(), function (block: Element, blockName: string) {
blockElements.push(blockName);
});
const 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
*/
function removeWebKitStyles(editor: Editor, content: string, internal: boolean, isWordHtml: boolean) {
// WordFilter has already processed styles at this point and internal doesn't need any processing
if (isWordHtml || internal) {
return content;
}
// Filter away styles that isn't matching the target node
const webKitStylesSetting = Settings.getWebkitStyles(editor);
let webKitStyles: string[] | string;
if (Settings.shouldRemoveWebKitStyles(editor) === false || webKitStylesSetting === 'all') {
return content;
}
if (webKitStylesSetting) {
webKitStyles = webKitStylesSetting.split(/[, ]/);
}
// Keep specific styles that doesn't match the current node computed style
if (webKitStyles) {
const dom = editor.dom, node = editor.selection.getNode();
content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, function (all, before, value, after) {
const inputStyles = dom.parseStyle(dom.decode(value));
let outputStyles = {};
if (webKitStyles === 'none') {
return before + after;
}
for (let i = 0; i < webKitStyles.length; i++) {
let inputValue = inputStyles[webKitStyles[i]], currentValue = dom.getStyle(node, webKitStyles[i], true);
if (/color/.test(webKitStyles[i])) {
inputValue = dom.toHex(inputValue as string);
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;
}
function removeUnderlineAndFontInAnchor(editor: Editor, root: Element) {
editor.$('a', root).find('font,u').each(function (i, node) {
editor.dom.remove(node, true);
});
}
const setup = function (editor: Editor) {
if (Env.webkit) {
addPreProcessFilter(editor, removeWebKitStyles);
}
if (Env.ie) {
addPreProcessFilter(editor, removeExplorerBrElementsAfterBlocks);
addPostProcessFilter(editor, removeUnderlineAndFontInAnchor);
}
};
export default {
setup
};

View File

@@ -0,0 +1,94 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import Tools from 'tinymce/core/api/util/Tools';
import Settings from '../api/Settings';
import { Editor } from 'tinymce/core/api/Editor';
const removeMeta = (editor: Editor, html: string) => {
const body = editor.dom.create('body', {}, html);
Tools.each(body.querySelectorAll('meta'), (elm) => elm.parentNode.removeChild(elm));
return body.innerHTML;
};
const pasteHtml = function (editor: Editor, html: string) {
editor.insertContent(removeMeta(editor, html), {
merge: Settings.shouldMergeFormats(editor),
paste: true
});
return true;
};
/**
* Tries to be smart depending on what the user pastes if it looks like an url
* it will make a link out of the current selection. If it's an image url that looks
* like an image it will check if it's an image and insert it as an image.
*
* @class tinymce.pasteplugin.SmartPaste
* @private
*/
const isAbsoluteUrl = function (url: string) {
return /^https?:\/\/[\w\?\-\/+=.&%@~#]+$/i.test(url);
};
const isImageUrl = function (url: string) {
return isAbsoluteUrl(url) && /.(gif|jpe?g|png)$/.test(url);
};
const createImage = function (editor: Editor, url: string, pasteHtmlFn: typeof pasteHtml) {
editor.undoManager.extra(function () {
pasteHtmlFn(editor, url);
}, function () {
editor.insertContent('<img src="' + url + '">');
});
return true;
};
const createLink = function (editor: Editor, url: string, pasteHtmlFn: typeof pasteHtml) {
editor.undoManager.extra(function () {
pasteHtmlFn(editor, url);
}, function () {
editor.execCommand('mceInsertLink', false, url);
});
return true;
};
const linkSelection = function (editor: Editor, html: string, pasteHtmlFn: typeof pasteHtml) {
return editor.selection.isCollapsed() === false && isAbsoluteUrl(html) ? createLink(editor, html, pasteHtmlFn) : false;
};
const insertImage = function (editor: Editor, html: string, pasteHtmlFn: typeof pasteHtml) {
return isImageUrl(html) ? createImage(editor, html, pasteHtmlFn) : false;
};
const smartInsertContent = function (editor: Editor, html: string) {
Tools.each([
linkSelection,
insertImage,
pasteHtml
], function (action) {
return action(editor, html, pasteHtml) !== true;
});
};
const insertContent = function (editor: Editor, html: string) {
if (Settings.isSmartPasteEnabled(editor) === false) {
pasteHtml(editor, html);
} else {
smartInsertContent(editor, html);
}
};
export default {
isImageUrl,
isAbsoluteUrl,
insertContent
};

View File

@@ -0,0 +1,149 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import DomParser from 'tinymce/core/api/html/DomParser';
import Schema from 'tinymce/core/api/html/Schema';
import Tools from 'tinymce/core/api/util/Tools';
import { navigator } from '@ephox/dom-globals';
/**
* This class contails various utility functions for the paste plugin.
*
* @class tinymce.pasteplugin.Utils
*/
function filter(content, items) {
Tools.each(items, function (v) {
if (v.constructor === RegExp) {
content = content.replace(v, '');
} else {
content = content.replace(v[0], v[1]);
}
});
return content;
}
/**
* Gets the innerText of the specified element. It will handle edge cases
* and works better than textContent on Gecko.
*
* @param {String} html HTML string to get text from.
* @return {String} String of text with line feeds.
*/
function innerText(html: string) {
const schema = Schema();
const domParser = DomParser({}, schema);
let text = '';
const shortEndedElements = schema.getShortEndedElements();
const ignoreElements = Tools.makeMap('script noscript style textarea video audio iframe object', ' ');
const blockElements = schema.getBlockElements();
function walk(node) {
const name = node.name, currentNode = node;
if (name === 'br') {
text += '\n';
return;
}
// Ignore wbr, to replicate innerText on Chrome/Firefox
if (name === 'wbr') {
return;
}
// img/input/hr but ignore wbr as it's just a potential word break
if (shortEndedElements[name]) {
text += ' ';
}
// Ignore script, video contents
if (ignoreElements[name]) {
text += ' ';
return;
}
if (node.type === 3) {
text += node.value;
}
// Walk all children
if (!node.shortEnded) {
if ((node = node.firstChild)) {
do {
walk(node);
} while ((node = node.next));
}
}
// Add \n or \n\n for blocks or P
if (blockElements[name] && currentNode.next) {
text += '\n';
if (name === 'p') {
text += '\n';
}
}
}
html = filter(html, [
/<!\[[^\]]+\]>/g // Conditional comments
]);
walk(domParser.parse(html));
return text;
}
/**
* Trims the specified HTML by removing all WebKit fragments, all elements wrapping the body trailing BR elements etc.
*
* @param {String} html Html string to trim contents on.
* @return {String} Html contents that got trimmed.
*/
function trimHtml(html: string) {
function trimSpaces(all, s1, s2) {
// WebKit &nbsp; meant to preserve multiple spaces but instead inserted around all inline tags,
// including the spans with inline styles created on paste
if (!s1 && !s2) {
return ' ';
}
return '\u00a0';
}
html = filter(html, [
/^[\s\S]*<body[^>]*>\s*|\s*<\/body[^>]*>[\s\S]*$/ig, // Remove anything but the contents within the BODY element
/<!--StartFragment-->|<!--EndFragment-->/g, // Inner fragments (tables from excel on mac)
[/( ?)<span class="Apple-converted-space">\u00a0<\/span>( ?)/g, trimSpaces],
/<br class="Apple-interchange-newline">/g,
/<br>$/i // Trailing BR elements
]);
return html;
}
// TODO: Should be in some global class
function createIdGenerator(prefix: string) {
let count = 0;
return function () {
return prefix + (count++);
};
}
const isMsEdge = function () {
return navigator.userAgent.indexOf(' Edge/') !== -1;
};
export default {
filter,
innerText,
trimHtml,
createIdGenerator,
isMsEdge
};

View File

@@ -0,0 +1,489 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import DomParser from 'tinymce/core/api/html/DomParser';
import Node from 'tinymce/core/api/html/Node';
import Schema from 'tinymce/core/api/html/Schema';
import Serializer from 'tinymce/core/api/html/Serializer';
import Tools from 'tinymce/core/api/util/Tools';
import Settings from '../api/Settings';
import Utils from './Utils';
import { Editor } from 'tinymce/core/api/Editor';
/**
* This class parses word HTML into proper TinyMCE markup.
*
* @class tinymce.pasteplugin.WordFilter
* @private
*/
/**
* Checks if the specified content is from any of the following sources: MS Word/Office 365/Google docs.
*/
function isWordContent(content) {
return (
(/<font face="Times New Roman"|class="?Mso|style="[^"]*\bmso-|style='[^'']*\bmso-|w:WordDocument/i).test(content) ||
(/class="OutlineElement/).test(content) ||
(/id="?docs\-internal\-guid\-/.test(content))
);
}
/**
* Checks if the specified text starts with "1. " or "a. " etc.
*/
function isNumericList(text) {
let found, patterns;
patterns = [
/^[IVXLMCD]{1,2}\.[ \u00a0]/, // Roman upper case
/^[ivxlmcd]{1,2}\.[ \u00a0]/, // Roman lower case
/^[a-z]{1,2}[\.\)][ \u00a0]/, // Alphabetical a-z
/^[A-Z]{1,2}[\.\)][ \u00a0]/, // Alphabetical A-Z
/^[0-9]+\.[ \u00a0]/, // Numeric lists
/^[\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d]+\.[ \u00a0]/, // Japanese
/^[\u58f1\u5f10\u53c2\u56db\u4f0d\u516d\u4e03\u516b\u4e5d\u62fe]+\.[ \u00a0]/ // Chinese
];
text = text.replace(/^[\u00a0 ]+/, '');
Tools.each(patterns, function (pattern) {
if (pattern.test(text)) {
found = true;
return false;
}
});
return found;
}
function isBulletList(text) {
return /^[\s\u00a0]*[\u2022\u00b7\u00a7\u25CF]\s*/.test(text);
}
/**
* Converts fake bullet and numbered lists to real semantic OL/UL.
*
* @param {tinymce.html.Node} node Root node to convert children of.
*/
function convertFakeListsToProperLists(node) {
let currentListNode, prevListNode, lastLevel = 1;
function getText(node) {
let txt = '';
if (node.type === 3) {
return node.value;
}
if ((node = node.firstChild)) {
do {
txt += getText(node);
} while ((node = node.next));
}
return txt;
}
function trimListStart(node, regExp) {
if (node.type === 3) {
if (regExp.test(node.value)) {
node.value = node.value.replace(regExp, '');
return false;
}
}
if ((node = node.firstChild)) {
do {
if (!trimListStart(node, regExp)) {
return false;
}
} while ((node = node.next));
}
return true;
}
function removeIgnoredNodes(node) {
if (node._listIgnore) {
node.remove();
return;
}
if ((node = node.firstChild)) {
do {
removeIgnoredNodes(node);
} while ((node = node.next));
}
}
function convertParagraphToLi(paragraphNode, listName, start?) {
const level = paragraphNode._listLevel || lastLevel;
// Handle list nesting
if (level !== lastLevel) {
if (level < lastLevel) {
// Move to parent list
if (currentListNode) {
currentListNode = currentListNode.parent.parent;
}
} else {
// Create new list
prevListNode = currentListNode;
currentListNode = null;
}
}
if (!currentListNode || currentListNode.name !== listName) {
prevListNode = prevListNode || currentListNode;
currentListNode = new Node(listName, 1);
if (start > 1) {
currentListNode.attr('start', '' + start);
}
paragraphNode.wrap(currentListNode);
} else {
currentListNode.append(paragraphNode);
}
paragraphNode.name = 'li';
// Append list to previous list if it exists
if (level > lastLevel && prevListNode) {
prevListNode.lastChild.append(currentListNode);
}
lastLevel = level;
// Remove start of list item "1. " or "&middot; " etc
removeIgnoredNodes(paragraphNode);
trimListStart(paragraphNode, /^\u00a0+/);
trimListStart(paragraphNode, /^\s*([\u2022\u00b7\u00a7\u25CF]|\w+\.)/);
trimListStart(paragraphNode, /^\u00a0+/);
}
// Build a list of all root level elements before we start
// altering them in the loop below.
const elements = [];
let child = node.firstChild;
while (typeof child !== 'undefined' && child !== null) {
elements.push(child);
child = child.walk();
if (child !== null) {
while (typeof child !== 'undefined' && child.parent !== node) {
child = child.walk();
}
}
}
for (let i = 0; i < elements.length; i++) {
node = elements[i];
if (node.name === 'p' && node.firstChild) {
// Find first text node in paragraph
const nodeText = getText(node);
// Detect unordered lists look for bullets
if (isBulletList(nodeText)) {
convertParagraphToLi(node, 'ul');
continue;
}
// Detect ordered lists 1., a. or ixv.
if (isNumericList(nodeText)) {
// Parse OL start number
const matches = /([0-9]+)\./.exec(nodeText);
let start = 1;
if (matches) {
start = parseInt(matches[1], 10);
}
convertParagraphToLi(node, 'ol', start);
continue;
}
// Convert paragraphs marked as lists but doesn't look like anything
if (node._listLevel) {
convertParagraphToLi(node, 'ul', 1);
continue;
}
currentListNode = null;
} else {
// If the root level element isn't a p tag which can be
// processed by convertParagraphToLi, it interrupts the
// lists, causing a new list to start instead of having
// elements from the next list inserted above this tag.
prevListNode = currentListNode;
currentListNode = null;
}
}
}
function filterStyles(editor, validStyles, node, styleValue) {
let outputStyles = {}, matches;
const styles = editor.dom.parseStyle(styleValue);
Tools.each(styles, function (value, name) {
// Convert various MS styles to W3C styles
switch (name) {
case 'mso-list':
// Parse out list indent level for lists
matches = /\w+ \w+([0-9]+)/i.exec(styleValue);
if (matches) {
node._listLevel = parseInt(matches[1], 10);
}
// Remove these nodes <span style="mso-list:Ignore">o</span>
// Since the span gets removed we mark the text node and the span
if (/Ignore/i.test(value) && node.firstChild) {
node._listIgnore = true;
node.firstChild._listIgnore = true;
}
break;
case 'horiz-align':
name = 'text-align';
break;
case 'vert-align':
name = 'vertical-align';
break;
case 'font-color':
case 'mso-foreground':
name = 'color';
break;
case 'mso-background':
case 'mso-highlight':
name = 'background';
break;
case 'font-weight':
case 'font-style':
if (value !== 'normal') {
outputStyles[name] = value;
}
return;
case 'mso-element':
// Remove track changes code
if (/^(comment|comment-list)$/i.test(value)) {
node.remove();
return;
}
break;
}
if (name.indexOf('mso-comment') === 0) {
node.remove();
return;
}
// Never allow mso- prefixed names
if (name.indexOf('mso-') === 0) {
return;
}
// Output only valid styles
if (Settings.getRetainStyleProps(editor) === 'all' || (validStyles && validStyles[name])) {
outputStyles[name] = value;
}
});
// Convert bold style to "b" element
if (/(bold)/i.test(outputStyles['font-weight'])) {
delete outputStyles['font-weight'];
node.wrap(new Node('b', 1));
}
// Convert italic style to "i" element
if (/(italic)/i.test(outputStyles['font-style'])) {
delete outputStyles['font-style'];
node.wrap(new Node('i', 1));
}
// Serialize the styles and see if there is something left to keep
outputStyles = editor.dom.serializeStyle(outputStyles, node.name);
if (outputStyles) {
return outputStyles;
}
return null;
}
const filterWordContent = function (editor: Editor, content: string) {
let retainStyleProperties, validStyles;
retainStyleProperties = Settings.getRetainStyleProps(editor);
if (retainStyleProperties) {
validStyles = Tools.makeMap(retainStyleProperties.split(/[, ]/));
}
// Remove basic Word junk
content = Utils.filter(content, [
// Remove apple new line markers
/<br class="?Apple-interchange-newline"?>/gi,
// Remove google docs internal guid markers
/<b[^>]+id="?docs-internal-[^>]*>/gi,
// Word comments like conditional comments etc
/<!--[\s\S]+?-->/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 <s> into <strike> for line-though
[/<(\/?)s>/gi, '<$1strike>'],
// Replace nsbp entites to char since it's easier to handle
[/&nbsp;/gi, '\u00a0'],
// Convert <span style="mso-spacerun:yes">___</span> to string of alternating
// breaking/non-breaking spaces of same length
[/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi,
function (str, spaces) {
return (spaces.length > 0) ?
spaces.replace(/./, ' ').slice(Math.floor(spaces.length / 2)).split('').join('\u00a0') : '';
}
]
]);
const validElements = Settings.getWordValidElements(editor);
// Setup strict schema
const schema = Schema({
valid_elements: validElements,
valid_children: '-li[p]'
});
// Add style/class attribute to all element rules since the user might have removed them from
// paste_word_valid_elements config option and we need to check them for properties
Tools.each(schema.elements, function (rule) {
/*eslint dot-notation:0*/
if (!rule.attributes.class) {
rule.attributes.class = {};
rule.attributesOrder.push('class');
}
if (!rule.attributes.style) {
rule.attributes.style = {};
rule.attributesOrder.push('style');
}
});
// Parse HTML into DOM structure
const domParser = DomParser({}, schema);
// Filter styles to remove "mso" specific styles and convert some of them
domParser.addAttributeFilter('style', function (nodes) {
let i = nodes.length, node;
while (i--) {
node = nodes[i];
node.attr('style', filterStyles(editor, validStyles, node, node.attr('style')));
// Remove pointess spans
if (node.name === 'span' && node.parent && !node.attributes.length) {
node.unwrap();
}
}
});
// Check the class attribute for comments or del items and remove those
domParser.addAttributeFilter('class', function (nodes) {
let i = nodes.length, node, className;
while (i--) {
node = nodes[i];
className = node.attr('class');
if (/^(MsoCommentReference|MsoCommentText|msoDel)$/i.test(className)) {
node.remove();
}
node.attr('class', null);
}
});
// Remove all del elements since we don't want the track changes code in the editor
domParser.addNodeFilter('del', function (nodes) {
let i = nodes.length;
while (i--) {
nodes[i].remove();
}
});
// Keep some of the links and anchors
domParser.addNodeFilter('a', function (nodes) {
let i = nodes.length, node, href, name;
while (i--) {
node = nodes[i];
href = node.attr('href');
name = node.attr('name');
if (href && href.indexOf('#_msocom_') !== -1) {
node.remove();
continue;
}
if (href && href.indexOf('file://') === 0) {
href = href.split('#')[1];
if (href) {
href = '#' + href;
}
}
if (!href && !name) {
node.unwrap();
} else {
// Remove all named anchors that aren't specific to TOC, Footnotes or Endnotes
if (name && !/^_?(?:toc|edn|ftn)/i.test(name)) {
node.unwrap();
continue;
}
node.attr({
href,
name
});
}
}
});
// Parse into DOM structure
const rootNode = domParser.parse(content);
// Process DOM
if (Settings.shouldConvertWordFakeLists(editor)) {
convertFakeListsToProperLists(rootNode);
}
// Serialize DOM back to HTML
content = Serializer({
validate: editor.settings.validate
}, schema).serialize(rootNode);
return content;
};
const preProcess = function (editor: Editor, content) {
return Settings.shouldUseDefaultFilters(editor) ? filterWordContent(editor, content) : content;
};
export default {
preProcess,
isWordContent
};

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import { Fun } from '@ephox/katamari';
import { Editor } from 'tinymce/core/api/Editor';
import { Clipboard } from '../api/Clipboard';
const stateChange = function (editor: Editor, clipboard: Clipboard, e) {
const ctrl = e.control;
ctrl.active(clipboard.pasteFormat.get() === 'text');
editor.on('PastePlainTextToggle', function (e) {
ctrl.active(e.state);
});
};
const register = function (editor: Editor, clipboard: Clipboard) {
const postRender = Fun.curry(stateChange, editor, clipboard);
editor.addButton('pastetext', {
active: false,
icon: 'pastetext',
tooltip: 'Paste as text',
cmd: 'mceTogglePlainTextPaste',
onPostRender: postRender
});
editor.addMenuItem('pastetext', {
text: 'Paste as text',
selectable: true,
active: clipboard.pasteFormat,
cmd: 'mceTogglePlainTextPaste',
onPostRender: postRender
});
};
export default {
register
};

View File

@@ -0,0 +1,19 @@
{
"env": {
"browser": false,
"amd": true
},
"globals": {
"assert": true,
"test": true,
"asynctest": true
},
"rules": {
"eqeqeq": "error",
"yoda": "error"
},
"extends": "../../../../../.eslintrc"
}

View File

@@ -0,0 +1,150 @@
import { RawAssertions } from '@ephox/agar';
import FragmentParser from 'tinymce/plugins/paste/core/FragmentParser';
import { UnitTest } from '@ephox/bedrock';
UnitTest.test('atomic.tinymce.plugins.paste.FragmentParserTest', function () {
const testGetFragmentInfo = function () {
RawAssertions.assertEq(
'Should be the input string and context body',
{
html: 'abc',
context: 'body'
},
FragmentParser.getFragmentInfo('abc')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers and context body', {
html: 'abc',
context: 'body'
},
FragmentParser.getFragmentInfo('<!-- StartFragment -->abc<!-- EndFragment -->')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers and context body', {
html: 'abc',
context: 'body'
},
FragmentParser.getFragmentInfo('<!--StartFragment-->abc<!--EndFragment-->')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers and contents before/after fragment markers', {
html: 'abc',
context: 'body'
},
FragmentParser.getFragmentInfo('X<!--StartFragment-->abc<!--EndFragment-->Y')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers and contents before/after fragment markers',
{
html: '<B>bold</B><I><B>abc</B>This</I>',
context: 'body'
},
FragmentParser.getFragmentInfo('<!DOCTYPE html><BODY><!-- StartFragment --><B>bold</B><I><B>abc</B>This</I><!-- EndFragment --></BODY></HTML>')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers and contents before/after them but with the ul context',
{
html: '<LI>abc</LI>',
context: 'ul'
},
FragmentParser.getFragmentInfo('<BODY><UL><!--StartFragment--><LI>abc</LI><!--EndFragment--></UL></BODY>')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers and contents before/after them but with the ul context',
{
html: '\n<LI>abc</LI>\n',
context: 'ul'
},
FragmentParser.getFragmentInfo('<BODY>\n<UL>\n<!--StartFragment-->\n<LI>abc</LI>\n<!--EndFragment-->\n</UL>\n</BODY>')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers and contents before/after them but with the p context',
{
html: '<B>abc</B>',
context: 'p'
},
FragmentParser.getFragmentInfo('<BODY><P><!--StartFragment--><B>abc</B><!--EndFragment--></P></BODY>')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers and contents before/after them but with the h1 context',
{
html: '<B>abc</B>',
context: 'h1'
},
FragmentParser.getFragmentInfo('<BODY><H1><!--StartFragment--><B>abc</B><!--EndFragment--></H1></BODY>')
);
};
const testGetFragmentHtml = function () {
RawAssertions.assertEq(
'Should be the input string',
'abc',
FragmentParser.getFragmentHtml('abc')
);
RawAssertions.assertEq(
'Should be the input without fragment markers',
'abc',
FragmentParser.getFragmentHtml('<!-- StartFragment -->abc<!-- EndFragment -->')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers',
'abc',
FragmentParser.getFragmentHtml('<!--StartFragment-->abc<!--EndFragment-->')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers and suffix/prefix contents',
'abc',
FragmentParser.getFragmentHtml('X<!--StartFragment-->abc<!--EndFragment-->Y')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers and suffix/prefix contents',
'<B>bold</B><I><B>abc</B>This</I>',
FragmentParser.getFragmentHtml('<!DOCTYPE html><BODY><!-- StartFragment --><B>bold</B><I><B>abc</B>This</I><!-- EndFragment --></BODY></HTML>')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers and suffix/prefix contents',
'<LI>abc</LI>',
FragmentParser.getFragmentHtml('<BODY><UL><!--StartFragment--><LI>abc</LI><!--EndFragment--></UL></BODY>')
);
RawAssertions.assertEq(
'Should be the input string without fragment markers and suffix/prefix contents',
'\n<LI>abc</LI>\n',
FragmentParser.getFragmentHtml('<BODY>\n<UL>\n<!--StartFragment-->\n<LI>abc</LI>\n<!--EndFragment-->\n</UL>\n</BODY>')
);
RawAssertions.assertEq(
'Should be the input string with body element removed',
'<UL><LI>abc</LI></UL>',
FragmentParser.getFragmentHtml('<!DOCTYPE html><HTML><BODY><UL><LI>abc</LI></UL></BODY></HTML>')
);
RawAssertions.assertEq(
'Should be the input string with body element removed',
'<UL><LI>abc</LI></UL>',
FragmentParser.getFragmentHtml('<BODY CLASS="x"><UL><LI>abc</LI></UL></BODY>')
);
RawAssertions.assertEq(
'Should be the input string with fragments and body element removed',
'<UL><LI>abc</LI></UL>',
FragmentParser.getFragmentHtml('<BODY CLASS="x"><!--StartFragment--><UL><LI>abc</LI></UL><!--EndFragment--></BODY>')
);
};
testGetFragmentInfo();
testGetFragmentHtml();
});

View File

@@ -0,0 +1,28 @@
import InternalHtml from 'tinymce/plugins/paste/core/InternalHtml';
import { UnitTest, assert } from '@ephox/bedrock';
UnitTest.test('atomic.tinymce.plugins.paste.InternalHtmlTest', function () {
const testMark = function () {
assert.eq('<!-- x-tinymce/html -->abc', InternalHtml.mark('abc'));
};
const testUnmark = function () {
assert.eq('abc', InternalHtml.unmark('<!-- x-tinymce/html -->abc'));
assert.eq('abc', InternalHtml.unmark('abc<!-- x-tinymce/html -->'));
};
const testIsMarked = function () {
assert.eq(true, InternalHtml.isMarked('<!-- x-tinymce/html -->abc'));
assert.eq(true, InternalHtml.isMarked('abc<!-- x-tinymce/html -->'));
assert.eq(false, InternalHtml.isMarked('abc'));
};
const testInternalHtmlMime = function () {
assert.eq('x-tinymce/html', InternalHtml.internalHtmlMime());
};
testMark();
testUnmark();
testIsMarked();
testInternalHtmlMime();
});

View File

@@ -0,0 +1,234 @@
import { Pipeline, Step } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { Arr, Cell } from '@ephox/katamari';
import { LegacyUnit, TinyLoader } from '@ephox/mcagar';
import { Blob, Uint8Array, Window } from '@ephox/sand';
import Delay from 'tinymce/core/api/util/Delay';
import Promise from 'tinymce/core/api/util/Promise';
import { Clipboard } from 'tinymce/plugins/paste/api/Clipboard';
import Plugin from 'tinymce/plugins/paste/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('tinymce.plugins.paste.browser.ImagePasteTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const suite = LegacyUnit.createSuite();
Plugin();
Theme();
const base64ImgSrc = [
'R0lGODdhZABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQECgAAACwAAAAAZABkAIEAAAD78jY/',
'P3SsMjIC/4SPqcvtD6OctNqLs968+w+G4kiW5ommR8C27gvHrxrK9g3TIM7f+tcL5n4doZFFLB6F',
'Sc6SCRFIp9SqVTp6BiPXbjer5XG95Ck47IuWy2e0bLz2tt3DR5w8p7vgd2tej6TW5ycCGMM3aFZo',
'OCOYqFjDuOf4KPAHiPh4qZeZuEnXOfjpFto3ilZ6dxqWGreq1br2+hTLtigZaFcJuYOb67DLC+Qb',
'UIt3i2sshyzZtEFc7JwBLT1NXI2drb3N3e39DR4uPk5ebn6Onq6+zu488A4fLz9P335Aj58fb2+g',
'71/P759AePwADBxY8KDAhAr9MWyY7yFEgPYmRgxokWK7jEYa2XGcJ/HjgJAfSXI0mRGlRZUTWUJ0',
'2RCmQpkHaSLEKPKdzYU4c+78VzCo0KFEixo9ijSp0qVMmzp9CjWq1KlUq1q9eqEAADs='
].join('');
const base64ImgSrc2 = [
'R0lGODlhAQABAPAAAP8REf///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=='
].join('');
const sTeardown = function (editor) {
return Step.sync(function () {
delete editor.settings.paste_data_images;
delete editor.settings.images_dataimg_filter;
editor.editorUpload.destroy();
});
};
const appendTeardown = function (editor, steps) {
return Arr.bind(steps, function (step) {
return [step, sTeardown(editor)];
});
};
const base64ToBlob = function (base64, type) {
const buff = Window.atob(base64);
const bytes = Uint8Array(buff.length);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = buff.charCodeAt(i);
}
return Blob([bytes], { type });
};
const noop = function () {
};
const mockEvent = function (type, files) {
let event, transferName;
event = {
type,
preventDefault: noop
};
transferName = type === 'drop' ? 'dataTransfer' : 'clipboardData';
event[transferName] = {
files
};
return event;
};
const setupContent = function (editor) {
editor.setContent('<p>a</p>');
LegacyUnit.setSelection(editor, 'p', 0);
return editor.selection.getRng();
};
const waitFor = function (predicate) {
return new Promise(function (resolve, reject) {
const check = function (time, count) {
if (predicate()) {
resolve();
} else {
if (count === 0) {
reject(new Error('Waited for predicate to be true'));
} else {
Delay.setTimeout(function () {
check(time, count - 1);
}, time);
}
}
};
check(10, 100);
});
};
const waitForSelector = function (editor, selector) {
return waitFor(() => editor.dom.select(selector).length > 0);
};
suite.asyncTest('pasteImages should set unique id in blobcache', function (editor, done, die) {
let rng, event;
const clipboard = Clipboard(editor, Cell('html'));
const hasCachedItem = (name) => !!editor.editorUpload.blobCache.get(name);
editor.settings.paste_data_images = true;
rng = setupContent(editor);
event = mockEvent('paste', [
base64ToBlob(base64ImgSrc, 'image/gif'),
base64ToBlob(base64ImgSrc2, 'image/gif')
]);
clipboard.pasteImageData(event, rng);
waitForSelector(editor, 'img').then(function () {
waitFor((editor) => hasCachedItem('mceclip0') && hasCachedItem('mceclip1')).then(() => {
const cachedBlob1 = editor.editorUpload.blobCache.get('mceclip0');
const cachedBlob2 = editor.editorUpload.blobCache.get('mceclip1');
LegacyUnit.equal(base64ImgSrc, cachedBlob1.base64());
LegacyUnit.equal(base64ImgSrc2, cachedBlob2.base64());
done();
}).catch(die);
}).catch(die);
});
suite.asyncTest('dropImages', function (editor, done, die) {
let rng, event;
const clipboard = Clipboard(editor, Cell('html'));
editor.settings.paste_data_images = true;
rng = setupContent(editor);
event = mockEvent('drop', [
base64ToBlob(base64ImgSrc, 'image/gif')
]);
clipboard.pasteImageData(event, rng);
waitForSelector(editor, 'img').then(function () {
LegacyUnit.equal(editor.getContent(), '<p><img src=\"data:image/gif;base64,' + base64ImgSrc + '" />a</p>');
LegacyUnit.strictEqual(editor.dom.select('img')[0].src.indexOf('blob:'), 0);
done();
}).catch(die);
});
suite.asyncTest('pasteImages', function (editor, done, die) {
let rng, event;
const clipboard = Clipboard(editor, Cell('html'));
editor.settings.paste_data_images = true;
rng = setupContent(editor);
event = mockEvent('paste', [
base64ToBlob(base64ImgSrc, 'image/gif')
]);
clipboard.pasteImageData(event, rng);
waitForSelector(editor, 'img').then(function () {
LegacyUnit.equal(editor.getContent(), '<p><img src=\"data:image/gif;base64,' + base64ImgSrc + '" />a</p>');
LegacyUnit.strictEqual(editor.dom.select('img')[0].src.indexOf('blob:'), 0);
done();
}).catch(die);
});
suite.asyncTest('dropImages - images_dataimg_filter', function (editor, done, die) {
let rng, event;
const clipboard = Clipboard(editor, Cell('html'));
editor.settings.paste_data_images = true;
editor.settings.images_dataimg_filter = function (img) {
LegacyUnit.strictEqual(img.src, 'data:image/gif;base64,' + base64ImgSrc);
return false;
};
rng = setupContent(editor);
event = mockEvent('drop', [
base64ToBlob(base64ImgSrc, 'image/gif')
]);
clipboard.pasteImageData(event, rng);
waitForSelector(editor, 'img').then(function () {
LegacyUnit.equal(editor.getContent(), '<p><img src=\"data:image/gif;base64,' + base64ImgSrc + '" />a</p>');
LegacyUnit.strictEqual(editor.dom.select('img')[0].src.indexOf('data:'), 0);
done();
}).catch(die);
});
suite.asyncTest('pasteImages - images_dataimg_filter', function (editor, done, die) {
let rng, event;
const clipboard = Clipboard(editor, Cell('html'));
editor.settings.paste_data_images = true;
editor.settings.images_dataimg_filter = function (img) {
LegacyUnit.strictEqual(img.src, 'data:image/gif;base64,' + base64ImgSrc);
return false;
};
rng = setupContent(editor);
event = mockEvent('paste', [
base64ToBlob(base64ImgSrc, 'image/gif')
]);
clipboard.pasteImageData(event, rng);
waitForSelector(editor, 'img').then(function () {
LegacyUnit.equal(editor.getContent(), '<p><img src=\"data:image/gif;base64,' + base64ImgSrc + '" />a</p>');
LegacyUnit.strictEqual(editor.dom.select('img')[0].src.indexOf('data:'), 0);
done();
}).catch(die);
});
TinyLoader.setup(function (editor, onSuccess, onFailure) {
Pipeline.async({}, appendTeardown(editor, suite.toSteps(editor)), onSuccess, onFailure);
}, {
add_unload_trigger: false,
disable_nodechange: true,
entities: 'raw',
indent: false,
automatic_uploads: false,
plugins: 'paste',
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,240 @@
import { GeneralSteps, Logger, Pipeline, RawAssertions, Step, Waiter } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { TinyApis, TinyLoader } from '@ephox/mcagar';
import InternalHtml from 'tinymce/plugins/paste/core/InternalHtml';
import Utils from 'tinymce/plugins/paste/core/Utils';
import PastePlugin from 'tinymce/plugins/paste/Plugin';
import TablePlugin from 'tinymce/plugins/table/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
import MockDataTransfer from '../module/test/MockDataTransfer';
UnitTest.asynctest('browser.tinymce.plugins.paste.InternalClipboardTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
let dataTransfer, lastPreProcessEvent, lastPostProcessEvent;
PastePlugin();
TablePlugin();
Theme();
const sResetProcessEvents = Step.sync(function () {
lastPreProcessEvent = null;
lastPostProcessEvent = null;
});
const sCutCopyDataTransferEvent = function (editor, type) {
return Step.sync(function () {
dataTransfer = MockDataTransfer.create({});
editor.fire(type, { clipboardData: dataTransfer });
});
};
const sPasteDataTransferEvent = function (editor, data) {
return Step.sync(function () {
dataTransfer = MockDataTransfer.create(data);
editor.fire('paste', { clipboardData: dataTransfer });
});
};
const sAssertClipboardData = function (expectedHtml, expectedText) {
return Step.sync(function () {
RawAssertions.assertEq('text/html data should match', expectedHtml, dataTransfer.getData('text/html'));
RawAssertions.assertEq('text/plain data should match', expectedText, dataTransfer.getData('text/plain'));
});
};
const sCopy = function (editor, tinyApis, html, spath, soffset, fpath, foffset) {
return GeneralSteps.sequence([
tinyApis.sSetContent(html),
tinyApis.sSetSelection(spath, soffset, fpath, foffset),
sCutCopyDataTransferEvent(editor, 'copy')
]);
};
const sCut = function (editor, tinyApis, html, spath, soffset, fpath, foffset) {
return GeneralSteps.sequence([
tinyApis.sSetContent(html),
tinyApis.sSetSelection(spath, soffset, fpath, foffset),
sCutCopyDataTransferEvent(editor, 'cut')
]);
};
const sPaste = function (editor, tinyApis, startHtml, pasteData, spath, soffset, fpath, foffset) {
return GeneralSteps.sequence([
tinyApis.sSetContent(startHtml),
tinyApis.sSetSelection(spath, soffset, fpath, foffset),
sResetProcessEvents,
sPasteDataTransferEvent(editor, pasteData)
]);
};
const sTestCopy = function (editor, tinyApis) {
return Logger.t('Copy tests', GeneralSteps.sequence([
Logger.t('Copy simple text', GeneralSteps.sequence([
sCopy(editor, tinyApis, '<p>text</p>', [0, 0], 0, [0, 0], 4),
sAssertClipboardData('text', 'text'),
tinyApis.sAssertContent('<p>text</p>'),
tinyApis.sAssertSelection([0, 0], 0, [0, 0], 4)
])),
Logger.t('Copy inline elements', GeneralSteps.sequence([
sCopy(editor, tinyApis, '<p>te<em>x</em>t</p>', [0, 0], 0, [0, 2], 1),
sAssertClipboardData('te<em>x</em>t', 'text'),
tinyApis.sAssertContent('<p>te<em>x</em>t</p>'),
tinyApis.sAssertSelection([0, 0], 0, [0, 2], 1)
])),
Logger.t('Copy partialy selected inline elements', GeneralSteps.sequence([
sCopy(editor, tinyApis, '<p>a<em>cd</em>e</p>', [0, 0], 0, [0, 1, 0], 1),
sAssertClipboardData('a<em>c</em>', 'ac'),
tinyApis.sAssertContent('<p>a<em>cd</em>e</p>'),
tinyApis.sAssertSelection([0, 0], 0, [0, 1, 0], 1)
])),
Logger.t('Copy collapsed selection', GeneralSteps.sequence([
sCopy(editor, tinyApis, '<p>abc</p>', [0, 0], 1, [0, 0], 1),
sAssertClipboardData('', ''),
tinyApis.sAssertContent('<p>abc</p>'),
tinyApis.sAssertSelection([0, 0], 1, [0, 0], 1)
])),
Logger.t('Copy collapsed selection with table selection', GeneralSteps.sequence([
sCopy(editor, tinyApis,
'<table data-mce-selected="1">' +
'<tbody>' +
'<tr>' +
'<td data-mce-first-selected="1" data-mce-selected="1">a</td>' +
'<td data-mce-last-selected="1" data-mce-selected="1">b</td>' +
'</tr>' +
'</tbody>' +
'</table>',
[0, 0, 0, 1, 0], 0, [0, 0, 0, 1, 0], 0),
sAssertClipboardData(
'<table>\n' +
'<tbody>\n' +
'<tr>\n' +
'<td>a</td>\n' +
'<td>b</td>\n' +
'</tr>\n' +
'</tbody>\n' +
'</table>', 'ab'),
tinyApis.sAssertSelection([0, 0, 0, 1, 0], 0, [0, 0, 0, 1, 0], 0)
]))
]));
};
const sTestCut = function (editor, tinyApis) {
const sWaitUntilAssertContent = function (expected) {
return Waiter.sTryUntil('Cut is async now, so need to wait for content', tinyApis.sAssertContent(expected), 100, 1000);
};
return Logger.t('Cut tests', GeneralSteps.sequence([
Logger.t('Cut simple text', GeneralSteps.sequence([
sCut(editor, tinyApis, '<p>text</p>', [0, 0], 0, [0, 0], 4),
sAssertClipboardData('text', 'text'),
sWaitUntilAssertContent(''),
tinyApis.sAssertSelection([0], 0, [0], 0)
])),
Logger.t('Cut inline elements', GeneralSteps.sequence([
sCut(editor, tinyApis, '<p>te<em>x</em>t</p>', [0, 0], 0, [0, 2], 1),
sAssertClipboardData('te<em>x</em>t', 'text'),
sWaitUntilAssertContent(''),
tinyApis.sAssertSelection([0], 0, [0], 0)
])),
Logger.t('Cut partialy selected inline elements', GeneralSteps.sequence([
sCut(editor, tinyApis, '<p>a<em>cd</em>e</p>', [0, 0], 0, [0, 1, 0], 1),
sAssertClipboardData('a<em>c</em>', 'ac'),
sWaitUntilAssertContent('<p><em>d</em>e</p>'),
tinyApis.sAssertSelection([0, 0, 0], 0, [0, 0, 0], 0)
])),
Logger.t('Cut collapsed selection', GeneralSteps.sequence([
sCut(editor, tinyApis, '<p>abc</p>', [0, 0], 1, [0, 0], 1),
sAssertClipboardData('', ''),
sWaitUntilAssertContent('<p>abc</p>'),
tinyApis.sAssertSelection([0, 0], 1, [0, 0], 1)
]))
]));
};
const sAssertLastPreProcessEvent = function (expectedData) {
return Step.sync(function () {
RawAssertions.assertEq('Internal property should be equal', expectedData.internal, lastPreProcessEvent.internal);
RawAssertions.assertEq('Content property should be equal', expectedData.content, lastPreProcessEvent.content);
});
};
const sAssertLastPostProcessEvent = function (expectedData) {
return Step.sync(function () {
RawAssertions.assertEq('Internal property should be equal', expectedData.internal, lastPostProcessEvent.internal);
RawAssertions.assertEq('Content property should be equal', expectedData.content, lastPostProcessEvent.node.innerHTML);
});
};
const sWaitForProcessEvents = Waiter.sTryUntil('Did not get any events fired', Step.sync(function () {
RawAssertions.assertEq('PastePreProcess event object', lastPreProcessEvent !== null, true);
RawAssertions.assertEq('PastePostProcess event object', lastPostProcessEvent !== null, true);
}), 100, 100);
const sTestPaste = function (editor, tinyApis) {
return Logger.t('Paste tests', GeneralSteps.sequence([
Logger.t('Paste external content', GeneralSteps.sequence([
sPaste(editor, tinyApis, '<p>abc</p>', { 'text/plain': 'X', 'text/html': '<p>X</p>' }, [0, 0], 0, [0, 0], 3),
sWaitForProcessEvents,
sAssertLastPreProcessEvent({ internal: false, content: 'X' }),
sAssertLastPostProcessEvent({ internal: false, content: 'X' })
])),
Logger.t('Paste external content treated as plain text', GeneralSteps.sequence([
sPaste(editor, tinyApis, '<p>abc</p>', { 'text/html': '<p>X</p>' }, [0, 0], 0, [0, 0], 3),
sWaitForProcessEvents,
sAssertLastPreProcessEvent({ internal: false, content: 'X' }),
sAssertLastPostProcessEvent({ internal: false, content: 'X' })
])),
Logger.t('Paste internal content with mark', GeneralSteps.sequence([
sPaste(editor, tinyApis, '<p>abc</p>', { 'text/plain': 'X', 'text/html': InternalHtml.mark('<p>X</p>') }, [0, 0], 0, [0, 0], 3),
sWaitForProcessEvents,
sAssertLastPreProcessEvent({ internal: true, content: '<p>X</p>' }),
sAssertLastPostProcessEvent({ internal: true, content: '<p>X</p>' })
])),
Logger.t('Paste internal content with mime', GeneralSteps.sequence([
sPaste(editor, tinyApis, '<p>abc</p>',
{ 'text/plain': 'X', 'text/html': '<p>X</p>', 'x-tinymce/html': '<p>X</p>' },
[0, 0], 0, [0, 0], 3
),
sWaitForProcessEvents,
sAssertLastPreProcessEvent({ internal: true, content: '<p>X</p>' }),
sAssertLastPostProcessEvent({ internal: true, content: '<p>X</p>' })
]))
]));
};
TinyLoader.setup(function (editor, onSuccess, onFailure) {
const tinyApis = TinyApis(editor);
// Disabled tests on Edge 15 due to broken clipboard API
Pipeline.async({}, Utils.isMsEdge() ? [ ] : [
sTestCopy(editor, tinyApis),
sTestCut(editor, tinyApis),
sTestPaste(editor, tinyApis)
], onSuccess, onFailure);
}, {
plugins: 'paste table',
init_instance_callback (editor) {
editor.on('PastePreProcess', function (evt) {
lastPreProcessEvent = evt;
});
editor.on('PastePostProcess', function (evt) {
lastPostProcessEvent = evt;
});
},
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,67 @@
import { Assertions } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { Arr } from '@ephox/katamari';
import Newlines from 'tinymce/plugins/paste/core/Newlines';
import PastePlugin from 'tinymce/plugins/paste/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
UnitTest.test('tinymce.plugins.paste.browser.NewlinesTest', function () {
Theme();
PastePlugin();
// testing Newlines.isPlainText()
const textCases = [
{
label: 'Basic Chrome markup (including span-wrapped tab)',
content: '<div><span style="white-space:pre"> </span>a</div><div><br></div><div>b</div>',
isText: true
},
{
label: 'Case shouldn\'t matter',
content: '<DIV>a</DIV><DIV><BR></DIV>',
isText: true
},
{
label: 'Support all BR types',
content: '<br><br />',
isText: true
},
{
label: 'Basic IE markup',
content: '<p>a</p><p><br></p><p>b</p>',
isText: true
},
{
label: 'White-space wrapper (Chrome)',
content: '<div><span style="white-space: pre;"> </span>a</div>',
isText: true
},
{
label: 'White-space wrapper (Chrome) with additional styles',
content: '<div><span style="white-space: pre; color: red;"> </span>a</div>',
isText: false
},
{
label: 'Allowed tag but with attributes qualifies string as not a plain text',
content: '<br data-mce-bogus="all" />',
isText: false
}
];
// only DIV,P,BR and SPAN[style="white-space:pre"] tags are allowed in "plain text" string
Arr.each('a,abbr,address,article,aside,audio,b,bdi,bdo,blockquote,button,cite,code,del,details,dfn,dl,em,embed,fieldset,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,i,ins,label,menu,nav,noscript,object,ol,pre,q,s,script,section,select,small,strong,style,sub,sup,svg,table,textarea,time,u,ul,var,video,wbr'.split(','),
function (tag) {
const content = '<p>a</p><' + tag + '>b</' + tag + '><p>c<br>d</p>';
textCases.push({
label: tag.toUpperCase() + ' tag should qualify content (' + content + ') as not a plain text',
content,
isText: false
});
}
);
Arr.each(textCases, function (c) {
Assertions.assertEq(c.label || 'Asserting: ' + c.content, c.isText, Newlines.isPlainText(c.content));
});
});

View File

@@ -0,0 +1,99 @@
import { Assertions, Chain, Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { Id, Merger, Obj } from '@ephox/katamari';
import EditorManager from 'tinymce/core/api/EditorManager';
import { PasteBin, getPasteBinParent } from 'tinymce/plugins/paste/core/PasteBin';
import PastePlugin from 'tinymce/plugins/paste/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
import ViewBlock from '../module/test/ViewBlock';
UnitTest.asynctest('tinymce.plugins.paste.browser.PasteBin', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
Theme();
PastePlugin();
const cases = [
{
label: 'TINY-1162: testing nested paste bins',
content: '<div id="mcepastebin" contenteditable="true" data-mce-bogus="all" data-mce-style="position: absolute; top: 0.40000057220458984px;width: 10px; height: 10px; overflow: hidden; opacity: 0" style="position: absolute; top: 0.40000057220458984px;width: 10px; height: 10px; overflow: hidden; opacity: 0"><div id="mcepastebin" data-mce-bogus="all" data-mce-style="position: absolute; top: 0.40000057220458984px;width: 10px; height: 10px; overflow: hidden; opacity: 0" style="position: absolute; top: 0.40000057220458984px;width: 10px; height: 10px; overflow: hidden; opacity: 0">a</div><div id="mcepastebin" data-mce-bogus="all" data-mce-style="position: absolute; top: 0.40000057220458984px;width: 10px; height: 10px; overflow: hidden; opacity: 0" style="position: absolute; top: 0.40000057220458984px;width: 10px; height: 10px; overflow: hidden; opacity: 0">b</div></div>',
result: '<div>a</div><div>b</div>'
},
{
label: 'TINY-1162: testing adjacent paste bins',
content: '<div id="mcepastebin" contenteditable="true" data-mce-bogus="all" data-mce-style="position: absolute; top: 0.40000057220458984px;width: 10px; height: 10px; overflow: hidden; opacity: 0" style="position: absolute; top: 0.40000057220458984px;width: 10px; height: 10px; overflow: hidden; opacity: 0"><p>a</p><p>b</p></div><div id="mcepastebin" contenteditable="true" data-mce-bogus="all" data-mce-style="position: absolute; top: 0.40000057220458984px;width: 10px; height: 10px; overflow: hidden; opacity: 0" style="position: absolute; top: 0.40000057220458984px;width: 10px; height: 10px; overflow: hidden; opacity: 0"><p>c</p></div>',
result: '<p>a</p><p>b</p><p>c</p>'
}
];
const viewBlock = ViewBlock();
const cCreateEditorFromSettings = function (settings?, html?) {
return Chain.async(function (viewBlock: any, next, die) {
const randomId = Id.generate('tiny');
html = html || '<textarea></textarea>';
viewBlock.update(html);
viewBlock.get().firstChild.id = randomId;
EditorManager.init(Merger.merge(settings || {}, {
selector: '#' + randomId,
add_unload_trigger: false,
indent: false,
plugins: 'paste',
skin_url: '/project/js/tinymce/skins/lightgray',
setup (editor) {
editor.on('SkinLoaded', function () {
setTimeout(function () {
next(editor);
}, 0);
});
}
}));
});
};
const cCreateEditorFromHtml = function (html, settings) {
return cCreateEditorFromSettings(settings, html);
};
const cRemoveEditor = function () {
return Chain.op(function (editor: any) {
editor.remove();
});
};
const cAssertCases = function (cases) {
return Chain.op(function (editor: any) {
const pasteBin = PasteBin(editor);
Obj.each(cases, function (c, i) {
getPasteBinParent(editor).appendChild(editor.dom.createFragment(c.content));
Assertions.assertEq(c.label || 'Asserting paste bin case ' + i, c.result, pasteBin.getHtml());
pasteBin.remove();
});
});
};
viewBlock.attach();
Pipeline.async({}, [
Chain.asStep(viewBlock, [
cCreateEditorFromSettings(),
cAssertCases(cases),
cRemoveEditor()
]),
// TINY-1208/TINY-1209: same cases, but for inline editor
Chain.asStep(viewBlock, [
cCreateEditorFromHtml('<div>some text</div>', { inline: true }),
cAssertCases(cases),
cRemoveEditor()
])
], function () {
viewBlock.detach();
success();
}, failure);
});

View File

@@ -0,0 +1,38 @@
import { GeneralSteps, Logger, Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { TinyApis, TinyLoader } from '@ephox/mcagar';
import Env from 'tinymce/core/api/Env';
import PastePlugin from 'tinymce/plugins/paste/Plugin';
import ModernTheme from 'tinymce/themes/modern/Theme';
import Paste from '../module/test/Paste';
UnitTest.asynctest('tinymce.plugins.paste.browser.PasteFormatToggleTest', (success, failure) => {
ModernTheme();
PastePlugin();
TinyLoader.setup(function (editor, onSuccess, onFailure) {
const tinyApis = TinyApis(editor);
const steps = Env.webkit ? [
Logger.t('paste plain text',
GeneralSteps.sequence([
tinyApis.sExecCommand('mceTogglePlainTextPaste'),
Paste.sPaste(editor, { 'text/html': '<p><strong>test</strong></p>'}),
tinyApis.sAssertContent('<p>test</p>'),
tinyApis.sSetContent(''),
tinyApis.sExecCommand('mceTogglePlainTextPaste'),
Paste.sPaste(editor, { 'text/html': '<p><strong>test</strong></p>'}),
tinyApis.sAssertContent('<p><strong>test</strong></p>')
])
)
] : [];
Pipeline.async({}, steps, onSuccess, onFailure);
}, {
plugins: 'paste',
toolbar: '',
valid_styles: 'font-family,color',
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,56 @@
import { Assertions, Chain, Logger, Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { Merger } from '@ephox/katamari';
import EditorManager from 'tinymce/core/api/EditorManager';
import Plugin from 'tinymce/plugins/paste/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
import ViewBlock from '../module/test/ViewBlock';
UnitTest.asynctest('tinymce.plugins.paste.browser.PasteSettingsTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const viewBlock = ViewBlock();
Theme();
Plugin();
const cCreateInlineEditor = function (settings) {
return Chain.async(function (viewBlock: any, next, die) {
viewBlock.update('<div id="inline-tiny"></div>');
EditorManager.init(Merger.merge({
selector: '#inline-tiny',
inline: true,
skin_url: '/project/js/tinymce/skins/lightgray',
setup (editor) {
editor.on('SkinLoaded', function () {
next(editor);
});
}
}, settings));
});
};
const cRemoveEditor = Chain.op(function (editor: any) {
editor.remove();
});
viewBlock.attach();
Pipeline.async({}, [
Logger.t('paste_as_text setting', Chain.asStep(viewBlock, [
cCreateInlineEditor({
paste_as_text: true,
plugins: 'paste'
}),
Chain.op(function (editor) {
Assertions.assertEq('Should be text format', 'text', editor.plugins.paste.clipboard.pasteFormat.get());
}),
cRemoveEditor
]))
], function () {
viewBlock.detach();
success();
}, failure);
});

View File

@@ -0,0 +1,69 @@
import { GeneralSteps, Logger, Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { TinyApis, TinyLoader } from '@ephox/mcagar';
import Env from 'tinymce/core/api/Env';
import PastePlugin from 'tinymce/plugins/paste/Plugin';
import ModernTheme from 'tinymce/themes/modern/Theme';
import Paste from '../module/test/Paste';
UnitTest.asynctest('Browser Test: .PasteStylesTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
ModernTheme();
PastePlugin();
TinyLoader.setup(function (editor, onSuccess, onFailure) {
const tinyApis = TinyApis(editor);
const steps = Env.webkit ? [
Logger.t('Paste span with encoded style attribute, paste_webkit_styles: font-family',
GeneralSteps.sequence([
tinyApis.sSetSetting('paste_webkit_styles', 'font-family'),
tinyApis.sSetContent('<p>test</p>'),
tinyApis.sSetSelection([0, 0], 0, [0, 0], 4),
Paste.sPaste(editor, { 'text/html': '<span style="font-family: &quot;a b&quot;;color:green;">b</span>' }),
tinyApis.sAssertContent('<p><span style="font-family: \'a b\';">b</span></p>')
])
),
Logger.t('Paste span with encoded style attribute, paste_webkit_styles: all',
GeneralSteps.sequence([
tinyApis.sSetSetting('paste_webkit_styles', 'all'),
tinyApis.sSetContent('<p>test</p>'),
tinyApis.sSetSelection([0, 0], 0, [0, 0], 4),
Paste.sPaste(editor, { 'text/html': '<span style="font-family: &quot;a b&quot;; color: green;">b</span>' }),
tinyApis.sAssertContent('<p><span style="font-family: \'a b\'; color: green;">b</span></p>')
])
),
Logger.t('Paste span with encoded style attribute, paste_webkit_styles: none',
GeneralSteps.sequence([
tinyApis.sSetSetting('paste_webkit_styles', 'none'),
tinyApis.sSetContent('<p>test</p>'),
tinyApis.sSetSelection([0, 0], 0, [0, 0], 4),
Paste.sPaste(editor, { 'text/html': '<span style="font-family: &quot;a b&quot;;">b</span>' }),
tinyApis.sAssertContent('<p>b</p>')
])
),
Logger.t('Paste span with encoded style attribute, paste_remove_styles_if_webkit: false',
GeneralSteps.sequence([
tinyApis.sSetSetting('paste_remove_styles_if_webkit', false),
tinyApis.sSetContent('<p>test</p>'),
tinyApis.sSetSelection([0, 0], 0, [0, 0], 4),
Paste.sPaste(editor, { 'text/html': '<span style="font-family: &quot;a b&quot;;">b</span>' }),
tinyApis.sAssertContent('<p><span style="font-family: \'a b\';">b</span></p>')
])
)
] : [];
Pipeline.async({}, steps, onSuccess, onFailure);
}, {
plugins: 'paste',
toolbar: '',
valid_styles: 'font-family,color',
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,879 @@
import { Pipeline, Step } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { Arr } from '@ephox/katamari';
import { LegacyUnit, TinyLoader } from '@ephox/mcagar';
import Env from 'tinymce/core/api/Env';
import Utils from 'tinymce/plugins/paste/core/Utils';
import Plugin from 'tinymce/plugins/paste/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
import Strings from '../module/test/Strings';
UnitTest.asynctest('tinymce.plugins.paste.browser.ImagePasteTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const suite = LegacyUnit.createSuite();
Plugin();
Theme();
/* eslint-disable max-len */
const sTeardown = function (editor) {
return Step.sync(function () {
delete editor.settings.paste_remove_styles_if_webkit;
delete editor.settings.paste_retain_style_properties;
delete editor.settings.paste_enable_default_filters;
delete editor.settings.paste_data_images;
delete editor.settings.paste_webkit_styles;
});
};
const appendTeardown = function (editor, steps) {
return Arr.bind(steps, function (step) {
return [step, sTeardown(editor)];
});
};
const trimContent = function (content) {
return content.replace(/^<p>&nbsp;<\/p>\n?/, '').replace(/\n?<p>&nbsp;<\/p>$/, '');
};
suite.test('Plain text toggle event', function (editor) {
const events = [];
editor.on('PastePlainTextToggle', function (e) {
events.push({ state: e.state });
});
editor.execCommand('mceTogglePlainTextPaste');
LegacyUnit.deepEqual(events, [
{ state: true }
], 'Should be enabled');
editor.execCommand('mceTogglePlainTextPaste');
LegacyUnit.deepEqual(events, [
{ state: true },
{ state: false }
], 'Should be disabled');
editor.execCommand('mceTogglePlainTextPaste');
LegacyUnit.deepEqual(events, [
{ state: true },
{ state: false },
{ state: true }
], 'Should be enabled again');
});
suite.test('Paste simple text content', function (editor) {
const rng = editor.dom.createRng();
editor.setContent('<p>1234</p>');
editor.focus();
rng.setStart(editor.getBody().firstChild.firstChild, 1);
rng.setEnd(editor.getBody().firstChild.firstChild, 3);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: 'TEST' });
LegacyUnit.equal(editor.getContent(), '<p>1TEST4</p>');
});
suite.test('Paste text with meta and nbsp', function (editor) {
const rng = editor.dom.createRng();
editor.setContent('<p>1&nbsp;</p>');
editor.focus();
rng.setStart(editor.getBody().firstChild.firstChild, 2);
rng.setEnd(editor.getBody().firstChild.firstChild, 2);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: '<meta charset="utf-8">TEST' });
LegacyUnit.equal(editor.getContent(), '<p>1 TEST</p>');
});
suite.test('Paste styled text content', function (editor) {
const rng = editor.dom.createRng();
editor.settings.paste_remove_styles_if_webkit = false;
editor.setContent('<p>1234</p>');
rng.setStart(editor.getBody().firstChild.firstChild, 1);
rng.setEnd(editor.getBody().firstChild.firstChild, 3);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: '<strong><em><span style="color: red;">TEST</span></em></strong>' });
LegacyUnit.equal(editor.getContent(), '<p>1<strong><em><span style="color: red;">TEST</span></em></strong>4</p>');
});
suite.test('Paste paragraph in paragraph', function (editor) {
const rng = editor.dom.createRng();
editor.setContent('<p>1234</p>');
rng.setStart(editor.getBody().firstChild.firstChild, 1);
rng.setEnd(editor.getBody().firstChild.firstChild, 3);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: '<p>TEST</p>' });
LegacyUnit.equal(editor.getContent(), '<p>1</p><p>TEST</p><p>4</p>');
});
suite.test('Paste paragraphs in complex paragraph', function (editor) {
const rng = editor.dom.createRng();
editor.setContent('<p><strong><em>1234</em></strong></p>');
rng.setStart(editor.dom.select('em,i')[0].firstChild, 1);
rng.setEnd(editor.dom.select('em,i')[0].firstChild, 3);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: '<p>TEST 1</p><p>TEST 2</p>' });
LegacyUnit.equal(editor.getContent(), '<p><strong><em>1</em></strong></p><p>TEST 1</p><p>TEST 2</p><p><strong><em>4</em></strong></p>');
});
suite.test('Paste Word fake list', function (editor) {
let rng = editor.dom.createRng();
editor.setContent('<p>1234</p>');
rng.setStart(editor.getBody().firstChild.firstChild, 0);
rng.setEnd(editor.getBody().firstChild.firstChild, 4);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: Strings.wordList2 });
LegacyUnit.equal(editor.getContent(), '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li><li>Item 5</li><li>Item 6</li></ul>');
editor.settings.paste_retain_style_properties = 'border';
rng = editor.dom.createRng();
editor.setContent('<p>1234</p>');
rng.setStart(editor.getBody().firstChild.firstChild, 0);
rng.setEnd(editor.getBody().firstChild.firstChild, 4);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: '<p class="ListStyle" style="margin-top:0cm;margin-right:0cm;margin-bottom:3.0pt;margin-left:18.0pt;mso-add-space:auto;text-align:justify;text-indent:-18.0pt;mso-list:l0 level1 lfo1;tab-stops:list 18.0pt"><span lang="DE" style="font-family:Verdana;mso-fareast-font-family:Verdana;mso-bidi-font-family:Verdana;color:black"><span style="mso-list:Ignore">\u25CF<span style="font:7.0pt &quot;Times New Roman&quot;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></span></span><span lang="DE" style="font-family:Arial;mso-fareast-font-family:Arial;mso-bidi-font-family:Arial;color:black">Item&nbsp; Spaces.<o:p></o:p></span></p>' });
LegacyUnit.equal(editor.getContent(), '<ul><li>Item&nbsp; Spaces.</li></ul>');
rng = editor.dom.createRng();
editor.setContent('<p>1234</p>');
rng.setStart(editor.getBody().firstChild.firstChild, 0);
rng.setEnd(editor.getBody().firstChild.firstChild, 4);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: '<p class="ListStyle" style="margin-left:36.0pt;mso-add-space:auto;text-indent:-18.0pt;mso-list:l0 level1 lfo1;tab-stops:list 36.0pt"><span lang="EN-US" style="color:black;mso-ansi-language:EN-US"><span style="mso-list:Ignore">1.<span style="font:7.0pt &quot;Times New Roman&quot;">&nbsp;&nbsp;&nbsp;&nbsp; </span></span></span><span lang="EN-US" style="font-family:Arial;mso-fareast-font-family:Arial;mso-bidi-font-family:Arial;color:black;mso-ansi-language:EN-US">Version 7.0</span><span lang="EN-US" style="font-family:Arial;mso-fareast-font-family:Arial;mso-bidi-font-family:Arial;color:black;mso-ansi-language:EN-US">:<o:p></o:p></span></p>' });
LegacyUnit.equal(editor.getContent(), '<ol><li>Version 7.0:</li></ol>');
});
suite.test('Paste Word fake list before BR', function (editor) {
let rng = editor.dom.createRng();
editor.setContent('<p>1234</p>');
rng.setStart(editor.getBody().firstChild.firstChild, 0);
rng.setEnd(editor.getBody().firstChild.firstChild, 4);
editor.selection.setRng(rng);
editor.execCommand('mceInsertContent', false, '<br>a');
rng = editor.dom.createRng();
rng.setStart(editor.getBody().firstChild, 0);
rng.setEnd(editor.getBody().firstChild, 0);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: Strings.wordList1 });
LegacyUnit.equal(editor.getContent(), '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li><li>Item 5</li><li>Item 6</li></ul><p><br />a</p>');
});
suite.test('Paste Word fake lists interrupted by header', function (editor) {
const rng = editor.dom.createRng();
editor.setContent('<p>1234</p>');
rng.setStart(editor.getBody().firstChild.firstChild, 0);
rng.setEnd(editor.getBody().firstChild.firstChild, 4);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: '<p class=MsoListParagraphCxSpFirst style=\'text-indent:-.25in;mso-list:l0 level1 lfo1\'><![if !supportLists]><span style=\'font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family: Symbol\'><span style=\'mso-list:Ignore\'>·<span style=\'font:7.0pt "Times New Roman"\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span></span></span><![endif]>List before heading A<o:p></o:p></p> <p class=MsoListParagraphCxSpLast style=\'text-indent:-.25in;mso-list:l0 level1 lfo1\'><![if !supportLists]><span style=\'font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family: Symbol\'><span style=\'mso-list:Ignore\'>·<span style=\'font:7.0pt "Times New Roman"\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span></span></span><![endif]>List before heading B<o:p></o:p></p> <h1>heading<o:p></o:p></h1> <p class=MsoListParagraphCxSpFirst style=\'text-indent:-.25in;mso-list:l0 level1 lfo1\'><![if !supportLists]><span style=\'font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family: Symbol\'><span style=\'mso-list:Ignore\'>·<span style=\'font:7.0pt "Times New Roman"\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span></span></span><![endif]>List after heading A<o:p></o:p></p> <p class=MsoListParagraphCxSpLast style=\'text-indent:-.25in;mso-list:l0 level1 lfo1\'><![if !supportLists]><span style=\'font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family: Symbol\'><span style=\'mso-list:Ignore\'>·<span style=\'font:7.0pt "Times New Roman"\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span></span></span><![endif]>List after heading B<o:p></o:p></p>' });
LegacyUnit.equal(editor.getContent(), '<ul><li>List before heading A</li><li>List before heading B</li></ul><h1>heading</h1><ul><li>List after heading A</li><li>List after heading B</li></ul>');
});
suite.test('Paste list like paragraph and list', function (editor) {
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, {
content: '<p class=MsoNormal><span style=\'font-size:10.0pt;line-height:115%;font-family:"Trebuchet MS","sans-serif";color:#666666\'>ABC. X<o:p></o:p></span></p><p class=MsoListParagraph style=\'text-indent:-.25in;mso-list:l0 level1 lfo1\'><![if !supportLists]><span style=\'mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin;mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin\'><span style=\'mso-list:Ignore\'>1.<span style=\'font:7.0pt "Times New Roman"\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></span></span><![endif]>Y</p>'
});
LegacyUnit.equal(editor.getContent(), '<p>ABC. X</p><ol><li>Y</li></ol>');
});
suite.test('Paste list like paragraph and list (disabled)', function (editor) {
editor.setContent('');
editor.settings.paste_convert_word_fake_lists = false;
editor.execCommand('mceInsertClipboardContent', false, {
content: '<p class=MsoNormal><span style=\'font-size:10.0pt;line-height:115%;font-family:"Trebuchet MS","sans-serif";color:#666666\'>ABC. X<o:p></o:p></span></p><p class=MsoListParagraph style=\'text-indent:-.25in;mso-list:l0 level1 lfo1\'><![if !supportLists]><span style=\'mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin;mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin\'><span style=\'mso-list:Ignore\'>1.<span style=\'font:7.0pt "Times New Roman"\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></span></span><![endif]>Y</p>'
});
delete editor.settings.paste_convert_word_fake_lists;
LegacyUnit.equal(editor.getContent(), '<p>ABC. X</p><p>1.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Y</p>');
});
suite.test('Paste Word table', function (editor) {
const rng = editor.dom.createRng();
editor.setContent('<p>1234</p>');
rng.setStart(editor.getBody().firstChild.firstChild, 0);
rng.setEnd(editor.getBody().firstChild.firstChild, 4);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: Strings.table });
LegacyUnit.equal(editor.getContent(), '<table><tbody><tr><td width="307"><p>Cell 1</p></td><td width="307"><p>Cell 2</p></td></tr><tr><td width="307"><p>Cell 3</p></td><td width="307"><p>Cell 4</p></td></tr></tbody></table><p>&nbsp;</p>');
});
suite.test('Paste Office 365', function (editor) {
const rng = editor.dom.createRng();
editor.setContent('<p>1234</p>');
rng.setStart(editor.getBody().firstChild.firstChild, 0);
rng.setEnd(editor.getBody().firstChild.firstChild, 4);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: '<div class="OutlineElement Ltr SCX195156559">Test</div>' });
LegacyUnit.equal(editor.getContent(), '<p>Test</p>');
});
suite.test('Paste Google Docs 1', function (editor) {
const rng = editor.dom.createRng();
editor.setContent('<p>1234</p>');
rng.setStart(editor.getBody().firstChild.firstChild, 0);
rng.setEnd(editor.getBody().firstChild.firstChild, 4);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: '<span id="docs-internal-guid-94e46f1a-1c88-b42b-d502-1d19da30dde7"></span><p dir="ltr>Test</p>' });
LegacyUnit.equal(editor.getContent(), '<p>Test</p>');
});
suite.test('Paste Google Docs 2', function (editor) {
const rng = editor.dom.createRng();
editor.setContent('<p>1234</p>');
rng.setStart(editor.getBody().firstChild.firstChild, 0);
rng.setEnd(editor.getBody().firstChild.firstChild, 4);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, {
content: (
'<meta charset="utf-8">' +
'<b style="font-weight:normal;" id="docs-internal-guid-adeb6845-fec6-72e6-6831-5e3ce002727c">' +
'<p dir="ltr">a</p>' +
'<p dir="ltr">b</p>' +
'<p dir="ltr">c</p>' +
'</b>' +
'<br class="Apple-interchange-newline">'
)
});
LegacyUnit.equal(editor.getContent(), '<p>a</p><p>b</p><p>c</p>');
});
suite.test('Paste Word without mso markings', function (editor) {
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, {
content: (
'<font face="Times New Roman" size="3"></font>' +
'<p style="margin: 0in 0in 10pt;">' +
'<span style=\'line-height: 115%; font-family: "Comic Sans MS"; font-size: 22pt;\'>Comic Sans MS</span>' +
'</p>' +
'<font face="Times New Roman" size="3"></font>'
)
});
LegacyUnit.equal(editor.getContent(), (
'<p>Comic Sans MS</p>'
));
});
suite.test('Paste Word links', function (editor) {
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, {
content: (
'<p class="MsoNormal">' +
'<a href="file:///C:/somelocation/filename.doc#_Toc238571849">1</a>' +
'<a href="#_Toc238571849">2</a>' +
'<a name="Toc238571849">3</a>' +
'<a name="_Toc238571849">4</a>' +
'<a href="#_ftn238571849" name="_ftnref238571849">[5]</a>' +
'<a href="#_ftnref238571849" name="_ftn238571849">[5]</a>' +
'<a href="#_edn238571849" name="_ednref238571849">[6]</a>' +
'<a href="#_ednref238571849" name="_edn238571849">[7]</a>' +
'<a href="http://domain.tinymce.com/someurl">8</a>' +
'<a name="#unknown">9</a>' +
'<a href="http://domain.tinymce.com/someurl" name="named_link">named_link</a>' +
'<a>5</a>' +
'</p>'
)
});
LegacyUnit.equal(editor.getContent(), (
'<p>' +
'<a href="#_Toc238571849">1</a>' +
'<a href="#_Toc238571849">2</a>' +
'<a name="Toc238571849"></a>3' +
'<a name="_Toc238571849"></a>4' +
'<a href="#_ftn238571849" name="_ftnref238571849">[5]</a>' +
'<a href="#_ftnref238571849" name="_ftn238571849">[5]</a>' +
'<a href="#_edn238571849" name="_ednref238571849">[6]</a>' +
'<a href="#_ednref238571849" name="_edn238571849">[7]</a>' +
'<a href="http://domain.tinymce.com/someurl">8</a>' +
'9' +
'named_link' +
'5' +
'</p>'
));
});
suite.test('Paste Word retain styles', function (editor) {
editor.settings.paste_retain_style_properties = 'color,background-color,font-family';
// Test color
editor.setContent('');
editor.execCommand('SelectAll');
editor.execCommand('mceInsertClipboardContent', false, { content: '<p class="MsoNormal" style="color: #ff0000">Test</p>' });
LegacyUnit.equal(editor.getContent(), '<p style=\"color: #ff0000;\">Test</p>');
// Test background-color
editor.setContent('');
editor.execCommand('SelectAll');
editor.execCommand('mceInsertClipboardContent', false, { content: '<p class="MsoNormal" style="background-color: #ff0000">Test</p>' });
LegacyUnit.equal(editor.getContent(), '<p style=\"background-color: #ff0000;\">Test</p>');
});
suite.test('Paste Word retain bold/italic styles to elements', function (editor) {
editor.settings.paste_retain_style_properties = 'color';
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, {
content: (
'<p class="MsoNormal">' +
'<span style="font-weight: bold">bold</span>' +
'<span style="font-style: italic">italic</span>' +
'<span style="font-weight: bold; font-style: italic">bold + italic</span>' +
'<span style="font-weight: bold; color: red">bold + color</span>' +
'</p>'
)
});
LegacyUnit.equal(editor.getContent(), '<p><strong>bold</strong><em>italic</em><strong><em>bold + italic</em></strong><strong><span style="color: red;">bold + color</span></strong></p>');
});
suite.test('paste track changes comment', function (editor) {
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, {
content: (
'<p class="MsoNormal">1</p>' +
'<div style="mso-element: comment;">2</div>' +
'<span class="msoDel">3</span>' +
'<del>4</del>'
)
});
LegacyUnit.equal(editor.getContent(), '<p>1</p>');
});
suite.test('paste nested (UL) word list', function (editor) {
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, {
content: (
'<p class=MsoListParagraphCxSpFirst style=\'text-indent:-18.0pt;mso-list:l0 level1 lfo1\'>' +
'<![if !supportLists]><span style=\'font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family:Symbol\'>' +
'<span style=\'mso-list:Ignore\'>·<span style=\'font:7.0pt "Times New Roman"\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' +
'</span></span></span><![endif]>a</p>' +
'<p class=MsoListParagraphCxSpMiddle style=\'margin-left:72.0pt;mso-add-space:auto;text-indent:-18.0pt;mso-list:l0 level2 lfo1\'>' +
'<![if !supportLists]><span style=\'font-family:"Courier New";mso-fareast-font-family:"Courier New"\'>' +
'<span style=\'mso-list:Ignore\'>o<span style=\'font:7.0pt "Times New Roman"\'>&nbsp;&nbsp;</span></span></span><![endif]>b</p>' +
'<p class=MsoListParagraphCxSpLast style=\'margin-left:108.0pt;mso-add-space:auto;text-indent:-18.0pt;mso-list:l0 level3 lfo1\'>' +
'<![if !supportLists]><span style=\'font-family:Wingdings;mso-fareast-font-family:Wingdings;mso-bidi-font-family:Wingdings\'>' +
'<span style=\'mso-list:Ignore\'>§<span style=\'font:7.0pt "Times New Roman"\'>&nbsp;</span></span></span><![endif]>c 1. x</p>'
)
});
LegacyUnit.equal(
editor.getContent(),
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b' +
'<ul>' +
'<li>c 1. x</li>' +
'</ul>' +
'</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
});
suite.test('paste nested (OL) word list', function (editor) {
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, {
content: (
'<p class=MsoListParagraphCxSpFirst style=\'text-indent:-18.0pt;mso-list:l0 level1 lfo1\'>' +
'<![if !supportLists]><span style=\'mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin\'>' +
'<span style=\'mso-list:Ignore\'>1.<span style=\'font:7.0pt "Times New Roman"\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>' +
'</span></span><![endif]>a</p>' +
'<p class=MsoListParagraphCxSpMiddle style=\'margin-left:72.0pt;mso-add-space:auto;text-indent:-18.0pt;mso-list:l0 level2 lfo1\'>' +
'<![if !supportLists]><span style=\'mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin\'><span style=\'mso-list:Ignore\'>a.' +
'<span style=\'font:7.0pt "Times New Roman"\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></span></span><![endif]>b</p>' +
'<p class=MsoListParagraphCxSpLast style=\'margin-left:108.0pt;mso-add-space:auto;text-indent:-108.0pt;mso-text-indent-alt:-9.0pt;mso-list:l0 level3 lfo1\'>' +
'<![if !supportLists]><span style=\'mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin\'><span style=\'mso-list:Ignore\'>' +
'<span style=\'font:7.0pt "Times New Roman"\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' +
'&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' +
'&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' +
'&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>i.<span style=\'font:7.0pt "Times New Roman"\'>' +
'&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></span></span><![endif]>c</p>'
)
});
LegacyUnit.equal(
editor.getContent(),
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b' +
'<ol>' +
'<li>c</li>' +
'</ol>' +
'</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
});
suite.test('Paste list start index', function (editor) {
editor.settings.paste_merge_formats = true;
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, {
content: (
'<p class=MsoListParagraphCxSpMiddle style="text-indent:-18.0pt;mso-list:l0 level1 lfo1">' +
'<![if !supportLists]><span style="mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin;' +
'mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin"><span style="mso-list:Ignore">10.' +
'<span style="font:7.0pt Times>&nbsp;&nbsp;</span></span></span><![endif]>J<o:p></o:p></p>'
)
});
LegacyUnit.equal(editor.getContent(), '<ol start="10"><li>J</li></ol>');
});
suite.test('Paste paste_merge_formats: true', function (editor) {
editor.settings.paste_merge_formats = true;
editor.setContent('<p><strong>a</strong></p>');
LegacyUnit.setSelection(editor, 'p', 1);
editor.execCommand('mceInsertClipboardContent', false, { content: '<em><strong>b</strong></em>' });
LegacyUnit.equal(editor.getContent(), '<p><strong>a<em>b</em></strong></p>');
});
suite.test('Paste paste_merge_formats: false', function (editor) {
editor.settings.paste_merge_formats = false;
editor.setContent('<p><strong>a</strong></p>');
LegacyUnit.setSelection(editor, 'p', 1);
editor.execCommand('mceInsertClipboardContent', false, { content: '<em><strong>b</strong></em>' });
LegacyUnit.equal(editor.getContent(), '<p><strong>a<em><strong>b</strong></em></strong></p>');
});
suite.test('Paste word DIV as P', function (editor) {
editor.setContent('');
editor.execCommand('SelectAll');
editor.execCommand('mceInsertClipboardContent', false, { content: '<p class="MsoNormal">1</p><div>2</div>' });
LegacyUnit.equal(editor.getContent(), '<p>1</p><p>2</p>');
});
if (Env.ie) {
suite.test('Paste part of list from IE', function (editor) {
editor.setContent('');
editor.execCommand('SelectAll');
editor.execCommand('mceInsertClipboardContent', false, { content: '<li>item2</li><li>item3</li>' });
LegacyUnit.equal(trimContent(editor.getContent()), '<ul><li>item2</li><li>item3</li></ul>', 'List tags are inferred when pasting LI');
});
}
suite.test('Disable default filters', function (editor) {
editor.settings.paste_enable_default_filters = false;
// Test color
editor.setContent('');
editor.execCommand('SelectAll');
editor.execCommand('mceInsertClipboardContent', false, { content: '<p class="MsoNormal" style="color: #ff0000;">Test</p>' });
LegacyUnit.equal(editor.getContent(), '<p class="MsoNormal" style="color: #ff0000;">Test</p>');
});
suite.test('paste invalid content with spans on page', function (editor) {
const startingContent = '<p>123 testing <span id="x">span later in document</span></p>',
insertedContent = '<ul><li>u</li><li>l</li></ul>';
editor.setContent(startingContent);
const rng = editor.dom.createRng();
rng.setStart(editor.dom.select('p')[0].firstChild, 0);
rng.setEnd(editor.dom.select('p')[0].firstChild, 0);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { content: insertedContent });
LegacyUnit.equal(editor.getContent(), insertedContent + startingContent);
});
suite.test('paste plain text with space', function (editor) {
editor.setContent('<p>text</p>');
const rng = editor.dom.createRng();
rng.setStart(editor.dom.select('p')[0].firstChild, 1);
rng.setEnd(editor.dom.select('p')[0].firstChild, 2);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { text: ' a ' });
LegacyUnit.equal(editor.getContent(), '<p>t a xt</p>');
});
suite.test('paste plain text with linefeeds', function (editor) {
editor.setContent('<p>text</p>');
const rng = editor.dom.createRng();
rng.setStart(editor.dom.select('p')[0].firstChild, 1);
rng.setEnd(editor.dom.select('p')[0].firstChild, 2);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { text: 'a\nb\nc ' });
LegacyUnit.equal(editor.getContent(), '<p>ta<br />b<br />c xt</p>');
});
suite.test('paste plain text with double linefeeds', function (editor) {
editor.setContent('<p>text</p>');
const rng = editor.dom.createRng();
rng.setStart(editor.dom.select('p')[0].firstChild, 1);
rng.setEnd(editor.dom.select('p')[0].firstChild, 2);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { text: 'a\n\nb\n\nc' });
LegacyUnit.equal(editor.getContent(), '<p>t</p><p>a</p><p>b</p><p>c</p><p>xt</p>');
});
suite.test('paste plain text with entities', function (editor) {
editor.setContent('<p>text</p>');
const rng = editor.dom.createRng();
rng.setStart(editor.dom.select('p')[0].firstChild, 1);
rng.setEnd(editor.dom.select('p')[0].firstChild, 2);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { text: '< & >' });
LegacyUnit.equal(editor.getContent(), '<p>t&lt; &amp; &gt;xt</p>');
});
suite.test('paste plain text with paragraphs', function (editor) {
editor.setContent('<p>text</p>');
const rng = editor.dom.createRng();
rng.setStart(editor.dom.select('p')[0].firstChild, 1);
rng.setEnd(editor.dom.select('p')[0].firstChild, 2);
editor.selection.setRng(rng);
editor.execCommand('mceInsertClipboardContent', false, { text: 'a\n<b>b</b>\n\nc' });
LegacyUnit.equal(editor.getContent(), '<p>t</p><p>a<br />&lt;b&gt;b&lt;/b&gt;</p><p>c</p><p>xt</p>');
});
suite.test('paste data image with paste_data_images: false', function (editor) {
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<img src="data:image/gif;base64,R0lGODlhAQABAPAAAP8REf///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==">' });
LegacyUnit.equal(editor.getContent(), '');
editor.execCommand('mceInsertClipboardContent', false, { content: '<img alt="alt" src="data:image/gif;base64,R0lGODlhAQABAPAAAP8REf///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==">' });
LegacyUnit.equal(editor.getContent(), '');
});
suite.test('paste data image with paste_data_images: true', function (editor) {
editor.settings.paste_data_images = true;
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<img src="data:image/gif;base64,R0lGODlhAQABAPAAAP8REf///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==">' });
LegacyUnit.equal(editor.getContent(), '<p><img src="data:image/gif;base64,R0lGODlhAQABAPAAAP8REf///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" /></p>');
});
suite.test('paste pre process text (event)', function (editor) {
function callback(e) {
e.content = 'PRE:' + e.content;
}
editor.setContent('<p>a</p>');
LegacyUnit.setSelection(editor, 'p', 0, 'p', 1);
editor.on('PastePreProcess', callback);
editor.execCommand('mceInsertClipboardContent', false, { text: 'b\n2' });
LegacyUnit.equal(editor.getContent(), '<p>PRE:b<br />2</p>');
editor.setContent('<p>a</p>');
LegacyUnit.setSelection(editor, 'p', 0, 'p', 1);
editor.off('PastePreProcess', callback);
editor.execCommand('mceInsertClipboardContent', false, { text: 'c' });
LegacyUnit.equal(editor.getContent(), '<p>c</p>');
});
suite.test('paste pre process html (event)', function (editor) {
function callback(e) {
e.content = 'PRE:' + e.content;
}
editor.setContent('<p>a</p>');
LegacyUnit.setSelection(editor, 'p', 0, 'p', 1);
editor.on('PastePreProcess', callback);
editor.execCommand('mceInsertClipboardContent', false, { content: '<em>b</em>' });
LegacyUnit.equal(editor.getContent(), '<p>PRE:<em>b</em></p>');
editor.setContent('<p>a</p>');
LegacyUnit.setSelection(editor, 'p', 0, 'p', 1);
editor.off('PastePreProcess', callback);
editor.execCommand('mceInsertClipboardContent', false, { content: '<em>c</em>' });
LegacyUnit.equal(editor.getContent(), '<p><em>c</em></p>');
});
suite.test('paste post process (event)', function (editor) {
function callback(e) {
e.node.innerHTML += ':POST';
}
editor.setContent('<p>a</p>');
LegacyUnit.setSelection(editor, 'p', 0, 'p', 1);
editor.on('PastePostProcess', callback);
editor.execCommand('mceInsertClipboardContent', false, { content: '<em>b</em>' });
LegacyUnit.equal(editor.getContent(), '<p><em>b</em>:POST</p>');
editor.setContent('<p>a</p>');
LegacyUnit.setSelection(editor, 'p', 0, 'p', 1);
editor.off('PastePostProcess', callback);
editor.execCommand('mceInsertClipboardContent', false, { content: '<em>c</em>' });
LegacyUnit.equal(editor.getContent(), '<p><em>c</em></p>');
});
suite.test('paste innerText of conditional comments', function () {
LegacyUnit.equal(Utils.innerText('<![if !supportLists]>X<![endif]>'), 'X');
});
suite.test('paste innerText of single P', function (editor) {
editor.setContent('<p>a</p>');
LegacyUnit.equal(Utils.innerText(editor.getBody().innerHTML), 'a');
});
suite.test('paste innerText of single P with whitespace wrapped content', function (editor) {
editor.setContent('<p> a </p>');
LegacyUnit.equal(Utils.innerText(editor.getBody().innerHTML), 'a');
});
suite.test('paste innerText of two P', function (editor) {
editor.setContent('<p>a</p><p>b</p>');
LegacyUnit.equal(Utils.innerText(editor.getBody().innerHTML), 'a\n\nb');
});
suite.test('paste innerText of H1 and P', function (editor) {
editor.setContent('<h1>a</h1><p>b</p>');
LegacyUnit.equal(Utils.innerText(editor.getBody().innerHTML), 'a\nb');
});
suite.test('paste innerText of P with BR', function (editor) {
editor.setContent('<p>a<br>b</p>');
LegacyUnit.equal(Utils.innerText(editor.getBody().innerHTML), 'a\nb');
});
suite.test('paste innerText of P with WBR', function (editor) {
editor.setContent('<p>a<wbr>b</p>');
LegacyUnit.equal(Utils.innerText(editor.getBody().innerHTML), 'ab');
});
suite.test('paste innerText of P with VIDEO', function (editor) {
editor.setContent('<p>a<video>b<br>c</video>d</p>');
LegacyUnit.equal(Utils.innerText(editor.getBody().innerHTML), 'a d');
});
suite.test('paste innerText of PRE', function (editor) {
editor.getBody().innerHTML = '<pre>a\nb\n</pre>';
LegacyUnit.equal(Utils.innerText(editor.getBody().innerHTML).replace(/\r\n/g, '\n'), 'a\nb\n');
});
suite.test('paste innerText of textnode with whitespace', function (editor) {
editor.getBody().innerHTML = '<pre> a </pre>';
LegacyUnit.equal(Utils.innerText(editor.getBody().firstChild.innerHTML), ' a ');
});
suite.test('trim html from clipboard fragments', function () {
LegacyUnit.equal(Utils.trimHtml('<!--StartFragment-->a<!--EndFragment-->'), 'a');
LegacyUnit.equal(Utils.trimHtml('a\n<body>\n<!--StartFragment-->\nb\n<!--EndFragment-->\n</body>\nc'), '\nb\n');
LegacyUnit.equal(Utils.trimHtml('a<!--StartFragment-->b<!--EndFragment-->c'), 'abc');
LegacyUnit.equal(Utils.trimHtml('a<body>b</body>c'), 'b');
LegacyUnit.equal(Utils.trimHtml('<HTML><HEAD><TITLE>a</TITLE></HEAD><BODY>b</BODY></HTML>'), 'b');
LegacyUnit.equal(Utils.trimHtml('a<span class="Apple-converted-space">\u00a0<\/span>b'), 'a b');
LegacyUnit.equal(Utils.trimHtml('<span class="Apple-converted-space">\u00a0<\/span>b'), ' b');
LegacyUnit.equal(Utils.trimHtml('a<span class="Apple-converted-space">\u00a0<\/span>'), 'a ');
LegacyUnit.equal(Utils.trimHtml('<span class="Apple-converted-space">\u00a0<\/span>'), ' ');
});
if (Env.ie) {
suite.test('paste font and u in anchor', function (editor) {
editor.setContent('<p>a</p>');
LegacyUnit.setSelection(editor, 'p', 1);
editor.execCommand('mceInsertClipboardContent', false, {
content: '<p><a href="#"><font size="3"><u>b</u></font></a></p>'
});
LegacyUnit.equal(editor.getContent(), '<p>a</p><p><a href="#">b</a></p>');
});
}
if (Env.webkit) {
suite.test('paste webkit retains text styles runtime styles internal', function (editor) {
editor.settings.paste_webkit_styles = 'color';
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '&lt;span style="color:red"&gt;&lt;span data-mce-style="color:red"&gt;' });
LegacyUnit.equal(editor.getContent(), '<p>&lt;span style="color:red"&gt;&lt;span data-mce-style="color:red"&gt;</p>');
});
suite.test('paste webkit remove runtime styles internal', function (editor) {
editor.settings.paste_webkit_styles = 'color';
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<span style="color:red; font-size: 42px" data-mce-style="color: red;">Test</span>' });
LegacyUnit.equal(editor.getContent(), '<p><span style="color: red;">Test</span></p>');
});
suite.test('paste webkit remove runtime styles (color)', function (editor) {
editor.settings.paste_webkit_styles = 'color';
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<span style="color:red; text-indent: 10px">Test</span>' });
LegacyUnit.equal(editor.getContent(), '<p><span style="color: red;">Test</span></p>');
});
suite.test('paste webkit remove runtime styles keep before attr', function (editor) {
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<span class="c" style="color:red; text-indent: 10px">Test</span>' });
LegacyUnit.equal(editor.getContent(), '<p><span class="c">Test</span></p>');
});
suite.test('paste webkit remove runtime styles keep after attr', function (editor) {
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<span style="color:red; text-indent: 10px" title="t">Test</span>' });
LegacyUnit.equal(editor.getContent(), '<p><span title="t">Test</span></p>');
});
suite.test('paste webkit remove runtime styles keep before/after attr', function (editor) {
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<span class="c" style="color:red; text-indent: 10px" title="t">Test</span>' });
LegacyUnit.equal(editor.getContent(), '<p><span class="c" title="t">Test</span></p>');
});
suite.test('paste webkit remove runtime styles (background-color)', function (editor) {
editor.settings.paste_webkit_styles = 'background-color';
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<span style="background-color:red; text-indent: 10px">Test</span>' });
LegacyUnit.equal(editor.getContent(), '<p><span style="background-color: red;">Test</span></p>');
});
suite.test('paste webkit remove runtime styles (font-size)', function (editor) {
editor.settings.paste_webkit_styles = 'font-size';
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<span style="font-size:42px; text-indent: 10px">Test</span>' });
LegacyUnit.equal(editor.getContent(), '<p><span style="font-size: 42px;">Test</span></p>');
});
suite.test('paste webkit remove runtime styles (font-family)', function (editor) {
editor.settings.paste_webkit_styles = 'font-family';
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<span style="font-family:Arial; text-indent: 10px">Test</span>' });
LegacyUnit.equal(editor.getContent(), '<p><span style="font-family: Arial;">Test</span></p>');
});
suite.test('paste webkit remove runtime styles font-family allowed but not specified', function (editor) {
editor.settings.paste_webkit_styles = 'font-family';
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<p title="x" style="text-indent: 10px">Test</p>' });
LegacyUnit.equal(editor.getContent(), '<p title="x">Test</p>');
});
suite.test('paste webkit remove runtime styles (custom styles)', function (editor) {
editor.settings.paste_webkit_styles = 'color font-style';
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<span style="color: red; font-style: italic; text-indent: 10px">Test</span>' });
LegacyUnit.equal(editor.getContent(), '<p><span style="color: red; font-style: italic;">Test</span></p>');
});
suite.test('paste webkit remove runtime styles (all)', function (editor) {
editor.settings.paste_webkit_styles = 'all';
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<span style="color: red; font-style: italic; text-indent: 10px">Test</span>' });
LegacyUnit.equal(editor.getContent(), '<p><span style=\"color: red; font-style: italic; text-indent: 10px;\">Test</span></p>');
});
suite.test('paste webkit remove runtime styles (none)', function (editor) {
editor.settings.paste_webkit_styles = 'none';
editor.setContent('');
editor.execCommand('mceInsertClipboardContent', false, { content: '<span style="color: red; font-style: italic; text-indent: 10px">Test</span>' });
LegacyUnit.equal(editor.getContent(), '<p>Test</p>');
});
suite.test('paste webkit remove runtime styles (color) in the same (color) (named)', function (editor) {
editor.settings.paste_webkit_styles = 'color';
editor.setContent('<p style="color:red">Test</span>');
LegacyUnit.setSelection(editor, 'p', 0, 'p', 4);
editor.execCommand('mceInsertClipboardContent', false, {
content: (
'<span style="color:#ff0000; text-indent: 10px">a</span>' +
'<span style="color:rgb(255, 0, 0); text-indent: 10px">b</span>'
)
});
LegacyUnit.equal(editor.getContent(), '<p style="color: red;">ab</p>');
});
suite.test('paste webkit remove runtime styles (color) in the same (color) (hex)', function (editor) {
editor.setContent('<p style="color:#ff0000">Test</span>');
LegacyUnit.setSelection(editor, 'p', 0, 'p', 4);
editor.execCommand('mceInsertClipboardContent', false, {
content: (
'<span style="color:red; text-indent: 10px">a</span>' +
'<span style="color:#ff0000; text-indent: 10px">b</span>' +
'<span style="color:rgb(255, 0, 0); text-indent: 10px">c</span>'
)
});
LegacyUnit.equal(editor.getContent(), '<p style="color: #ff0000;">abc</p>');
});
suite.test('paste webkit remove runtime styles (color) in the same (color) (rgb)', function (editor) {
editor.setContent('<p style="color:rgb(255, 0, 0)">Test</span>');
LegacyUnit.setSelection(editor, 'p', 0, 'p', 4);
editor.execCommand('mceInsertClipboardContent', false, {
content: (
'<span style="color:red; text-indent: 10px">a</span>' +
'<span style="color:#ff0000; text-indent: 10px">b</span>' +
'<span style="color:rgb(255, 0, 0); text-indent: 10px">c</span>'
)
});
LegacyUnit.equal(editor.getContent(), '<p style="color: #ff0000;">abc</p>');
});
}
TinyLoader.setup(function (editor, onSuccess, onFailure) {
Pipeline.async({}, appendTeardown(editor, suite.toSteps(editor)), onSuccess, onFailure);
}, {
add_unload_trigger: false,
indent: false,
plugins: 'paste',
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,147 @@
import { Assertions, Chain, Guard, Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { Id, Merger, Obj } from '@ephox/katamari';
import EditorManager from 'tinymce/core/api/EditorManager';
import PastePlugin from 'tinymce/plugins/paste/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
import MockDataTransfer from '../module/test/MockDataTransfer';
import ViewBlock from '../module/test/ViewBlock';
UnitTest.asynctest('tinymce.plugins.paste.browser.PlainTextPaste', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const viewBlock = ViewBlock();
const cCreateEditorFromSettings = function (settings, html?) {
return Chain.async(function (viewBlock: any, next, die) {
const randomId = Id.generate('tiny-');
html = html || '<textarea></textarea>';
viewBlock.update(html);
viewBlock.get().firstChild.id = randomId;
EditorManager.init(Merger.merge(settings, {
selector: '#' + randomId,
skin_url: '/project/js/tinymce/skins/lightgray',
indent: false,
setup (editor) {
editor.on('SkinLoaded', function () {
setTimeout(function () {
next(editor);
}, 0);
});
}
}));
});
};
const cRemoveEditor = function () {
return Chain.op(function (editor: any) {
editor.remove();
});
};
const cClearEditor = function () {
return Chain.async(function (editor: any, next, die) {
editor.setContent('');
next(editor);
});
};
const cFireFakePasteEvent = function (data) {
return Chain.async(function (editor: any, next, die) {
editor.fire('paste', { clipboardData: MockDataTransfer.create(data) });
next(editor);
});
};
const cAssertEditorContent = function (label, expected) {
return Chain.async(function (editor: any, next, die) {
Assertions.assertHtml(label || 'Asserting editors content', expected, editor.getContent());
next(editor);
});
};
const cAssertClipboardPaste = function (expected, data) {
const chains = [];
Obj.each(data, function (data, label) {
chains.push(
cFireFakePasteEvent(data),
Chain.control(
cAssertEditorContent(label, expected),
Guard.tryUntil('Wait for paste to succeed.', 100, 1000)
),
cClearEditor()
);
});
return Chain.fromChains(chains);
};
const srcText = 'one\r\ntwo\r\n\r\nthree\r\n\r\n\r\nfour\r\n\r\n\r\n\r\n.';
const pasteData = {
Firefox: {
'text/plain': srcText,
'text/html': 'one<br>two<br><br>three<br><br><br>four<br><br><br><br>.'
},
Chrome: {
'text/plain': srcText,
'text/html': '<div>one</div><div>two</div><div><br></div><div>three</div><div><br></div><div><br></div><div>four</div><div><br></div><div><br></div><div><br></div><div>.'
},
Edge: {
'text/plain': srcText,
'text/html': '<div>one<br>two</div><div>three</div><div><br>four</div><div><br></div><div>.</div>'
},
IE: {
'text/plain': srcText,
'text/html': '<p>one<br>two</p><p>three</p><p><br>four</p><p><br></p><p>.</p>'
}
};
const expectedWithRootBlock = '<p>one<br />two</p><p>three</p><p><br />four</p><p>&nbsp;</p><p>.</p>';
const expectedWithRootBlockAndAttrs = '<p class="attr">one<br />two</p><p class="attr">three</p><p class="attr"><br />four</p><p class="attr">&nbsp;</p><p class="attr">.</p>';
const expectedWithoutRootBlock = 'one<br />two<br /><br />three<br /><br /><br />four<br /><br /><br /><br />.';
Theme();
PastePlugin();
viewBlock.attach();
Pipeline.async({}, [
Chain.asStep(viewBlock, [
cCreateEditorFromSettings({
plugins: 'paste',
forced_root_block: 'p' // default
}),
cAssertClipboardPaste(expectedWithRootBlock, pasteData),
cRemoveEditor()
]),
Chain.asStep(viewBlock, [
cCreateEditorFromSettings({
plugins: 'paste',
forced_root_block: 'p',
forced_root_block_attrs: {
class: 'attr'
}
}),
cAssertClipboardPaste(expectedWithRootBlockAndAttrs, pasteData),
cRemoveEditor()
]),
Chain.asStep(viewBlock, [
cCreateEditorFromSettings({
plugins: 'paste',
forced_root_block: false
}),
cAssertClipboardPaste(expectedWithoutRootBlock, pasteData),
cRemoveEditor()
])
], function () {
viewBlock.detach();
success();
}, failure);
});

View File

@@ -0,0 +1,116 @@
import { Assertions, Chain, Logger, Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { Fun } from '@ephox/katamari';
import { TinyLoader } from '@ephox/mcagar';
import ProcessFilters from 'tinymce/plugins/paste/core/ProcessFilters';
import PastePlugin from 'tinymce/plugins/paste/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('tinymce.plugins.paste.browser.ProcessFiltersTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
Theme();
PastePlugin();
const cProcessPre = function (html, internal, preProcess) {
return Chain.mapper(function (editor: any) {
editor.on('PastePreProcess', preProcess);
const result = ProcessFilters.process(editor, html, internal);
editor.off('PastePreProcess', preProcess);
return result;
});
};
const cProcessPrePost = function (html, internal, preProcess, postProcess) {
return Chain.mapper(function (editor: any) {
editor.on('PastePreProcess', preProcess);
editor.on('PastePostProcess', postProcess);
const result = ProcessFilters.process(editor, html, internal);
editor.off('PastePreProcess', preProcess);
editor.off('PastePostProcess', postProcess);
return result;
});
};
const preventHandler = function (e) {
e.preventDefault();
};
const preProcessHandler = function (e) {
e.content += 'X';
};
const postProcessHandler = function (editor) {
return function (e) {
editor.dom.remove(editor.dom.select('b', e.node), true);
};
};
const assertInternal = function (expectedFlag) {
return function (e) {
Assertions.assertEq('Should be expected internal flag', expectedFlag, e.internal);
};
};
TinyLoader.setup(function (editor, onSuccess, onFailure) {
Pipeline.async({}, [
Logger.t('Paste pre process only', Chain.asStep(editor, [
cProcessPre('a', true, preProcessHandler),
Assertions.cAssertEq('Should be preprocessed by adding a X', { content: 'aX', cancelled: false })
])),
Logger.t('Paste pre/post process passthough as is', Chain.asStep(editor, [
cProcessPrePost('a', true, Fun.noop, Fun.noop),
Assertions.cAssertEq('Should be unchanged', { content: 'a', cancelled: false })
])),
Logger.t('Paste pre/post process assert internal false', Chain.asStep(editor, [
cProcessPrePost('a', false, assertInternal(false), assertInternal(false)),
Assertions.cAssertEq('Should be unchanged', { content: 'a', cancelled: false })
])),
Logger.t('Paste pre/post process assert internal true', Chain.asStep(editor, [
cProcessPrePost('a', true, assertInternal(true), assertInternal(true)),
Assertions.cAssertEq('Should be unchanged', { content: 'a', cancelled: false })
])),
Logger.t('Paste pre/post process alter on preprocess', Chain.asStep(editor, [
cProcessPrePost('a', true, preProcessHandler, Fun.noop),
Assertions.cAssertEq('Should be preprocessed by adding a X', { content: 'aX', cancelled: false })
])),
Logger.t('Paste pre/post process alter on postprocess', Chain.asStep(editor, [
cProcessPrePost('a<b>b</b>c', true, Fun.noop, postProcessHandler(editor)),
Assertions.cAssertEq('Should have all b elements removed', { content: 'abc', cancelled: false })
])),
Logger.t('Paste pre/post process alter on preprocess/postprocess', Chain.asStep(editor, [
cProcessPrePost('a<b>b</b>c', true, preProcessHandler, postProcessHandler(editor)),
Assertions.cAssertEq('Should have all b elements removed and have a X added', { content: 'abcX', cancelled: false })
])),
Logger.t('Paste pre/post process prevent default on preProcess', Chain.asStep(editor, [
cProcessPrePost('a<b>b</b>c', true, preventHandler, postProcessHandler(editor)),
Assertions.cAssertEq('Should have all b elements removed and be cancelled', { content: 'a<b>b</b>c', cancelled: true })
])),
Logger.t('Paste pre/post process prevent default on postProcess', Chain.asStep(editor, [
cProcessPrePost('a<b>b</b>c', true, preProcessHandler, preventHandler),
Assertions.cAssertEq('Should have a X added and be cancelled', { content: 'a<b>b</b>cX', cancelled: true })
]))
], onSuccess, onFailure);
}, {
add_unload_trigger: false,
indent: false,
plugins: 'paste',
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,88 @@
import { Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { LegacyUnit, TinyLoader } from '@ephox/mcagar';
import SmartPaste from 'tinymce/plugins/paste/core/SmartPaste';
import Plugin from 'tinymce/plugins/paste/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('tinymce.plugins.paste.browser.ImagePasteTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const suite = LegacyUnit.createSuite();
Plugin();
Theme();
suite.test('isAbsoluteUrl', function () {
LegacyUnit.equal(SmartPaste.isAbsoluteUrl('http://www.site.com'), true);
LegacyUnit.equal(SmartPaste.isAbsoluteUrl('https://www.site.com'), true);
LegacyUnit.equal(SmartPaste.isAbsoluteUrl('http://www.site.com/dir-name/file.gif?query=%42'), true);
LegacyUnit.equal(SmartPaste.isAbsoluteUrl('https://www.site.com/dir-name/file.gif?query=%42'), true);
LegacyUnit.equal(SmartPaste.isAbsoluteUrl('https://www.site.com/dir-name/file.gif?query=%42#a'), true);
LegacyUnit.equal(SmartPaste.isAbsoluteUrl('https://www.site.com/~abc'), true);
LegacyUnit.equal(SmartPaste.isAbsoluteUrl('file.gif'), false);
LegacyUnit.equal(SmartPaste.isAbsoluteUrl(''), false);
});
suite.test('isImageUrl', function () {
LegacyUnit.equal(SmartPaste.isImageUrl('http://www.site.com'), false);
LegacyUnit.equal(SmartPaste.isImageUrl('https://www.site.com'), false);
LegacyUnit.equal(SmartPaste.isImageUrl('http://www.site.com/dir-name/file.jpeg'), true);
LegacyUnit.equal(SmartPaste.isImageUrl('http://www.site.com/dir-name/file.jpg'), true);
LegacyUnit.equal(SmartPaste.isImageUrl('http://www.site.com/dir-name/file.png'), true);
LegacyUnit.equal(SmartPaste.isImageUrl('http://www.site.com/dir-name/file.gif'), true);
LegacyUnit.equal(SmartPaste.isImageUrl('https://www.site.com/dir-name/file.gif'), true);
LegacyUnit.equal(SmartPaste.isImageUrl('https://www.site.com/~dir-name/file.gif'), true);
LegacyUnit.equal(SmartPaste.isImageUrl('https://www.site.com/dir-name/file.gif?query=%42'), false);
LegacyUnit.equal(SmartPaste.isImageUrl('https://www.site.com/dir-name/file.html?query=%42'), false);
LegacyUnit.equal(SmartPaste.isImageUrl('file.gif'), false);
LegacyUnit.equal(SmartPaste.isImageUrl(''), false);
});
suite.test('smart paste url on selection', function (editor) {
editor.focus();
editor.undoManager.clear();
editor.setContent('<p>abc</p>');
LegacyUnit.setSelection(editor, 'p', 0, 'p', 3);
editor.undoManager.add();
editor.execCommand('mceInsertClipboardContent', false, { content: 'http://www.site.com' });
LegacyUnit.equal(editor.getContent(), '<p><a href="http://www.site.com">abc</a></p>');
LegacyUnit.equal(editor.undoManager.data.length, 3);
});
suite.test('smart paste image url', function (editor) {
editor.focus();
editor.undoManager.clear();
editor.setContent('<p>abc</p>');
LegacyUnit.setSelection(editor, 'p', 1);
editor.undoManager.add();
editor.execCommand('mceInsertClipboardContent', false, { content: 'http://www.site.com/my.jpg' });
LegacyUnit.equal(editor.getContent(), '<p>a<img src="http://www.site.com/my.jpg" />bc</p>');
LegacyUnit.equal(editor.undoManager.data.length, 3);
});
suite.test('smart paste option disabled', function (editor) {
editor.focus();
editor.undoManager.clear();
editor.setContent('<p>abc</p>');
LegacyUnit.setSelection(editor, 'p', 1);
editor.undoManager.add();
editor.settings.smart_paste = false;
editor.execCommand('mceInsertClipboardContent', false, { content: 'http://www.site.com/my.jpg' });
LegacyUnit.equal(editor.getContent(), '<p>ahttp://www.site.com/my.jpgbc</p>');
LegacyUnit.equal(editor.undoManager.data.length, 2);
});
TinyLoader.setup(function (editor, onSuccess, onFailure) {
Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure);
}, {
add_unload_trigger: false,
indent: false,
plugins: 'paste',
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,20 @@
import { Assertions } from '@ephox/agar';
import { Editor } from 'tinymce/core/api/Editor';
import EditorManager from 'tinymce/core/api/EditorManager';
import PluginManager from 'tinymce/core/api/PluginManager';
import DetectProPlugin from 'tinymce/plugins/paste/alien/DetectProPlugin';
import { UnitTest } from '@ephox/bedrock';
UnitTest.test('browser.tinymce.plugins.paste.alien.DetectProPluginTest', function () {
// Fake loading of powerpaste
PluginManager.add('powerpaste', function () { });
Assertions.assertEq('Should not have pro plugin', false, DetectProPlugin.hasProPlugin(new Editor('id', { plugins: 'paste' }, EditorManager)));
Assertions.assertEq('Should not have pro plugin', false, DetectProPlugin.hasProPlugin(new Editor('id', { plugins: '' }, EditorManager)));
Assertions.assertEq('Should have pro plugin', true, DetectProPlugin.hasProPlugin(new Editor('id', { plugins: 'powerpaste' }, EditorManager)));
Assertions.assertEq('Should have pro plugin', true, DetectProPlugin.hasProPlugin(new Editor('id', { plugins: 'paste powerpaste' }, EditorManager)));
Assertions.assertEq('Should have pro plugin', true, DetectProPlugin.hasProPlugin(new Editor('id', { plugins: 'powerpaste paste' }, EditorManager)));
Assertions.assertEq('Should have pro plugin', true, DetectProPlugin.hasProPlugin(new Editor('id', { plugins: 'paste powerpaste paste' }, EditorManager)));
Assertions.assertEq('Should have pro plugin', true, DetectProPlugin.hasProPlugin(new Editor('id', { plugins: 'paste,powerpaste,paste' }, EditorManager)));
Assertions.assertEq('Should have pro plugin', true, DetectProPlugin.hasProPlugin(new Editor('id', { plugins: 'paste powerpaste paste' }, EditorManager)));
});

View File

@@ -0,0 +1,61 @@
import { Arr, Obj } from '@ephox/katamari';
const notImplemented = function () {
throw new Error('Mockup function is not implemented.');
};
const createDataTransferItem = function (mime, content) {
return {
kind: 'string',
type: mime,
getAsFile: notImplemented,
getAsString () {
return content;
}
};
};
const create = function (inputData) {
let data = {}, result;
const clearData = function () {
data = {};
result.items = [];
result.types = [];
};
const getData = function (mime) {
return mime in data ? data[mime] : '';
};
const setData = function (mime, content) {
data[mime] = content;
result.types = Obj.keys(data);
result.items = Arr.map(result.types, function (type) {
return createDataTransferItem(type, data[type]);
});
};
result = {
dropEffect: '',
effectAllowed: 'all',
files: [],
items: [],
types: [],
clearData,
getData,
setData,
setDragImage: notImplemented,
addElement: notImplemented
};
Obj.each(inputData, function (value, key) {
setData(key, value);
});
return result;
};
export default {
create
};

View File

@@ -0,0 +1,13 @@
import { Step } from '@ephox/agar';
import MockDataTransfer from './MockDataTransfer';
const sPaste = function (editor, data) {
return Step.sync(function () {
const dataTransfer = MockDataTransfer.create(data);
editor.fire('paste', { clipboardData: dataTransfer });
});
};
export default {
sPaste
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,34 @@
import DOMUtils from 'tinymce/core/api/dom/DOMUtils';
import { document, HTMLElement } from '@ephox/dom-globals';
export default function () {
const domElm: HTMLElement = DOMUtils.DOM.create('div', {
style: 'position: absolute; right: 10px; top: 10px;'
});
const attach = function (preventDuplicates?) {
if (preventDuplicates && domElm.parentNode === document.body) {
detach();
}
document.body.appendChild(domElm);
};
const detach = function () {
DOMUtils.DOM.remove(domElm);
};
const update = function (html) {
DOMUtils.DOM.setHTML(domElm, html);
};
const get = function () {
return domElm;
};
return {
attach,
update,
detach,
get
};
}

View File

@@ -0,0 +1,41 @@
import { Pipeline, RealMouse, Waiter } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { TinyApis, TinyLoader, TinyUi } from '@ephox/mcagar';
import { PlatformDetection } from '@ephox/sand';
import PastePlugin from 'tinymce/plugins/paste/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
import { window } from '@ephox/dom-globals';
UnitTest.asynctest('tinymce.plugins.paste.webdriver.CutTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
Theme();
PastePlugin();
const platform = PlatformDetection.detect();
/* Test does not work on Phantom */
if (window.navigator.userAgent.indexOf('PhantomJS') > -1) {
return success();
}
TinyLoader.setup(function (editor, onSuccess, onFailure) {
const api = TinyApis(editor);
const ui = TinyUi(editor);
// Cut doesn't seem to work in webdriver mode on ie, firefox is producing moveto not supported, edge fails if it's not observed
Pipeline.async({}, (platform.browser.isIE() || platform.browser.isFirefox() || platform.browser.isEdge()) ? [] : [
api.sSetContent('<p>abc</p>'),
api.sSetSelection([0, 0], 1, [0, 0], 2),
ui.sClickOnMenu('Click Edit menu', 'button:contains("Edit")'),
ui.sWaitForUi('Wait for dropdown', '.mce-floatpanel[role="application"]'),
RealMouse.sClickOn('.mce-i-cut'),
Waiter.sTryUntil('Cut is async now, so need to wait for content', api.sAssertContent('<p>ac</p>'), 100, 1000)
], onSuccess, onFailure);
}, {
skin_url: '/project/js/tinymce/skins/lightgray',
plugins: 'paste'
}, success, failure);
});