init
This commit is contained in:
15
public/tinymce/src/plugins/paste/demo/html/demo.html
Normal file
15
public/tinymce/src/plugins/paste/demo/html/demo.html
Normal 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>
|
||||
33
public/tinymce/src/plugins/paste/demo/ts/demo/Demo.ts
Normal file
33
public/tinymce/src/plugins/paste/demo/ts/demo/Demo.ts
Normal 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 {};
|
||||
40
public/tinymce/src/plugins/paste/main/ts/Plugin.ts
Normal file
40
public/tinymce/src/plugins/paste/main/ts/Plugin.ts
Normal 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 () { }
|
||||
@@ -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
|
||||
};
|
||||
19
public/tinymce/src/plugins/paste/main/ts/api/Api.ts
Normal file
19
public/tinymce/src/plugins/paste/main/ts/api/Api.ts
Normal 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
|
||||
};
|
||||
38
public/tinymce/src/plugins/paste/main/ts/api/Clipboard.ts
Normal file
38
public/tinymce/src/plugins/paste/main/ts/api/Clipboard.ts
Normal 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
|
||||
};
|
||||
};
|
||||
30
public/tinymce/src/plugins/paste/main/ts/api/Commands.ts
Normal file
30
public/tinymce/src/plugins/paste/main/ts/api/Commands.ts
Normal 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
|
||||
};
|
||||
32
public/tinymce/src/plugins/paste/main/ts/api/Events.ts
Normal file
32
public/tinymce/src/plugins/paste/main/ts/api/Events.ts
Normal 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
|
||||
};
|
||||
94
public/tinymce/src/plugins/paste/main/ts/api/Settings.ts
Normal file
94
public/tinymce/src/plugins/paste/main/ts/api/Settings.ts
Normal 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
|
||||
};
|
||||
43
public/tinymce/src/plugins/paste/main/ts/core/Actions.ts
Normal file
43
public/tinymce/src/plugins/paste/main/ts/core/Actions.ts
Normal 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
|
||||
};
|
||||
484
public/tinymce/src/plugins/paste/main/ts/core/Clipboard.ts
Normal file
484
public/tinymce/src/plugins/paste/main/ts/core/Clipboard.ts
Normal 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
|
||||
};
|
||||
127
public/tinymce/src/plugins/paste/main/ts/core/CutCopy.ts
Normal file
127
public/tinymce/src/plugins/paste/main/ts/core/CutCopy.ts
Normal 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
|
||||
};
|
||||
113
public/tinymce/src/plugins/paste/main/ts/core/DragDrop.ts
Normal file
113
public/tinymce/src/plugins/paste/main/ts/core/DragDrop.ts
Normal 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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
75
public/tinymce/src/plugins/paste/main/ts/core/Newlines.ts
Normal file
75
public/tinymce/src/plugins/paste/main/ts/core/Newlines.ts
Normal 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
|
||||
};
|
||||
188
public/tinymce/src/plugins/paste/main/ts/core/PasteBin.ts
Normal file
188
public/tinymce/src/plugins/paste/main/ts/core/PasteBin.ts
Normal 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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
171
public/tinymce/src/plugins/paste/main/ts/core/Quirks.ts
Normal file
171
public/tinymce/src/plugins/paste/main/ts/core/Quirks.ts
Normal 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> [\\s\\r\\n]+|<br>)*(<\\/?(' + blockElements.join('|') + ')[^>]*>)(?:<br> [\\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
|
||||
};
|
||||
94
public/tinymce/src/plugins/paste/main/ts/core/SmartPaste.ts
Normal file
94
public/tinymce/src/plugins/paste/main/ts/core/SmartPaste.ts
Normal 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
|
||||
};
|
||||
149
public/tinymce/src/plugins/paste/main/ts/core/Utils.ts
Normal file
149
public/tinymce/src/plugins/paste/main/ts/core/Utils.ts
Normal 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 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
|
||||
};
|
||||
489
public/tinymce/src/plugins/paste/main/ts/core/WordFilter.ts
Normal file
489
public/tinymce/src/plugins/paste/main/ts/core/WordFilter.ts
Normal 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 "· " 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
|
||||
[/ /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
|
||||
};
|
||||
44
public/tinymce/src/plugins/paste/main/ts/ui/Buttons.ts
Normal file
44
public/tinymce/src/plugins/paste/main/ts/ui/Buttons.ts
Normal 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
|
||||
};
|
||||
19
public/tinymce/src/plugins/paste/test/.eslintrc
Normal file
19
public/tinymce/src/plugins/paste/test/.eslintrc
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": false,
|
||||
"amd": true
|
||||
},
|
||||
|
||||
"globals": {
|
||||
"assert": true,
|
||||
"test": true,
|
||||
"asynctest": true
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"eqeqeq": "error",
|
||||
"yoda": "error"
|
||||
},
|
||||
|
||||
"extends": "../../../../../.eslintrc"
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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: "a b";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: "a b"; 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: "a b";">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: "a b";">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);
|
||||
});
|
||||
879
public/tinymce/src/plugins/paste/test/ts/browser/PasteTest.ts
Normal file
879
public/tinymce/src/plugins/paste/test/ts/browser/PasteTest.ts
Normal 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> <\/p>\n?/, '').replace(/\n?<p> <\/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 </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 "Times New Roman""> </span></span></span><span lang="DE" style="font-family:Arial;mso-fareast-font-family:Arial;mso-bidi-font-family:Arial;color:black">Item Spaces.<o:p></o:p></span></p>' });
|
||||
LegacyUnit.equal(editor.getContent(), '<ul><li>Item 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 "Times New Roman""> </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"\'> </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"\'> </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"\'> </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"\'> </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"\'> </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"\'> </span></span></span><![endif]>Y</p>'
|
||||
});
|
||||
|
||||
delete editor.settings.paste_convert_word_fake_lists;
|
||||
|
||||
LegacyUnit.equal(editor.getContent(), '<p>ABC. X</p><p>1. 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> </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"\'> ' +
|
||||
'</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"\'> </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"\'> </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"\'> </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"\'> </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"\'> ' +
|
||||
' ' +
|
||||
' ' +
|
||||
' </span>i.<span style=\'font:7.0pt "Times New Roman"\'>' +
|
||||
' </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> </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< & >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 /><b>b</b></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: '<span style="color:red"><span data-mce-style="color:red">' });
|
||||
LegacyUnit.equal(editor.getContent(), '<p><span style="color:red"><span data-mce-style="color:red"></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);
|
||||
});
|
||||
@@ -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> </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"> </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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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)));
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user