/**
* @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @ignore
* File overview: Clipboard support.
*/
//
// COPY & PASTE EXECUTION FLOWS:
// -- CTRL+C
// * if ( isCustomCopyCutSupported )
// * dataTransfer.setData( 'text/html', getSelectedHtml )
// * else
// * browser's default behavior
// -- CTRL+X
// * listen onKey (onkeydown)
// * fire 'saveSnapshot' on editor
// * if ( isCustomCopyCutSupported )
// * dataTransfer.setData( 'text/html', getSelectedHtml )
// * extractSelectedHtml // remove selected contents
// * else
// * browser's default behavior
// * deferred second 'saveSnapshot' event
// -- CTRL+V
// * listen onKey (onkeydown)
// * simulate 'beforepaste' for non-IEs on editable
// * listen 'onpaste' on editable ('onbeforepaste' for IE)
// * fire 'beforePaste' on editor
// * if ( !canceled && ( htmlInDataTransfer || !external paste) && dataTransfer is not empty ) getClipboardDataByPastebin
// * fire 'paste' on editor
// * !canceled && fire 'afterPaste' on editor
// -- Copy command
// * tryToCutCopy
// * execCommand
// * !success && notification
// -- Cut command
// * fixCut
// * tryToCutCopy
// * execCommand
// * !success && notification
// -- Paste command
// * fire 'paste' on editable ('beforepaste' for IE)
// * !canceled && execCommand 'paste'
// * !success && fire 'pasteDialog' on editor
// -- Paste from native context menu & menubar
// (Fx & Webkits are handled in 'paste' default listner.
// Opera cannot be handled at all because it doesn't fire any events
// Special treatment is needed for IE, for which is this part of doc)
// * listen 'onpaste'
// * cancel native event
// * fire 'beforePaste' on editor
// * if ( !canceled && ( htmlInDataTransfer || !external paste) && dataTransfer is not empty ) getClipboardDataByPastebin
// * execIECommand( 'paste' ) -> this fires another 'paste' event, so cancel it
// * fire 'paste' on editor
// * !canceled && fire 'afterPaste' on editor
//
//
// PASTE EVENT - PREPROCESSING:
// -- Possible dataValue types: auto, text, html.
// -- Possible dataValue contents:
// * text (possible \n\r)
// * htmlified text (text + br,div,p - no presentional markup & attrs - depends on browser)
// * html
// -- Possible flags:
// * htmlified - if true then content is a HTML even if no markup inside. This flag is set
// for content from editable pastebins, because they 'htmlify' pasted content.
//
// -- Type: auto:
// * content: htmlified text -> filter, unify text markup (brs, ps, divs), set type: text
// * content: html -> filter, set type: html
// -- Type: text:
// * content: htmlified text -> filter, unify text markup
// * content: html -> filter, strip presentional markup, unify text markup
// -- Type: html:
// * content: htmlified text -> filter, unify text markup
// * content: html -> filter
//
// -- Phases:
// * if dataValue is empty copy data from dataTransfer to dataValue (priority 1)
// * filtering (priorities 3-5) - e.g. pastefromword filters
// * content type sniffing (priority 6)
// * markup transformations for text (priority 6)
//
// DRAG & DROP EXECUTION FLOWS:
// -- Drag
// * save to the global object:
// * drag timestamp (with 'cke-' prefix),
// * selected html,
// * drag range,
// * editor instance.
// * put drag timestamp into event.dataTransfer.text
// -- Drop
// * if events text == saved timestamp && editor == saved editor
// internal drag & drop occurred
// * getRangeAtDropPosition
// * create bookmarks for drag and drop ranges starting from the end of the document
// * dragRange.deleteContents()
// * fire 'paste' with saved html and drop range
// * if events text == saved timestamp && editor != saved editor
// cross editor drag & drop occurred
// * getRangeAtDropPosition
// * fire 'paste' with saved html
// * dragRange.deleteContents()
// * FF: refreshCursor on afterPaste
// * if events text != saved timestamp
// drop form external source occurred
// * getRangeAtDropPosition
// * if event contains html data then fire 'paste' with html
// * else if event contains text data then fire 'paste' with encoded text
// * FF: refreshCursor on afterPaste
'use strict';
( function() {
// Register the plugin.
CKEDITOR.plugins.add( 'clipboard', {
requires: 'dialog',
// jscs:disable maximumLineLength
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
// jscs:enable maximumLineLength
icons: 'copy,copy-rtl,cut,cut-rtl,paste,paste-rtl', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
var filterType,
filtersFactory = filtersFactoryFactory();
if ( editor.config.forcePasteAsPlainText ) {
filterType = 'plain-text';
} else if ( editor.config.pasteFilter ) {
filterType = editor.config.pasteFilter;
}
// On Webkit the pasteFilter defaults 'semantic-content' because pasted data is so terrible
// that it must be always filtered.
else if ( CKEDITOR.env.webkit && !( 'pasteFilter' in editor.config ) ) {
filterType = 'semantic-content';
}
editor.pasteFilter = filtersFactory.get( filterType );
initPasteClipboard( editor );
initDragDrop( editor );
CKEDITOR.dialog.add( 'paste', CKEDITOR.getUrl( this.path + 'dialogs/paste.js' ) );
editor.on( 'paste', function( evt ) {
// Init `dataTransfer` if `paste` event was fired without it, so it will be always available.
if ( !evt.data.dataTransfer ) {
evt.data.dataTransfer = new CKEDITOR.plugins.clipboard.dataTransfer();
}
// If dataValue is already set (manually or by paste bin), so do not override it.
if ( evt.data.dataValue ) {
return;
}
var dataTransfer = evt.data.dataTransfer,
// IE support only text data and throws exception if we try to get html data.
// This html data object may also be empty if we drag content of the textarea.
value = dataTransfer.getData( 'text/html' );
if ( value ) {
evt.data.dataValue = value;
evt.data.type = 'html';
} else {
// Try to get text data otherwise.
value = dataTransfer.getData( 'text/plain' );
if ( value ) {
evt.data.dataValue = editor.editable().transformPlainTextToHtml( value );
evt.data.type = 'text';
}
}
}, null, null, 1 );
editor.on( 'paste', function( evt ) {
var data = evt.data.dataValue,
blockElements = CKEDITOR.dtd.$block;
// Filter webkit garbage.
if ( data.indexOf( 'Apple-' ) > -1 ) {
// Replace special webkit's with simple space, because webkit
// produces them even for normal spaces.
data = data.replace( / <\/span>/gi, ' ' );
// Strip around white-spaces when not in forced 'html' content type.
// This spans are created only when pasting plain text into Webkit,
// but for safety reasons remove them always.
if ( evt.data.type != 'html' ) {
data = data.replace( /]*>([^<]*)<\/span>/gi, function( all, spaces ) {
// Replace tabs with 4 spaces like Fx does.
return spaces.replace( /\t/g, ' ' );
} );
}
// This br is produced only when copying & pasting HTML content.
if ( data.indexOf( ' -> (br.cke-pasted-remove will be removed later)
data = data.replace( /^ (?: |\r\n)?<(\w+)/g, function( match, elementName ) {
if ( elementName.toLowerCase() in blockElements ) {
evt.data.preSniffing = 'html'; // Mark as not a text.
return '<' + elementName;
}
return match;
} );
} else if ( CKEDITOR.env.webkit ) {
//
' ) > -1 ) {
evt.data.startsWithEOL = 1;
evt.data.preSniffing = 'html'; // Mark as not text.
data = data.replace( /
/, '' );
}
// Remove all other classes.
data = data.replace( /(<[^>]+) class="Apple-[^"]*"/gi, '$1' );
}
// Strip editable that was copied from inside. (#9534)
if ( data.match( /^<[^<]+cke_(editable|contents)/i ) ) {
var tmp,
editable_wrapper,
wrapper = new CKEDITOR.dom.element( 'div' );
wrapper.setHtml( data );
// Verify for sure and check for nested editor UI parts. (#9675)
while ( wrapper.getChildCount() == 1 &&
( tmp = wrapper.getFirst() ) &&
tmp.type == CKEDITOR.NODE_ELEMENT && // Make sure first-child is element.
( tmp.hasClass( 'cke_editable' ) || tmp.hasClass( 'cke_contents' ) ) ) {
wrapper = editable_wrapper = tmp;
}
// If editable wrapper was found strip it and bogus
(added on FF).
if ( editable_wrapper )
data = editable_wrapper.getHtml().replace( /
$/i, '' );
}
if ( CKEDITOR.env.ie ) {
//
- paragraphs can be separated by new \r\n ).
if ( !data.match( /^([^<]|
)*$/gi ) && !data.match( /^(
([^<]|
)*<\/p>|(\r\n))*$/gi ) )
return 'html';
} else if ( CKEDITOR.env.gecko ) {
// Text or
.
if ( !data.match( /^([^<]|
)*$/gi ) )
return 'html';
} else {
return 'html';
}
return 'htmlifiedtext';
}
// This function transforms what browsers produce when
// pasting plain text into editable element (see clipboard/paste.html TCs
// for more info) into correct HTML (similar to that produced by text2Html).
function htmlifiedTextHtmlification( config, data ) {
function repeatParagraphs( repeats ) {
// Repeat blocks floor((n+1)/2) times.
// Even number of repeats - add
at the beginning of last
. return CKEDITOR.tools.repeat( '
', ~~( repeats / 2 ) ) + ( repeats % 2 == 1 ? '
' : '' );
}
// Replace adjacent white-spaces (EOLs too - Fx sometimes keeps them) with one space.
data = data.replace( /\s+/g, ' ' )
// Remove spaces from between tags.
.replace( /> +<' )
// Normalize XHTML syntax and upper cased
tags.
.replace( /
/gi, '
' );
// IE - lower cased tags.
data = data.replace( /<\/?[A-Z]+>/g, function( match ) {
return match.toLowerCase();
} );
// Don't touch single lines (no
) - nothing to do here.
if ( data.match( /^[^<]$/ ) )
return data;
// Webkit.
if ( CKEDITOR.env.webkit && data.indexOf( '
' + data.replace( /(
' + data.replace( /(
){2,}/g, function( match ) {
return repeatParagraphs( match.length / 4 );
} ) + '
element completely, because it's a basic structural element, // so it tries to replace it with an element created based on the active enter mode, eventually doing nothing. // // Now you can sleep well. return filters.plainText || ( filters.plainText = new CKEDITOR.filter( 'br' ) ); } else if ( type == 'semantic-content' ) { return filters.semanticContent || ( filters.semanticContent = createSemanticContentFilter() ); } else if ( type ) { // Create filter based on rules (string or object). return new CKEDITOR.filter( type ); } return null; } }; } function filterContent( editor, data, filter ) { var fragment = CKEDITOR.htmlParser.fragment.fromHtml( data ), writer = new CKEDITOR.htmlParser.basicWriter(); filter.applyTo( fragment, true, false, editor.activeEnterMode ); fragment.writeHtml( writer ); return writer.getHtml(); } function switchEnterMode( config, data ) { if ( config.enterMode == CKEDITOR.ENTER_BR ) { data = data.replace( /(<\/p>
)+/g, function( match ) {
return CKEDITOR.tools.repeat( '
', match.length / 7 * 2 );
} ).replace( /<\/?p>/g, '' );
} else if ( config.enterMode == CKEDITOR.ENTER_DIV ) {
data = data.replace( /<(\/)?p>/g, '<$1div>' );
}
return data;
}
function preventDefaultSetDropEffectToNone( evt ) {
evt.data.preventDefault();
evt.data.$.dataTransfer.dropEffect = 'none';
}
function initDragDrop( editor ) {
var clipboard = CKEDITOR.plugins.clipboard;
editor.on( 'contentDom', function() {
var editable = editor.editable(),
dropTarget = CKEDITOR.plugins.clipboard.getDropTarget( editor ),
top = editor.ui.space( 'top' ),
bottom = editor.ui.space( 'bottom' );
// -------------- DRAGOVER TOP & BOTTOM --------------
// Not allowing dragging on toolbar and bottom (#12613).
clipboard.preventDefaultDropOnElement( top );
clipboard.preventDefaultDropOnElement( bottom );
// -------------- DRAGSTART --------------
// Listed on dragstart to mark internal and cross-editor drag & drop
// and save range and selected HTML.
editable.attachListener( dropTarget, 'dragstart', fireDragEvent );
// Make sure to reset data transfer (in case dragend was not called or was canceled).
editable.attachListener( editor, 'dragstart', clipboard.resetDragDataTransfer, clipboard, null, 1 );
// Create a dataTransfer object and save it globally.
editable.attachListener( editor, 'dragstart', function( evt ) {
clipboard.initDragDataTransfer( evt, editor );
// Save drag range globally for cross editor D&D.
var dragRange = clipboard.dragRange = editor.getSelection().getRanges()[ 0 ];
// Store number of children, so we can later tell if any text node was split on drop. (#13011, #13447)
if ( CKEDITOR.env.ie && CKEDITOR.env.version < 10 ) {
clipboard.dragStartContainerChildCount = dragRange ? getContainerChildCount( dragRange.startContainer ) : null;
clipboard.dragEndContainerChildCount = dragRange ? getContainerChildCount( dragRange.endContainer ) : null;
}
}, null, null, 2 );
// -------------- DRAGEND --------------
// Clean up on dragend.
editable.attachListener( dropTarget, 'dragend', fireDragEvent );
// Init data transfer if someone wants to use it in dragend.
editable.attachListener( editor, 'dragend', clipboard.initDragDataTransfer, clipboard, null, 1 );
// When drag & drop is done we need to reset dataTransfer so the future
// external drop will be not recognize as internal.
editable.attachListener( editor, 'dragend', clipboard.resetDragDataTransfer, clipboard, null, 100 );
// -------------- DRAGOVER --------------
// We need to call preventDefault on dragover because otherwise if
// we drop image it will overwrite document.
editable.attachListener( dropTarget, 'dragover', function( evt ) {
var target = evt.data.getTarget();
// Prevent reloading page when dragging image on empty document (#12619).
if ( target && target.is && target.is( 'html' ) ) {
evt.data.preventDefault();
return;
}
// If we do not prevent default dragover on IE the file path
// will be loaded and we will lose content. On the other hand
// if we prevent it the cursor will not we shown, so we prevent
// dragover only on IE, on versions which support file API and only
// if the event contains files.
if ( CKEDITOR.env.ie &&
CKEDITOR.plugins.clipboard.isFileApiSupported &&
evt.data.$.dataTransfer.types.contains( 'Files' ) ) {
evt.data.preventDefault();
}
} );
// -------------- DROP --------------
editable.attachListener( dropTarget, 'drop', function( evt ) {
// Cancel native drop.
evt.data.preventDefault();
var target = evt.data.getTarget(),
readOnly = target.isReadOnly();
// Do nothing if drop on non editable element (#13015).
// The tag isn't editable (body is), but we want to allow drop on it
// (so it is possible to drop below editor contents).
if ( readOnly && !( target.type == CKEDITOR.NODE_ELEMENT && target.is( 'html' ) ) ) {
return;
}
// Getting drop position is one of the most complex parts.
var dropRange = clipboard.getRangeAtDropPosition( evt, editor ),
dragRange = clipboard.dragRange;
// Do nothing if it was not possible to get drop range.
if ( !dropRange ) {
return;
}
// Fire drop.
fireDragEvent( evt, dragRange, dropRange );
} );
// Create dataTransfer or get it, if it was created before.
editable.attachListener( editor, 'drop', clipboard.initDragDataTransfer, clipboard, null, 1 );
// Execute drop action, fire paste.
editable.attachListener( editor, 'drop', function( evt ) {
var data = evt.data;
if ( !data ) {
return;
}
// Let user modify drag and drop range.
var dropRange = data.dropRange,
dragRange = data.dragRange,
dataTransfer = data.dataTransfer;
if ( dataTransfer.getTransferType( editor ) == CKEDITOR.DATA_TRANSFER_INTERNAL ) {
// Execute drop with a timeout because otherwise selection, after drop,
// on IE is in the drag position, instead of drop position.
setTimeout( function() {
clipboard.internalDrop( dragRange, dropRange, dataTransfer, editor );
}, 0 );
} else if ( dataTransfer.getTransferType( editor ) == CKEDITOR.DATA_TRANSFER_CROSS_EDITORS ) {
crossEditorDrop( dragRange, dropRange, dataTransfer );
} else {
externalDrop( dropRange, dataTransfer );
}
}, null, null, 9999 );
// Cross editor drag and drop (drag in one Editor and drop in the other).
function crossEditorDrop( dragRange, dropRange, dataTransfer ) {
// Paste event should be fired before delete contents because otherwise
// Chrome have a problem with drop range (Chrome split the drop
// range container so the offset is bigger then container length).
dropRange.select();
firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop' }, 1 );
// Remove dragged content and make a snapshot.
dataTransfer.sourceEditor.fire( 'saveSnapshot' );
dataTransfer.sourceEditor.editable().extractHtmlFromRange( dragRange );
// Make some selection before saving snapshot, otherwise error will be thrown, because
// there will be no valid selection after content is removed.
dataTransfer.sourceEditor.getSelection().selectRanges( [ dragRange ] );
dataTransfer.sourceEditor.fire( 'saveSnapshot' );
}
// Drop from external source.
function externalDrop( dropRange, dataTransfer ) {
// Paste content into the drop position.
dropRange.select();
firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop' }, 1 );
// Usually we reset DataTranfer on dragend,
// but dragend is called on the same element as dragstart
// so it will not be called on on external drop.
clipboard.resetDragDataTransfer();
}
// Fire drag/drop events (dragstart, dragend, drop).
function fireDragEvent( evt, dragRange, dropRange ) {
var eventData = {
$: evt.data.$,
target: evt.data.getTarget()
};
if ( dragRange ) {
eventData.dragRange = dragRange;
}
if ( dropRange ) {
eventData.dropRange = dropRange;
}
if ( editor.fire( evt.name, eventData ) === false ) {
evt.data.preventDefault();
}
}
function getContainerChildCount( container ) {
if ( container.type != CKEDITOR.NODE_ELEMENT ) {
container = container.getParent();
}
return container.getChildCount();
}
} );
}
/**
* @singleton
* @class CKEDITOR.plugins.clipboard
*/
CKEDITOR.plugins.clipboard = {
/**
* True if the environment allows to set data on copy or cut manually. This value is false in IE, because this browser
* shows the security dialog window when the script tries to set clipboard data and on iOS, because custom data is
* not saved to clipboard there.
*
* @since 4.5
* @readonly
* @property {Boolean}
*/
isCustomCopyCutSupported: !CKEDITOR.env.ie && !CKEDITOR.env.iOS,
/**
* True if the environment supports MIME types and custom data types in dataTransfer/cliboardData getData/setData methods.
*
* @since 4.5
* @readonly
* @property {Boolean}
*/
isCustomDataTypesSupported: !CKEDITOR.env.ie,
/**
* True if the environment supports File API.
*
* @since 4.5
* @readonly
* @property {Boolean}
*/
isFileApiSupported: !CKEDITOR.env.ie || CKEDITOR.env.version > 9,
/**
* Main native paste event editable should listen to.
*
* **Note:** Safari does not like the {@link CKEDITOR.editor#beforePaste} event — it sometimes does not
* handle Ctrl+C properly. This is probably caused by some race condition between events.
* Chrome, Firefox and Edge work well with both events, so it is better to use {@link CKEDITOR.editor#paste}
* which will handle pasting from e.g. browsers' menu bars.
* IE7/8 does not like the {@link CKEDITOR.editor#paste} event for which it is throwing random errors.
*
* @since 4.5
* @readonly
* @property {String}
*/
mainPasteEvent: ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) ? 'beforepaste' : 'paste',
/**
* Returns `true` if it is expected that a browser provides HTML data through the Clipboard API.
* If not, this method returns `false` and as a result CKEditor will use the paste bin. Read more in
* the [Clipboard Integration](http://docs.ckeditor.com/#!/guide/dev_clipboard-section-clipboard-api) guide.
*
* @since 4.5.2
* @returns {Boolean}
*/
canClipboardApiBeTrusted: function( dataTransfer, editor ) {
// If it's an internal or cross-editor data transfer, then it means that custom cut/copy/paste support works
// and that the data were put manually on the data transfer so we can be sure that it's available.
if ( dataTransfer.getTransferType( editor ) != CKEDITOR.DATA_TRANSFER_EXTERNAL ) {
return true;
}
// In Chrome we can trust Clipboard API, with the exception of Chrome on Android (in both - mobile and desktop modes), where
// clipboard API is not available so we need to check it (#13187).
if ( CKEDITOR.env.chrome && !dataTransfer.isEmpty() ) {
return true;
}
// Because of a Firefox bug HTML data are not available in some cases (e.g. paste from Word), in such cases we
// need to use the pastebin (#13528, https://bugzilla.mozilla.org/show_bug.cgi?id=1183686).
if ( CKEDITOR.env.gecko && ( dataTransfer.getData( 'text/html' ) || dataTransfer.getFilesCount() ) ) {
return true;
}
// In Safari and IE HTML data is not available though the Clipboard API.
// In Edge things are a bit messy at the moment -
// https://connect.microsoft.com/IE/feedback/details/1572456/edge-clipboard-api-text-html-content-messed-up-in-event-clipboarddata
// It is safer to use the paste bin in unknown cases.
return false;
},
/**
* Returns the element that should be used as the target for the drop event.
*
* @since 4.5
* @param {CKEDITOR.editor} editor The editor instance.
* @returns {CKEDITOR.dom.domObject} the element that should be used as the target for the drop event.
*/
getDropTarget: function( editor ) {
var editable = editor.editable();
// #11123 Firefox needs to listen on document, because otherwise event won't be fired.
// #11086 IE8 cannot listen on document.
if ( ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) || editable.isInline() ) {
return editable;
} else {
return editor.document;
}
},
/**
* IE 8 & 9 split text node on drop so the first node contains the
* text before the drop position and the second contains the rest. If you
* drag the content from the same node you will be not be able to get
* it (the range becomes invalid), so you need to join them back.
*
* Note that the first node in IE 8 & 9 is the original node object
* but with shortened content.
*
* Before:
* --- Text Node A ----------------------------------
* /\
* Drag position
*
* After (IE 8 & 9):
* --- Text Node A ----- --- Text Node B -----------
* /\ /\
* Drop position Drag position
* (invalid)
*
* After (other browsers):
* --- Text Node A ----------------------------------
* /\ /\
* Drop position Drag position
*
* **Note:** This function is in the public scope for tests usage only.
*
* @since 4.5
* @private
* @param {CKEDITOR.dom.range} dragRange The drag range.
* @param {CKEDITOR.dom.range} dropRange The drop range.
* @param {Number} preDragStartContainerChildCount The number of children of the drag range start container before the drop.
* @param {Number} preDragEndContainerChildCount The number of children of the drag range end container before the drop.
*/
fixSplitNodesAfterDrop: function( dragRange, dropRange, preDragStartContainerChildCount, preDragEndContainerChildCount ) {
var dropContainer = dropRange.startContainer;
if (
typeof preDragEndContainerChildCount != 'number' ||
typeof preDragStartContainerChildCount != 'number'
) {
return;
}
// We are only concerned about ranges anchored in elements.
if ( dropContainer.type != CKEDITOR.NODE_ELEMENT ) {
return;
}
if ( handleContainer( dragRange.startContainer, dropContainer, preDragStartContainerChildCount ) ) {
return;
}
if ( handleContainer( dragRange.endContainer, dropContainer, preDragEndContainerChildCount ) ) {
return;
}
function handleContainer( dragContainer, dropContainer, preChildCount ) {
var dragElement = dragContainer;
if ( dragElement.type == CKEDITOR.NODE_TEXT ) {
dragElement = dragContainer.getParent();
}
if ( dragElement.equals( dropContainer ) && preChildCount != dropContainer.getChildCount() ) {
applyFix( dropRange );
return true;
}
}
function applyFix( dropRange ) {
var nodeBefore = dropRange.startContainer.getChild( dropRange.startOffset - 1 ),
nodeAfter = dropRange.startContainer.getChild( dropRange.startOffset );
if (
nodeBefore && nodeBefore.type == CKEDITOR.NODE_TEXT &&
nodeAfter && nodeAfter.type == CKEDITOR.NODE_TEXT
) {
var offset = nodeBefore.getLength();
nodeBefore.setText( nodeBefore.getText() + nodeAfter.getText() );
nodeAfter.remove();
dropRange.setStart( nodeBefore, offset );
dropRange.collapse( true );
}
}
},
/**
* Checks whether turning the drag range into bookmarks will invalidate the drop range.
* This usually happens when the drop range shares the container with the drag range and is
* located after the drag range, but there are countless edge cases.
*
* This function is stricly related to {@link #internalDrop} which toggles
* order in which it creates bookmarks for both ranges based on a value returned
* by this method. In some cases this method returns a value which is not necessarily
* true in terms of what it was meant to check, but it is convenient, because
* we know how it is interpreted in {@link #internalDrop}, so the correct
* behavior of the entire algorithm is assured.
*
* **Note:** This function is in the public scope for tests usage only.
*
* @since 4.5
* @private
* @param {CKEDITOR.dom.range} dragRange The first range to compare.
* @param {CKEDITOR.dom.range} dropRange The second range to compare.
* @returns {Boolean} `true` if the first range is before the second range.
*/
isDropRangeAffectedByDragRange: function( dragRange, dropRange ) {
var dropContainer = dropRange.startContainer,
dropOffset = dropRange.endOffset;
// Both containers are the same and drop offset is at the same position or later.
// " A L] A " " M A "
// ^ ^
if ( dragRange.endContainer.equals( dropContainer ) && dragRange.endOffset <= dropOffset ) {
return true;
}
// Bookmark for drag start container will mess up with offsets.
// " O [L A " " M A "
// ^ ^
if (
dragRange.startContainer.getParent().equals( dropContainer ) &&
dragRange.startContainer.getIndex() < dropOffset
) {
return true;
}
// Bookmark for drag end container will mess up with offsets.
// " O] L A " " M A "
// ^ ^
if (
dragRange.endContainer.getParent().equals( dropContainer ) &&
dragRange.endContainer.getIndex() < dropOffset
) {
return true;
}
return false;
},
/**
* Internal drag and drop (drag and drop in the same editor instance).
*
* **Note:** This function is in the public scope for tests usage only.
*
* @since 4.5
* @private
* @param {CKEDITOR.dom.range} dragRange The first range to compare.
* @param {CKEDITOR.dom.range} dropRange The second range to compare.
* @param {CKEDITOR.plugins.clipboard.dataTransfer} dataTransfer
* @param {CKEDITOR.editor} editor
*/
internalDrop: function( dragRange, dropRange, dataTransfer, editor ) {
var clipboard = CKEDITOR.plugins.clipboard,
editable = editor.editable(),
dragBookmark, dropBookmark, isDropRangeAffected;
// Save and lock snapshot so there will be only
// one snapshot for both remove and insert content.
editor.fire( 'saveSnapshot' );
editor.fire( 'lockSnapshot', { dontUpdate: 1 } );
if ( CKEDITOR.env.ie && CKEDITOR.env.version < 10 ) {
this.fixSplitNodesAfterDrop(
dragRange,
dropRange,
clipboard.dragStartContainerChildCount,
clipboard.dragEndContainerChildCount
);
}
// Because we manipulate multiple ranges we need to do it carefully,
// changing one range (event creating a bookmark) may make other invalid.
// We need to change ranges into bookmarks so we can manipulate them easily in the future.
// We can change the range which is later in the text before we change the preceding range.
// We call isDropRangeAffectedByDragRange to test the order of ranges.
isDropRangeAffected = this.isDropRangeAffectedByDragRange( dragRange, dropRange );
if ( !isDropRangeAffected ) {
dragBookmark = dragRange.createBookmark( false );
}
dropBookmark = dropRange.clone().createBookmark( false );
if ( isDropRangeAffected ) {
dragBookmark = dragRange.createBookmark( false );
}
// Check if drop range is inside range.
// This is an edge case when we drop something on editable's margin/padding.
// That space is not treated as a part of the range we drag, so it is possible to drop there.
// When we drop, browser tries to find closest drop position and it finds it inside drag range. (#13453)
var startNode = dragBookmark.startNode,
endNode = dragBookmark.endNode,
dropNode = dropBookmark.startNode,
dropInsideDragRange =
// Must check endNode because dragRange could be collapsed in some edge cases (simulated DnD).
endNode &&
startNode.getPosition( dropNode ) == CKEDITOR.POSITION_PRECEDING &&
endNode.getPosition( dropNode ) == CKEDITOR.POSITION_FOLLOWING;
if ( dropInsideDragRange ) {
// When we normally drag and drop, the selection is changed to dropRange,
// so here we simulate the same behavior.
editor.getSelection().selectRanges( [ dropRange ] );
// Remove bookmark spans.
startNode.remove();
endNode.remove();
dropNode.remove();
}
else {
// Drop range is outside drag range.
// No we can safely delete content for the drag range...
dragRange = editor.createRange();
dragRange.moveToBookmark( dragBookmark );
editable.extractHtmlFromRange( dragRange, 1 );
// ...and paste content into the drop position.
dropRange = editor.createRange();
dropRange.moveToBookmark( dropBookmark );
// We do not select drop range, because of may be in the place we can not set the selection
// (e.g. between blocks, in case of block widget D&D). We put range to the paste event instead.
firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop', range: dropRange }, 1 );
}
editor.fire( 'unlockSnapshot' );
},
/**
* Gets the range from the `drop` event.
*
* @since 4.5
* @param {Object} domEvent A native DOM drop event object.
* @param {CKEDITOR.editor} editor The source editor instance.
* @returns {CKEDITOR.dom.range} range at drop position.
*/
getRangeAtDropPosition: function( dropEvt, editor ) {
var $evt = dropEvt.data.$,
x = $evt.clientX,
y = $evt.clientY,
$range,
defaultRange = editor.getSelection( true ).getRanges()[ 0 ],
range = editor.createRange();
// Make testing possible.
if ( dropEvt.data.testRange )
return dropEvt.data.testRange;
// Webkits.
if ( document.caretRangeFromPoint ) {
$range = editor.document.$.caretRangeFromPoint( x, y );
range.setStart( CKEDITOR.dom.node( $range.startContainer ), $range.startOffset );
range.collapse( true );
}
// FF.
else if ( $evt.rangeParent ) {
range.setStart( CKEDITOR.dom.node( $evt.rangeParent ), $evt.rangeOffset );
range.collapse( true );
}
// IEs 9+.
// We check if editable is focused to make sure that it's an internal DnD. External DnD must use the second
// mechanism because of http://dev.ckeditor.com/ticket/13472#comment:6.
else if ( CKEDITOR.env.ie && CKEDITOR.env.version > 8 && defaultRange && editor.editable().hasFocus ) {
// On IE 9+ range by default is where we expected it.
// defaultRange may be undefined if dragover was canceled (file drop).
return defaultRange;
}
// IE 8 and all IEs if !defaultRange or external DnD.
else if ( document.body.createTextRange ) {
// To use this method we need a focus (which may be somewhere else in case of external drop).
editor.focus();
$range = editor.document.getBody().$.createTextRange();
try {
var sucess = false;
// If user drop between text line IEs moveToPoint throws exception:
//
// Lorem ipsum pulvinar purus et euismod
//
// dolor sit amet,| consectetur adipiscing
// *
// vestibulum tincidunt augue eget tempus.
//
// * - drop position
// | - expected cursor position
//
// So we try to call moveToPoint with +-1px up to +-20px above or
// below original drop position to find nearest good drop position.
for ( var i = 0; i < 20 && !sucess; i++ ) {
if ( !sucess ) {
try {
$range.moveToPoint( x, y - i );
sucess = true;
} catch ( err ) {
}
}
if ( !sucess ) {
try {
$range.moveToPoint( x, y + i );
sucess = true;
} catch ( err ) {
}
}
}
if ( sucess ) {
var id = 'cke-temp-' + ( new Date() ).getTime();
$range.pasteHTML( '\u200b' );
var span = editor.document.getById( id );
range.moveToPosition( span, CKEDITOR.POSITION_BEFORE_START );
span.remove();
} else {
// If the fist method does not succeed we might be next to
// the short element (like header):
//
// Lorem ipsum pulvinar purus et euismod.
//
//
// SOME HEADER| *
//
//
// vestibulum tincidunt augue eget tempus.
//
// * - drop position
// | - expected cursor position
//
// In such situation elementFromPoint returns proper element. Using getClientRect
// it is possible to check if the cursor should be at the beginning or at the end
// of paragraph.
var $element = editor.document.$.elementFromPoint( x, y ),
element = new CKEDITOR.dom.element( $element ),
rect;
if ( !element.equals( editor.editable() ) && element.getName() != 'html' ) {
rect = element.getClientRect();
if ( x < rect.left ) {
range.setStartAt( element, CKEDITOR.POSITION_AFTER_START );
range.collapse( true );
} else {
range.setStartAt( element, CKEDITOR.POSITION_BEFORE_END );
range.collapse( true );
}
}
// If drop happens on no element elementFromPoint returns html or body.
//
// * |Lorem ipsum pulvinar purus et euismod.
//
// vestibulum tincidunt augue eget tempus.
//
// * - drop position
// | - expected cursor position
//
// In such case we can try to use default selection. If startContainer is not
// 'editable' element it is probably proper selection.
else if ( defaultRange && defaultRange.startContainer &&
!defaultRange.startContainer.equals( editor.editable() ) ) {
return defaultRange;
// Otherwise we can not find any drop position and we have to return null
// and cancel drop event.
} else {
return null;
}
}
} catch ( err ) {
return null;
}
} else {
return null;
}
return range;
},
/**
* This function tries to link the `evt.data.dataTransfer` property of the {@link CKEDITOR.editor#dragstart},
* {@link CKEDITOR.editor#dragend} and {@link CKEDITOR.editor#drop} events to a single
* {@link CKEDITOR.plugins.clipboard.dataTransfer} object.
*
* This method is automatically used by the core of the drag and drop functionality and
* usually does not have to be called manually when using the drag and drop events.
*
* This method behaves differently depending on whether the drag and drop events were fired
* artificially (to represent a non-native drag and drop) or whether they were caused by the native drag and drop.
*
* If the native event is not available, then it will create a new {@link CKEDITOR.plugins.clipboard.dataTransfer}
* instance (if it does not exist already) and will link it to this and all following event objects until
* the {@link #resetDragDataTransfer} method is called. It means that all three drag and drop events must be fired
* in order to ensure that the data transfer is bound correctly.
*
* If the native event is available, then the {@link CKEDITOR.plugins.clipboard.dataTransfer} is identified
* by its ID and a new instance is assigned to the `evt.data.dataTransfer` only if the ID changed or
* the {@link #resetDragDataTransfer} method was called.
*
* @since 4.5
* @param {CKEDITOR.dom.event} [evt] A drop event object.
* @param {CKEDITOR.editor} [sourceEditor] The source editor instance.
*/
initDragDataTransfer: function( evt, sourceEditor ) {
// Create a new dataTransfer object based on the drop event.
// If this event was used on dragstart to create dataTransfer
// both dataTransfer objects will have the same id.
var nativeDataTransfer = evt.data.$ ? evt.data.$.dataTransfer : null,
dataTransfer = new this.dataTransfer( nativeDataTransfer, sourceEditor );
if ( !nativeDataTransfer ) {
// No native event.
if ( this.dragData ) {
dataTransfer = this.dragData;
} else {
this.dragData = dataTransfer;
}
} else {
// Native event. If there is the same id we will replace dataTransfer with the one
// created on drag, because it contains drag editor, drag content and so on.
// Otherwise (in case of drag from external source) we save new object to
// the global clipboard.dragData.
if ( this.dragData && dataTransfer.id == this.dragData.id ) {
dataTransfer = this.dragData;
} else {
this.dragData = dataTransfer;
}
}
evt.data.dataTransfer = dataTransfer;
},
/**
* Removes the global {@link #dragData} so the next call to {@link #initDragDataTransfer}
* always creates a new instance of {@link CKEDITOR.plugins.clipboard.dataTransfer}.
*
* @since 4.5
*/
resetDragDataTransfer: function() {
this.dragData = null;
},
/**
* Global object storing the data transfer of the current drag and drop operation.
* Do not use it directly, use {@link #initDragDataTransfer} and {@link #resetDragDataTransfer}.
*
* Note: This object is global (meaning that it is not related to a single editor instance)
* in order to handle drag and drop from one editor into another.
*
* @since 4.5
* @private
* @property {CKEDITOR.plugins.clipboard.dataTransfer} dragData
*/
/**
* Range object to save the drag range and remove its content after the drop.
*
* @since 4.5
* @private
* @property {CKEDITOR.dom.range} dragRange
*/
/**
* Initializes and links data transfer objects based on the paste event. If the data
* transfer object was already initialized on this event, the function will
* return that object. In IE it is not possible to link copy/cut and paste events
* so the method always returns a new object. The same happens if there is no paste event
* passed to the method.
*
* @since 4.5
* @param {CKEDITOR.dom.event} [evt] A paste event object.
* @param {CKEDITOR.editor} [sourceEditor] The source editor instance.
* @returns {CKEDITOR.plugins.clipboard.dataTransfer} The data transfer object.
*/
initPasteDataTransfer: function( evt, sourceEditor ) {
if ( !this.isCustomCopyCutSupported ) {
return new this.dataTransfer( null, sourceEditor );
} else if ( evt && evt.data && evt.data.$ ) {
var dataTransfer = new this.dataTransfer( evt.data.$.clipboardData, sourceEditor );
if ( this.copyCutData && dataTransfer.id == this.copyCutData.id ) {
dataTransfer = this.copyCutData;
dataTransfer.$ = evt.data.$.clipboardData;
} else {
this.copyCutData = dataTransfer;
}
return dataTransfer;
} else {
return new this.dataTransfer( null, sourceEditor );
}
},
/**
* Prevents dropping on the specified element.
*
* @since 4.5
* @param {CKEDITOR.dom.element} element The element on which dropping should be disabled.
*/
preventDefaultDropOnElement: function( element ) {
element && element.on( 'dragover', preventDefaultSetDropEffectToNone );
}
};
// Data type used to link drag and drop events.
//
// In IE URL data type is buggie and there is no way to mark drag & drop without
// modifying text data (which would be displayed if user drop content to the textarea)
// so we just read dragged text.
//
// In Chrome and Firefox we can use custom data types.
var clipboardIdDataType = CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ? 'cke/id' : 'Text';
/**
* Facade for the native `dataTransfer`/`clipboadData` object to hide all differences
* between browsers.
*
* @since 4.5
* @class CKEDITOR.plugins.clipboard.dataTransfer
* @constructor Creates a class instance.
* @param {Object} [nativeDataTransfer] A native data transfer object.
* @param {CKEDITOR.editor} [editor] The source editor instance. If the editor is defined, dataValue will
* be created based on the editor content and the type will be 'html'.
*/
CKEDITOR.plugins.clipboard.dataTransfer = function( nativeDataTransfer, editor ) {
if ( nativeDataTransfer ) {
this.$ = nativeDataTransfer;
}
this._ = {
metaRegExp: /^