/**
* @license Copyright (c) 2003-2017, 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 listener.
// 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 presentational 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 presentational 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,az,bg,bn,bs,ca,cs,cy,da,de,de-ch,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,oc,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' ) );
// Convert image file (if present) to base64 string for Firefox. Do it as the first
// step as the conversion is asynchronous and should hold all further paste processing.
if ( CKEDITOR.env.gecko ) {
var supportedImageTypes = [ 'image/png', 'image/jpeg', 'image/gif' ],
latestId;
editor.on( 'paste', function( evt ) {
var dataObj = evt.data,
data = dataObj.dataValue,
dataTransfer = dataObj.dataTransfer;
// If data empty check for image content inside data transfer. #16705
if ( !data && dataObj.method == 'paste' && dataTransfer && dataTransfer.getFilesCount() == 1 && latestId != dataTransfer.id ) {
var file = dataTransfer.getFile( 0 );
if ( CKEDITOR.tools.indexOf( supportedImageTypes, file.type ) != -1 ) {
var fileReader = new FileReader();
// Convert image file to img tag with base64 image.
fileReader.addEventListener( 'load', function() {
evt.data.dataValue = '<img src="' + fileReader.result + '" />';
editor.fire( 'paste', evt.data );
}, false );
// Proceed with normal flow if reading file was aborted.
fileReader.addEventListener( 'abort', function() {
editor.fire( 'paste', evt.data );
}, false );
// Proceed with normal flow if reading file failed.
fileReader.addEventListener( 'error', function() {
editor.fire( 'paste', evt.data );
}, false );
fileReader.readAsDataURL( file );
latestId = dataObj.dataTransfer.id;
evt.stop();
}
}
}, null, null, 1 );
}
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 class="Apple-converted-space"> <\/span>/gi, ' ' );
// Strip <span> 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 class="Apple-tab-span"[^>]*>([^<]*)<\/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 class="Apple-interchange-newline">' ) > -1 ) {
evt.data.startsWithEOL = 1;
evt.data.preSniffing = 'html'; // Mark as not text.
data = data.replace( /<br class="Apple-interchange-newline">/, '' );
}
// 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 <br> (added on FF).
if ( editable_wrapper )
data = editable_wrapper.getHtml().replace( /<br>$/i, '' );
}
if ( CKEDITOR.env.ie ) {
// <p> -> <p> (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 ) {
// </p><div><br></div> -> </p><br>
// We don't mark br, because this situation can happen for htmlified text too.
data = data.replace( /<\/(\w+)><div><br><\/div>$/, function( match, elementName ) {
if ( elementName in blockElements ) {
evt.data.endsWithEOL = 1;
return '</' + elementName + '>';
}
return match;
} );
} else if ( CKEDITOR.env.gecko ) {
// Firefox adds bogus <br> when user pasted text followed by space(s).
data = data.replace( /(\s)<br>$/, '$1' );
}
evt.data.dataValue = data;
}, null, null, 3 );
editor.on( 'paste', function( evt ) {
var dataObj = evt.data,
type = dataObj.type,
data = dataObj.dataValue,
trueType,
// Default is 'html'.
defaultType = editor.config.clipboard_defaultContentType || 'html',
transferType = dataObj.dataTransfer.getTransferType( editor );
// If forced type is 'html' we don't need to know true data type.
if ( type == 'html' || dataObj.preSniffing == 'html' ) {
trueType = 'html';
} else {
trueType = recogniseContentType( data );
}
// Unify text markup.
if ( trueType == 'htmlifiedtext' ) {
data = htmlifiedTextHtmlification( editor.config, data );
}
// Strip presentational markup & unify text markup.
// Forced plain text (dialog or forcePAPT).
// Note: we do not check dontFilter option in this case, because forcePAPT was implemented
// before pasteFilter and pasteFilter is automatically used on Webkit&Blink since 4.5, so
// forcePAPT should have priority as it had before 4.5.
if ( type == 'text' && trueType == 'html' ) {
data = filterContent( editor, data, filtersFactory.get( 'plain-text' ) );
}
// External paste and pasteFilter exists and filtering isn't disabled.
else if ( transferType == CKEDITOR.DATA_TRANSFER_EXTERNAL && editor.pasteFilter && !dataObj.dontFilter ) {
data = filterContent( editor, data, editor.pasteFilter );
}
if ( dataObj.startsWithEOL ) {
data = '<br data-cke-eol="1">' + data;
}
if ( dataObj.endsWithEOL ) {
data += '<br data-cke-eol="1">';
}
if ( type == 'auto' ) {
type = ( trueType == 'html' || defaultType == 'html' ) ? 'html' : 'text';
}
dataObj.type = type;
dataObj.dataValue = data;
delete dataObj.preSniffing;
delete dataObj.startsWithEOL;
delete dataObj.endsWithEOL;
}, null, null, 6 );
// Inserts processed data into the editor at the end of the
// events chain.
editor.on( 'paste', function( evt ) {
var data = evt.data;
if ( data.dataValue ) {
editor.insertHtml( data.dataValue, data.type, data.range );
// Defer 'afterPaste' so all other listeners for 'paste' will be fired first.
// Fire afterPaste only if paste inserted some HTML.
setTimeout( function() {
editor.fire( 'afterPaste' );
}, 0 );
}
}, null, null, 1000 );
editor.on( 'pasteDialog', function( evt ) {
// TODO it's possible that this setTimeout is not needed any more,
// because of changes introduced in the same commit as this comment.
// Editor.getClipboardData adds listener to the dialog's events which are
// fired after a while (not like 'showDialog').
setTimeout( function() {
// Open default paste dialog.
editor.openDialog( 'paste', evt.data );
}, 0 );
} );
}
} );
function firePasteEvents( editor, data, withBeforePaste ) {
if ( !data.type ) {
data.type = 'auto';
}
if ( withBeforePaste ) {
// Fire 'beforePaste' event so clipboard flavor get customized
// by other plugins.
if ( editor.fire( 'beforePaste', data ) === false )
return false; // Event canceled
}
// Do not fire paste if there is no data (dataValue and dataTranfser are empty).
// This check should be done after firing 'beforePaste' because for native paste
// 'beforePaste' is by default fired even for empty clipboard.
if ( !data.dataValue && data.dataTransfer.isEmpty() ) {
return false;
}
if ( !data.dataValue ) {
data.dataValue = '';
}
// Because of FF bug we need to use this hack, otherwise cursor is hidden
// or it is not possible to move it (#12420).
// Also, check that editor.toolbox exists, because the toolbar plugin might not be loaded (#13305).
if ( CKEDITOR.env.gecko && data.method == 'drop' && editor.toolbox ) {
editor.once( 'afterPaste', function() {
editor.toolbox.focus();
} );
}
return editor.fire( 'paste', data );
}
function initPasteClipboard( editor ) {
var clipboard = CKEDITOR.plugins.clipboard,
preventBeforePasteEvent = 0,
preventPasteEvent = 0,
inReadOnly = 0;
addListeners();
addButtonsCommands();
/**
* Gets clipboard data by directly accessing the clipboard (IE only) or opening the paste dialog window.
*
* editor.getClipboardData( { title: 'Get my data' }, function( data ) {
* if ( data )
* alert( data.type + ' ' + data.dataValue );
* } );
*
* @member CKEDITOR.editor
* @param {Object} options
* @param {String} [options.title] The title of the paste dialog window.
* @param {Function} callback A function that will be executed with `data.type` and `data.dataValue`
* or `null` if none of the capturing methods succeeded.
*/
editor.getClipboardData = function( options, callback ) {
var beforePasteNotCanceled = false,
dataType = 'auto',
dialogCommited = false;
// Options are optional - args shift.
if ( !callback ) {
callback = options;
options = null;
}
// Listen with maximum priority to handle content before everyone else.
// This callback will handle paste event that will be fired if direct
// access to the clipboard succeed in IE.
editor.on( 'paste', onPaste, null, null, 0 );
// Listen at the end of listeners chain to see if event wasn't canceled
// and to retrieve modified data.type.
editor.on( 'beforePaste', onBeforePaste, null, null, 1000 );
// getClipboardDataDirectly() will fire 'beforePaste' synchronously, so we can
// check if it was canceled and if any listener modified data.type.
// If command didn't succeed (only IE allows to access clipboard and only if
// user agrees) open and handle paste dialog.
if ( getClipboardDataDirectly() === false ) {
// Direct access to the clipboard wasn't successful so remove listener.
editor.removeListener( 'paste', onPaste );
// If beforePaste was canceled do not open dialog.
// Add listeners only if dialog really opened. 'pasteDialog' can be canceled.
if ( beforePasteNotCanceled && editor.fire( 'pasteDialog', onDialogOpen ) ) {
editor.on( 'pasteDialogCommit', onDialogCommit );
// 'dialogHide' will be fired after 'pasteDialogCommit'.
editor.on( 'dialogHide', function( evt ) {
evt.removeListener();
evt.data.removeListener( 'pasteDialogCommit', onDialogCommit );
// Because Opera has to wait a while in pasteDialog we have to wait here.
setTimeout( function() {
// Notify even if user canceled dialog (clicked 'cancel', ESC, etc).
if ( !dialogCommited )
callback( null );
}, 10 );
} );
} else {
callback( null );
}
}
function onPaste( evt ) {
evt.removeListener();
evt.cancel();
callback( evt.data );
}
function onBeforePaste( evt ) {
evt.removeListener();
beforePasteNotCanceled = true;
dataType = evt.data.type;
}
function onDialogCommit( evt ) {
evt.removeListener();
// Cancel pasteDialogCommit so paste dialog won't automatically fire
// 'paste' evt by itself.
evt.cancel();
dialogCommited = true;
callback( {
type: dataType,
dataValue: evt.data.dataValue,
dataTransfer: evt.data.dataTransfer,
method: 'paste'
} );
}
function onDialogOpen() {
this.customTitle = ( options && options.title );
}
};
function addButtonsCommands() {
addButtonCommand( 'Cut', 'cut', createCutCopyCmd( 'cut' ), 10, 1 );
addButtonCommand( 'Copy', 'copy', createCutCopyCmd( 'copy' ), 20, 4 );
addButtonCommand( 'Paste', 'paste', createPasteCmd(), 30, 8 );
function addButtonCommand( buttonName, commandName, command, toolbarOrder, ctxMenuOrder ) {
var lang = editor.lang.clipboard[ commandName ];
editor.addCommand( commandName, command );
editor.ui.addButton && editor.ui.addButton( buttonName, {
label: lang,
command: commandName,
toolbar: 'clipboard,' + toolbarOrder
} );
// If the "menu" plugin is loaded, register the menu item.
if ( editor.addMenuItems ) {
editor.addMenuItem( commandName, {
label: lang,
command: commandName,
group: 'clipboard',
order: ctxMenuOrder
} );
}
}
}
function addListeners() {
editor.on( 'key', onKey );
editor.on( 'contentDom', addPasteListenersToEditable );
// For improved performance, we're checking the readOnly state on selectionChange instead of hooking a key event for that.
editor.on( 'selectionChange', function( evt ) {
inReadOnly = evt.data.selection.getRanges()[ 0 ].checkReadOnly();
setToolbarStates();
} );
// If the "contextmenu" plugin is loaded, register the listeners.
if ( editor.contextMenu ) {
editor.contextMenu.addListener( function( element, selection ) {
inReadOnly = selection.getRanges()[ 0 ].checkReadOnly();
return {
cut: stateFromNamedCommand( 'cut' ),
copy: stateFromNamedCommand( 'copy' ),
paste: stateFromNamedCommand( 'paste' )
};
} );
}
}
// Add events listeners to editable.
function addPasteListenersToEditable() {
var editable = editor.editable();
if ( CKEDITOR.plugins.clipboard.isCustomCopyCutSupported ) {
var initOnCopyCut = function( evt ) {
// If user tries to cut in read-only editor, we must prevent default action. (#13872)
if ( !editor.readOnly || evt.name != 'cut' ) {
clipboard.initPasteDataTransfer( evt, editor );
}
evt.data.preventDefault();
};
editable.on( 'copy', initOnCopyCut );
editable.on( 'cut', initOnCopyCut );
// Delete content with the low priority so one can overwrite cut data.
editable.on( 'cut', function() {
// If user tries to cut in read-only editor, we must prevent default action. (#13872)
if ( !editor.readOnly ) {
editor.extractSelectedHtml();
}
}, null, null, 999 );
}
// We'll be catching all pasted content in one line, regardless of whether
// it's introduced by a document command execution (e.g. toolbar buttons) or
// user paste behaviors (e.g. CTRL+V).
editable.on( clipboard.mainPasteEvent, function( evt ) {
if ( clipboard.mainPasteEvent == 'beforepaste' && preventBeforePasteEvent ) {
return;
}
// If you've just asked yourself why preventPasteEventNow() is not here, but
// in listener for CTRL+V and exec method of 'paste' command
// you've asked the same question we did.
//
// THE ANSWER:
//
// First thing to notice - this answer makes sense only for IE,
// because other browsers don't listen for 'paste' event.
//
// What would happen if we move preventPasteEventNow() here?
// For:
// * CTRL+V - IE fires 'beforepaste', so we prevent 'paste' and pasteDataFromClipboard(). OK.
// * editor.execCommand( 'paste' ) - we fire 'beforepaste', so we prevent
// 'paste' and pasteDataFromClipboard() and doc.execCommand( 'Paste' ). OK.
// * native context menu - IE fires 'beforepaste', so we prevent 'paste', but unfortunately
// on IE we fail with pasteDataFromClipboard() here, because of... we don't know why, but
// we just fail, so... we paste nothing. FAIL.
// * native menu bar - the same as for native context menu.
//
// But don't you know any way to distinguish first two cases from last two?
// Only one - special flag set in CTRL+V handler and exec method of 'paste'
// command. And that's what we did using preventPasteEventNow().
pasteDataFromClipboard( evt );
} );
// It's not possible to clearly handle all four paste methods (ctrl+v, native menu bar
// native context menu, editor's command) in one 'paste/beforepaste' event in IE.
//
// For ctrl+v & editor's command it's easy to handle pasting in 'beforepaste' listener,
// so we do this. For another two methods it's better to use 'paste' event.
//
// 'paste' is always being fired after 'beforepaste' (except of weird one on opening native
// context menu), so for two methods handled in 'beforepaste' we're canceling 'paste'
// using preventPasteEvent state.
//
// 'paste' event in IE is being fired before getClipboardDataByPastebin executes its callback.
//
// QUESTION: Why didn't you handle all 4 paste methods in handler for 'paste'?
// Wouldn't this just be simpler?
// ANSWER: Then we would have to evt.data.preventDefault() only for native
// context menu and menu bar pastes. The same with execIECommand().
// That would force us to mark CTRL+V and editor's paste command with
// special flag, other than preventPasteEvent. But we still would have to
// have preventPasteEvent for the second event fired by execIECommand.
// Code would be longer and not cleaner.
if ( clipboard.mainPasteEvent == 'beforepaste' ) {
editable.on( 'paste', function( evt ) {
if ( preventPasteEvent ) {
return;
}
// Cancel next 'paste' event fired by execIECommand( 'paste' )
// at the end of this callback.
preventPasteEventNow();
// Prevent native paste.
evt.data.preventDefault();
pasteDataFromClipboard( evt );
// Force IE to paste content into pastebin so pasteDataFromClipboard will work.
if ( !execIECommand( 'paste' ) ) {
editor.openDialog( 'paste' );
}
} );
// If mainPasteEvent is 'beforePaste' (IE before Edge),
// dismiss the (wrong) 'beforepaste' event fired on context/toolbar menu open. (#7953)
editable.on( 'contextmenu', preventBeforePasteEventNow, null, null, 0 );
editable.on( 'beforepaste', function( evt ) {
// Do not prevent event on CTRL+V and SHIFT+INS because it blocks paste (#11970).
if ( evt.data && !evt.data.$.ctrlKey && !evt.data.$.shiftKey )
preventBeforePasteEventNow();
}, null, null, 0 );
}
editable.on( 'beforecut', function() {
!preventBeforePasteEvent && fixCut( editor );
} );
var mouseupTimeout;
// Use editor.document instead of editable in non-IEs for observing mouseup
// since editable won't fire the event if selection process started within
// iframe and ended out of the editor (#9851).
editable.attachListener( CKEDITOR.env.ie ? editable : editor.document.getDocumentElement(), 'mouseup', function() {
mouseupTimeout = setTimeout( function() {
setToolbarStates();
}, 0 );
} );
// Make sure that deferred mouseup callback isn't executed after editor instance
// had been destroyed. This may happen when editor.destroy() is called in parallel
// with mouseup event (i.e. a button with onclick callback) (#10219).
editor.on( 'destroy', function() {
clearTimeout( mouseupTimeout );
} );
editable.on( 'keyup', setToolbarStates );
}
// Create object representing Cut or Copy commands.
function createCutCopyCmd( type ) {
return {
type: type,
canUndo: type == 'cut', // We can't undo copy to clipboard.
startDisabled: true,
fakeKeystroke: type == 'cut' ? CKEDITOR.CTRL + 88 /*X*/ : CKEDITOR.CTRL + 67 /*C*/,
exec: function() {
// Attempts to execute the Cut and Copy operations.
function tryToCutCopy( type ) {
if ( CKEDITOR.env.ie )
return execIECommand( type );
// non-IEs part
try {
// Other browsers throw an error if the command is disabled.
return editor.document.$.execCommand( type, false, null );
} catch ( e ) {
return false;
}
}
this.type == 'cut' && fixCut();
var success = tryToCutCopy( this.type );
if ( !success ) {
// Show cutError or copyError.
editor.showNotification( editor.lang.clipboard[ this.type + 'Error' ] ); // jshint ignore:line
}
return success;
}
};
}
function createPasteCmd() {
return {
// Snapshots are done manually by editable.insertXXX methods.
canUndo: false,
async: true,
fakeKeystroke: CKEDITOR.CTRL + 86 /*V*/,
exec: function( editor, data ) {
var cmd = this,
fire = function( data, withBeforePaste ) {
data && firePasteEvents( editor, data, !!withBeforePaste );
editor.fire( 'afterCommandExec', {
name: 'paste',
command: cmd,
returnValue: !!data
} );
};
// Check data precisely - don't open dialog on empty string.
if ( typeof data == 'string' )
fire( {
dataValue: data,
method: 'paste',
dataTransfer: clipboard.initPasteDataTransfer()
}, 1 );
else
editor.getClipboardData( fire );
}
};
}
function preventPasteEventNow() {
preventPasteEvent = 1;
// For safety reason we should wait longer than 0/1ms.
// We don't know how long execution of quite complex getClipboardData will take
// and in for example 'paste' listener execCommand() (which fires 'paste') is called
// after getClipboardData finishes.
// Luckily, it's impossible to immediately fire another 'paste' event we want to handle,
// because we only handle there native context menu and menu bar.
setTimeout( function() {
preventPasteEvent = 0;
}, 100 );
}
function preventBeforePasteEventNow() {
preventBeforePasteEvent = 1;
setTimeout( function() {
preventBeforePasteEvent = 0;
}, 10 );
}
// Tries to execute any of the paste, cut or copy commands in IE. Returns a
// boolean indicating that the operation succeeded.
// @param {String} command *LOWER CASED* name of command ('paste', 'cut', 'copy').
function execIECommand( command ) {
var doc = editor.document,
body = doc.getBody(),
enabled = false,
onExec = function() {
enabled = true;
};
// The following seems to be the only reliable way to detect that
// clipboard commands are enabled in IE. It will fire the
// onpaste/oncut/oncopy events only if the security settings allowed
// the command to execute.
body.on( command, onExec );
// IE7: document.execCommand has problem to paste into positioned element.
if ( CKEDITOR.env.version > 7 ) {
doc.$.execCommand( command );
} else {
doc.$.selection.createRange().execCommand( command );
}
body.removeListener( command, onExec );
return enabled;
}
// Cutting off control type element in IE standards breaks the selection entirely. (#4881)
function fixCut() {
if ( !CKEDITOR.env.ie || CKEDITOR.env.quirks )
return;
var sel = editor.getSelection(),
control, range, dummy;
if ( ( sel.getType() == CKEDITOR.SELECTION_ELEMENT ) && ( control = sel.getSelectedElement() ) ) {
range = sel.getRanges()[ 0 ];
dummy = editor.document.createText( '' );
dummy.insertBefore( control );
range.setStartBefore( dummy );
range.setEndAfter( control );
sel.selectRanges( [ range ] );
// Clear up the fix if the paste wasn't succeeded.
setTimeout( function() {
// Element still online?
if ( control.getParent() ) {
dummy.remove();
sel.selectElement( control );
}
}, 0 );
}
}
// Allow to peek clipboard content by redirecting the
// pasting content into a temporary bin and grab the content of it.
function getClipboardDataByPastebin( evt, callback ) {
var doc = editor.document,
editable = editor.editable(),
cancel = function( evt ) {
evt.cancel();
},
blurListener;
// Avoid recursions on 'paste' event or consequent paste too fast. (#5730)
if ( doc.getById( 'cke_pastebin' ) )
return;
var sel = editor.getSelection();
var bms = sel.createBookmarks();
// #11384. On IE9+ we use native selectionchange (i.e. editor#selectionCheck) to cache the most
// recent selection which we then lock on editable blur. See selection.js for more info.
// selectionchange fired before getClipboardDataByPastebin() cached selection
// before creating bookmark (cached selection will be invalid, because bookmarks modified the DOM),
// so we need to fire selectionchange one more time, to store current seleciton.
// Selection will be locked when we focus pastebin.
if ( CKEDITOR.env.ie )
sel.root.fire( 'selectionchange' );
// Create container to paste into.
// For rich content we prefer to use "body" since it holds
// the least possibility to be splitted by pasted content, while this may
// breaks the text selection on a frame-less editable, "div" would be
// the best one in that case.
// In another case on old IEs moving the selection into a "body" paste bin causes error panic.
// Body can't be also used for Opera which fills it with <br>
// what is indistinguishable from pasted <br> (copying <br> in Opera isn't possible,
// but it can be copied from other browser).
var pastebin = new CKEDITOR.dom.element(
( CKEDITOR.env.webkit || editable.is( 'body' ) ) && !CKEDITOR.env.ie ? 'body' : 'div', doc );
pastebin.setAttributes( {
id: 'cke_pastebin',
'data-cke-temp': '1'
} );
var containerOffset = 0,
offsetParent,
win = doc.getWindow();
if ( CKEDITOR.env.webkit ) {
// It's better to paste close to the real paste destination, so inherited styles
// (which Webkits will try to compensate by styling span) differs less from the destination's one.
editable.append( pastebin );
// Style pastebin like .cke_editable, to minimize differences between origin and destination. (#9754)
pastebin.addClass( 'cke_editable' );
// Compensate position of offsetParent.
if ( !editable.is( 'body' ) ) {
// We're not able to get offsetParent from pastebin (body element), so check whether
// its parent (editable) is positioned.
if ( editable.getComputedStyle( 'position' ) != 'static' )
offsetParent = editable;
// And if not - safely get offsetParent from editable.
else
offsetParent = CKEDITOR.dom.element.get( editable.$.offsetParent );
containerOffset = offsetParent.getDocumentPosition().y;
}
} else {
// Opera and IE doesn't allow to append to html element.
editable.getAscendant( CKEDITOR.env.ie ? 'body' : 'html', 1 ).append( pastebin );
}
pastebin.setStyles( {
position: 'absolute',
// Position the bin at the top (+10 for safety) of viewport to avoid any subsequent document scroll.
top: ( win.getScrollPosition().y - containerOffset + 10 ) + 'px',
width: '1px',
// Caret has to fit in that height, otherwise browsers like Chrome & Opera will scroll window to show it.
// Set height equal to viewport's height - 20px (safety gaps), minimum 1px.
height: Math.max( 1, win.getViewPaneSize().height - 20 ) + 'px',
overflow: 'hidden',
// Reset styles that can mess up pastebin position.
margin: 0,
padding: 0
} );
// Paste fails in Safari when the body tag has 'user-select: none'. (#12506)
if ( CKEDITOR.env.safari )
pastebin.setStyles( CKEDITOR.tools.cssVendorPrefix( 'user-select', 'text' ) );
// Check if the paste bin now establishes new editing host.
var isEditingHost = pastebin.getParent().isReadOnly();
if ( isEditingHost ) {
// Hide the paste bin.
pastebin.setOpacity( 0 );
// And make it editable.
pastebin.setAttribute( 'contenteditable', true );
}
// Transparency is not enough since positioned non-editing host always shows
// resize handler, pull it off the screen instead.
else {
pastebin.setStyle( editor.config.contentsLangDirection == 'ltr' ? 'left' : 'right', '-10000px' );
}
editor.on( 'selectionChange', cancel, null, null, 0 );
// Webkit fill fire blur on editable when moving selection to
// pastebin (if body is used). Cancel it because it causes incorrect
// selection lock in case of inline editor (#10644).
// The same seems to apply to Firefox (#10787).
if ( CKEDITOR.env.webkit || CKEDITOR.env.gecko )
blurListener = editable.once( 'blur', cancel, null, null, -100 );
// Temporarily move selection to the pastebin.
isEditingHost && pastebin.focus();
var range = new CKEDITOR.dom.range( pastebin );
range.selectNodeContents( pastebin );
var selPastebin = range.select();
// If non-native paste is executed, IE will open security alert and blur editable.
// Editable will then lock selection inside itself and after accepting security alert
// this selection will be restored. We overwrite stored selection, so it's restored
// in pastebin. (#9552)
if ( CKEDITOR.env.ie ) {
blurListener = editable.once( 'blur', function() {
editor.lockSelection( selPastebin );
} );
}
var scrollTop = CKEDITOR.document.getWindow().getScrollPosition().y;
// Wait a while and grab the pasted contents.
setTimeout( function() {
// Restore main window's scroll position which could have been changed
// by browser in cases described in #9771.
if ( CKEDITOR.env.webkit )
CKEDITOR.document.getBody().$.scrollTop = scrollTop;
// Blur will be fired only on non-native paste. In other case manually remove listener.
blurListener && blurListener.removeListener();
// Restore properly the document focus. (#8849)
if ( CKEDITOR.env.ie )
editable.focus();
// IE7: selection must go before removing pastebin. (#8691)
sel.selectBookmarks( bms );
pastebin.remove();
// Grab the HTML contents.
// We need to look for a apple style wrapper on webkit it also adds
// a div wrapper if you copy/paste the body of the editor.
// Remove hidden div and restore selection.
var bogusSpan;
if ( CKEDITOR.env.webkit && ( bogusSpan = pastebin.getFirst() ) && ( bogusSpan.is && bogusSpan.hasClass( 'Apple-style-span' ) ) )
pastebin = bogusSpan;
editor.removeListener( 'selectionChange', cancel );
callback( pastebin.getHtml() );
}, 0 );
}
// Try to get content directly on IE from clipboard, without native event
// being fired before. In other words - synthetically get clipboard data, if it's possible.
// mainPasteEvent will be fired, so if forced native paste:
// * worked, getClipboardDataByPastebin will grab it,
// * didn't work, dataValue and dataTransfer will be empty and editor#paste won't be fired.
// Clipboard data can be accessed directly only on IEs older than Edge.
// On other browsers we should fire beforePaste event and return false.
function getClipboardDataDirectly() {
if ( clipboard.mainPasteEvent == 'paste' ) {
// beforePaste should be fired when dialog open so it can be canceled.
editor.fire( 'beforePaste', { type: 'auto', method: 'paste' } );
return false;
}
// Prevent IE from pasting at the begining of the document.
editor.focus();
// Command will be handled by 'beforepaste', but as
// execIECommand( 'paste' ) will fire also 'paste' event
// we're canceling it.
preventPasteEventNow();
// #9247: Lock focus to prevent IE from hiding toolbar for inline editor.
var focusManager = editor.focusManager;
focusManager.lock();
if ( editor.editable().fire( clipboard.mainPasteEvent ) && !execIECommand( 'paste' ) ) {
focusManager.unlock();
return false;
}
focusManager.unlock();
return true;
}
// Listens for some clipboard related keystrokes, so they get customized.
// Needs to be bind to keydown event.
function onKey( event ) {
if ( editor.mode != 'wysiwyg' )
return;
switch ( event.data.keyCode ) {
// Paste
case CKEDITOR.CTRL + 86: // CTRL+V
case CKEDITOR.SHIFT + 45: // SHIFT+INS
var editable = editor.editable();
// Cancel 'paste' event because ctrl+v is for IE handled
// by 'beforepaste'.
preventPasteEventNow();
// Simulate 'beforepaste' event for all browsers using 'paste' as main event.
if ( clipboard.mainPasteEvent == 'paste' ) {
editable.fire( 'beforepaste' );
}
return;
// Cut
case CKEDITOR.CTRL + 88: // CTRL+X
case CKEDITOR.SHIFT + 46: // SHIFT+DEL
// Save Undo snapshot.
editor.fire( 'saveSnapshot' ); // Save before cut
setTimeout( function() {
editor.fire( 'saveSnapshot' ); // Save after cut
}, 50 ); // OSX is slow (#11416).
}
}
function pasteDataFromClipboard( evt ) {
// Default type is 'auto', but can be changed by beforePaste listeners.
var eventData = {
type: 'auto',
method: 'paste',
dataTransfer: clipboard.initPasteDataTransfer( evt )
};
eventData.dataTransfer.cacheData();
// Fire 'beforePaste' event so clipboard flavor get customized by other plugins.
// If 'beforePaste' is canceled continue executing getClipboardDataByPastebin and then do nothing
// (do not fire 'paste', 'afterPaste' events). This way we can grab all - synthetically
// and natively pasted content and prevent its insertion into editor
// after canceling 'beforePaste' event.
var beforePasteNotCanceled = editor.fire( 'beforePaste', eventData ) !== false;
// Do not use paste bin if the browser let us get HTML or files from dataTranfer.
if ( beforePasteNotCanceled && clipboard.canClipboardApiBeTrusted( eventData.dataTransfer, editor ) ) {
evt.data.preventDefault();
setTimeout( function() {
firePasteEvents( editor, eventData );
}, 0 );
} else {
getClipboardDataByPastebin( evt, function( data ) {
// Clean up.
eventData.dataValue = data.replace( /<span[^>]+data-cke-bookmark[^<]*?<\/span>/ig, '' );
// Fire remaining events (without beforePaste)
beforePasteNotCanceled && firePasteEvents( editor, eventData );
} );
}
}
function setToolbarStates() {
if ( editor.mode != 'wysiwyg' )
return;
var pasteState = stateFromNamedCommand( 'paste' );
editor.getCommand( 'cut' ).setState( stateFromNamedCommand( 'cut' ) );
editor.getCommand( 'copy' ).setState( stateFromNamedCommand( 'copy' ) );
editor.getCommand( 'paste' ).setState( pasteState );
editor.fire( 'pasteState', pasteState );
}
function stateFromNamedCommand( command ) {
if ( inReadOnly && command in { paste: 1, cut: 1 } )
return CKEDITOR.TRISTATE_DISABLED;
if ( command == 'paste' )
return CKEDITOR.TRISTATE_OFF;
// Cut, copy - check if the selection is not empty.
var sel = editor.getSelection(),
ranges = sel.getRanges(),
selectionIsEmpty = sel.getType() == CKEDITOR.SELECTION_NONE || ( ranges.length == 1 && ranges[ 0 ].collapsed );
return selectionIsEmpty ? CKEDITOR.TRISTATE_DISABLED : CKEDITOR.TRISTATE_OFF;
}
}
// Returns:
// * 'htmlifiedtext' if content looks like transformed by browser from plain text.
// See clipboard/paste.html TCs for more info.
// * 'html' if it is not 'htmlifiedtext'.
function recogniseContentType( data ) {
if ( CKEDITOR.env.webkit ) {
// Plain text or ( <div><br></div> and text inside <div> ).
if ( !data.match( /^[^<]*$/g ) && !data.match( /^(<div><br( ?\/)?><\/div>|<div>[^<]*<\/div>)*$/gi ) )
return 'html';
} else if ( CKEDITOR.env.ie ) {
// Text and <br> or ( text and <br> in <p> - paragraphs can be separated by new \r\n ).
if ( !data.match( /^([^<]|<br( ?\/)?>)*$/gi ) && !data.match( /^(<p>([^<]|<br( ?\/)?>)*<\/p>|(\r\n))*$/gi ) )
return 'html';
} else if ( CKEDITOR.env.gecko ) {
// Text or <br>.
if ( !data.match( /^([^<]|<br( ?\/)?>)*$/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 <br> at the beginning of last <p>.
return CKEDITOR.tools.repeat( '</p><p>', ~~( repeats / 2 ) ) + ( repeats % 2 == 1 ? '<br>' : '' );
}
// Replace adjacent white-spaces (EOLs too - Fx sometimes keeps them) with one space.
data = data.replace( /\s+/g, ' ' )
// Remove spaces from between tags.
.replace( /> +</g, '><' )
// Normalize XHTML syntax and upper cased <br> tags.
.replace( /<br ?\/>/gi, '<br>' );
// IE - lower cased tags.
data = data.replace( /<\/?[A-Z]+>/g, function( match ) {
return match.toLowerCase();
} );
// Don't touch single lines (no <br|p|div>) - nothing to do here.
if ( data.match( /^[^<]$/ ) )
return data;
// Webkit.
if ( CKEDITOR.env.webkit && data.indexOf( '<div>' ) > -1 ) {
// One line break at the beginning - insert <br>
data = data.replace( /^(<div>(<br>|)<\/div>)(?!$|(<div>(<br>|)<\/div>))/g, '<br>' )
// Two or more - reduce number of new lines by one.
.replace( /^(<div>(<br>|)<\/div>){2}(?!$)/g, '<div></div>' );
// Two line breaks create one paragraph in Webkit.
if ( data.match( /<div>(<br>|)<\/div>/ ) ) {
data = '<p>' + data.replace( /(<div>(<br>|)<\/div>)+/g, function( match ) {
return repeatParagraphs( match.split( '</div><div>' ).length + 1 );
} ) + '</p>';
}
// One line break create br.
data = data.replace( /<\/div><div>/g, '<br>' );
// Remove remaining divs.
data = data.replace( /<\/?div>/g, '' );
}
// Opera and Firefox and enterMode != BR.
if ( CKEDITOR.env.gecko && config.enterMode != CKEDITOR.ENTER_BR ) {
// Remove bogus <br> - Fx generates two <brs> for one line break.
// For two line breaks it still produces two <brs>, but it's better to ignore this case than the first one.
if ( CKEDITOR.env.gecko )
data = data.replace( /^<br><br>$/, '<br>' );
// This line satisfy edge case when for Opera we have two line breaks
//data = data.replace( /)
if ( data.indexOf( '<br><br>' ) > -1 ) {
// Two line breaks create one paragraph, three - 2, four - 3, etc.
data = '<p>' + data.replace( /(<br>){2,}/g, function( match ) {
return repeatParagraphs( match.length / 4 );
} ) + '</p>';
}
}
return switchEnterMode( config, data );
}
function filtersFactoryFactory() {
var filters = {};
function setUpTags() {
var tags = {};
for ( var tag in CKEDITOR.dtd ) {
if ( tag.charAt( 0 ) != '$' && tag != 'div' && tag != 'span' ) {
tags[ tag ] = 1;
}
}
return tags;
}
function createSemanticContentFilter() {
var filter = new CKEDITOR.filter();
filter.allow( {
$1: {
elements: setUpTags(),
attributes: true,
styles: false,
classes: false
}
} );
return filter;
}
return {
get: function( type ) {
if ( type == 'plain-text' ) {
// Does this look confusing to you? Did we forget about enter mode?
// It is a trick that let's us creating one filter for edidtor, regardless of its
// activeEnterMode (which as the name indicates can change during runtime).
//
// How does it work?
// The active enter mode is passed to the filter.applyTo method.
// The filter first marks all elements except <br> as disallowed and then tries to remove
// them. However, it cannot remove e.g. a <p> 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><p>)+/g, function( match ) {
return CKEDITOR.tools.repeat( '<br>', 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 );
}, null, null, 2 );
editable.attachListener( editor, 'dragstart', function() {
// 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, 100 );
// -------------- 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 ) {
// Do nothing if event was already prevented. (#13879)
if ( evt.data.$.defaultPrevented ) {
return;
}
// Cancel native drop.
evt.data.preventDefault();
var target = evt.data.getTarget(),
readOnly = target.isReadOnly();
// Do nothing if drop on non editable element (#13015).
// The <html> 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 );
}, null, null, 9999 );
// 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 <kbd>Ctrl+C</kbd> 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 the drop range happens to be inside drag range change it's position to the beginning of the drag range.
if ( dropInsideDragRange ) {
// We only change position of bookmark span that is connected with dropBookmark.
// dropRange will be overwritten and set to the dropBookmark later.
dropNode.insertBefore( startNode );
}
// 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( '<span id="' + id + '">\u200b</span>' );
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 ) {
// Edge does not support custom copy/cut, but it have some useful data in the clipboardData (#13755).
return new this.dataTransfer( ( CKEDITOR.env.edge && evt && evt.data.$ && evt.data.$.clipboardData ) || 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: /^<meta.*?>/i,
bodyRegExp: /<body(?:[\s\S]*?)>([\s\S]*)<\/body>/i,
fragmentRegExp: /<!--(?:Start|End)Fragment-->/g,
data: {},
files: [],
normalizeType: function( type ) {
type = type.toLowerCase();
if ( type == 'text' || type == 'text/plain' ) {
return 'Text'; // IE support only Text and URL;
} else if ( type == 'url' ) {
return 'URL'; // IE support only Text and URL;
} else {
return type;
}
}
};
// Check if ID is already created.
this.id = this.getData( clipboardIdDataType );
// If there is no ID we need to create it. Different browsers needs different ID.
if ( !this.id ) {
if ( clipboardIdDataType == 'Text' ) {
// For IE10+ only Text data type is supported and we have to compare dragged
// and dropped text. If the ID is not set it means that empty string was dragged
// (ex. image with no alt). We change null to empty string.
this.id = '';
} else {
// String for custom data type.
this.id = 'cke-' + CKEDITOR.tools.getUniqueId();
}
}
// In IE10+ we can not use any data type besides text, so we do not call setData.
if ( clipboardIdDataType != 'Text' ) {
// Try to set ID so it will be passed from the drag to the drop event.
// On some browsers with some event it is not possible to setData so we
// need to catch exceptions.
try {
this.$.setData( clipboardIdDataType, this.id );
} catch ( err ) {}
}
if ( editor ) {
this.sourceEditor = editor;
this.setData( 'text/html', editor.getSelectedHtml( 1 ) );
// Without setData( 'text', ... ) on dragstart there is no drop event in Safari.
// Also 'text' data is empty as drop to the textarea does not work if we do not put there text.
if ( clipboardIdDataType != 'Text' && !this.getData( 'text/plain' ) ) {
this.setData( 'text/plain', editor.getSelection().getSelectedText() );
}
}
/**
* Data transfer ID used to bind all dataTransfer
* objects based on the same event (e.g. in drag and drop events).
*
* @readonly
* @property {String} id
*/
/**
* A native DOM event object.
*
* @readonly
* @property {Object} $
*/
/**
* Source editor — the editor where the drag starts.
* Might be undefined if the drag starts outside the editor (e.g. when dropping files to the editor).
*
* @readonly
* @property {CKEDITOR.editor} sourceEditor
*/
/**
* Private properties and methods.
*
* @private
* @property {Object} _
*/
};
/**
* Data transfer operation (drag and drop or copy and paste) started and ended in the same
* editor instance.
*
* @since 4.5
* @readonly
* @property {Number} [=1]
* @member CKEDITOR
*/
CKEDITOR.DATA_TRANSFER_INTERNAL = 1;
/**
* Data transfer operation (drag and drop or copy and paste) started in one editor
* instance and ended in another.
*
* @since 4.5
* @readonly
* @property {Number} [=2]
* @member CKEDITOR
*/
CKEDITOR.DATA_TRANSFER_CROSS_EDITORS = 2;
/**
* Data transfer operation (drag and drop or copy and paste) started outside of the editor.
* The source of the data may be a textarea, HTML, another application, etc.
*
* @since 4.5
* @readonly
* @property {Number} [=3]
* @member CKEDITOR
*/
CKEDITOR.DATA_TRANSFER_EXTERNAL = 3;
CKEDITOR.plugins.clipboard.dataTransfer.prototype = {
/**
* Facade for the native `getData` method.
*
* @param {String} type The type of data to retrieve.
* @returns {String} type Stored data for the given type or an empty string if the data for that type does not exist.
*/
getData: function( type ) {
function isEmpty( data ) {
return data === undefined || data === null || data === '';
}
type = this._.normalizeType( type );
var data = this._.data[ type ],
result;
if ( isEmpty( data ) ) {
try {
data = this.$.getData( type );
} catch ( e ) {}
}
if ( isEmpty( data ) ) {
data = '';
}
// Some browsers add <meta http-equiv="content-type" content="text/html; charset=utf-8"> at the begging of the HTML data
// or surround it with <html><head>...</head><body>(some content)<!--StartFragment--> and <!--EndFragment-->(some content)</body></html>
// This code removes meta tags and returns only the contents of the <body> element if found. Note that
// some significant content may be placed outside Start/EndFragment comments so it's kept.
//
// See #13583 for more details.
if ( type == 'text/html' ) {
data = data.replace( this._.metaRegExp, '' );
// Keep only contents of the <body> element
result = this._.bodyRegExp.exec( data );
if ( result && result.length ) {
data = result[ 1 ];
// Remove also comments.
data = data.replace( this._.fragmentRegExp, '' );
}
}
// Firefox on Linux put files paths as a text/plain data if there are files
// in the dataTransfer object. We need to hide it, because files should be
// handled on paste only if dataValue is empty.
else if ( type == 'Text' && CKEDITOR.env.gecko && this.getFilesCount() &&
data.substring( 0, 7 ) == 'file://' ) {
data = '';
}
return data;
},
/**
* Facade for the native `setData` method.
*
* @param {String} type The type of data to retrieve.
* @param {String} value The data to add.
*/
setData: function( type, value ) {
type = this._.normalizeType( type );
this._.data[ type ] = value;
// There is "Unexpected call to method or property access." error if you try
// to set data of unsupported type on IE.
if ( !CKEDITOR.plugins.clipboard.isCustomDataTypesSupported && type != 'URL' && type != 'Text' ) {
return;
}
// If we use the text type to bind the ID, then if someone tries to set the text, we must also
// update ID accordingly. #13468.
if ( clipboardIdDataType == 'Text' && type == 'Text' ) {
this.id = value;
}
try {
this.$.setData( type, value );
} catch ( e ) {}
},
/**
* Gets the data transfer type.
*
* @param {CKEDITOR.editor} targetEditor The drop/paste target editor instance.
* @returns {Number} Possible values: {@link CKEDITOR#DATA_TRANSFER_INTERNAL},
* {@link CKEDITOR#DATA_TRANSFER_CROSS_EDITORS}, {@link CKEDITOR#DATA_TRANSFER_EXTERNAL}.
*/
getTransferType: function( targetEditor ) {
if ( !this.sourceEditor ) {
return CKEDITOR.DATA_TRANSFER_EXTERNAL;
} else if ( this.sourceEditor == targetEditor ) {
return CKEDITOR.DATA_TRANSFER_INTERNAL;
} else {
return CKEDITOR.DATA_TRANSFER_CROSS_EDITORS;
}
},
/**
* Copies the data from the native data transfer to a private cache.
* This function is needed because the data from the native data transfer
* is available only synchronously to the event listener. It is not possible
* to get the data asynchronously, after a timeout, and the {@link CKEDITOR.editor#paste}
* event is fired asynchronously — hence the need for caching the data.
*/
cacheData: function() {
if ( !this.$ ) {
return;
}
var that = this,
i, file;
function getAndSetData( type ) {
type = that._.normalizeType( type );
var data = that.getData( type );
if ( data ) {
that._.data[ type ] = data;
}
}
// Copy data.
if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ) {
if ( this.$.types ) {
for ( i = 0; i < this.$.types.length; i++ ) {
getAndSetData( this.$.types[ i ] );
}
}
} else {
getAndSetData( 'Text' );
getAndSetData( 'URL' );
}
// Copy files references.
file = this._getImageFromClipboard();
if ( ( this.$ && this.$.files ) || file ) {
this._.files = [];
// Edge have empty files property with no length (#13755).
if ( this.$.files && this.$.files.length ) {
for ( i = 0; i < this.$.files.length; i++ ) {
this._.files.push( this.$.files[ i ] );
}
}
// Don't include $.items if both $.files and $.items contains files, because,
// according to spec and browsers behavior, they contain the same files.
if ( this._.files.length === 0 && file ) {
this._.files.push( file );
}
}
},
/**
* Gets the number of files in the dataTransfer object.
*
* @returns {Number} The number of files.
*/
getFilesCount: function() {
if ( this._.files.length ) {
return this._.files.length;
}
if ( this.$ && this.$.files && this.$.files.length ) {
return this.$.files.length;
}
return this._getImageFromClipboard() ? 1 : 0;
},
/**
* Gets the file at the index given.
*
* @param {Number} i Index.
* @returns {File} File instance.
*/
getFile: function( i ) {
if ( this._.files.length ) {
return this._.files[ i ];
}
if ( this.$ && this.$.files && this.$.files.length ) {
return this.$.files[ i ];
}
// File or null if the file was not found.
return i === 0 ? this._getImageFromClipboard() : undefined;
},
/**
* Checks if the data transfer contains any data.
*
* @returns {Boolean} `true` if the object contains no data.
*/
isEmpty: function() {
var typesToCheck = {},
type;
// If dataTransfer contains files it is not empty.
if ( this.getFilesCount() ) {
return false;
}
// Add custom types.
for ( type in this._.data ) {
typesToCheck[ type ] = 1;
}
// Add native types.
if ( this.$ ) {
if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ) {
if ( this.$.types ) {
for ( var i = 0; i < this.$.types.length; i++ ) {
typesToCheck[ this.$.types[ i ] ] = 1;
}
}
} else {
typesToCheck.Text = 1;
typesToCheck.URL = 1;
}
}
// Remove ID.
if ( clipboardIdDataType != 'Text' ) {
typesToCheck[ clipboardIdDataType ] = 0;
}
for ( type in typesToCheck ) {
if ( typesToCheck[ type ] && this.getData( type ) !== '' ) {
return false;
}
}
return true;
},
/**
* When the content of the clipboard is pasted in Chrome, the clipboard data object has an empty `files` property,
* but it is possible to get the file as `items[0].getAsFile();` (#12961).
*
* @private
* @returns {File} File instance or `null` if not found.
*/
_getImageFromClipboard: function() {
var file;
if ( this.$ && this.$.items && this.$.items[ 0 ] ) {
try {
file = this.$.items[ 0 ].getAsFile();
// Duck typing
if ( file && file.type ) {
return file;
}
} catch ( err ) {
// noop
}
}
return undefined;
}
};
} )();
/**
* The default content type that is used when pasted data cannot be clearly recognized as HTML or text.
*
* For example: `'foo'` may come from a plain text editor or a website. It is not possible to recognize the content
* type in this case, so the default type will be used. At the same time it is clear that `'<b>example</b> text'` is
* HTML and its origin is a web page, email or another rich text editor.
*
* **Note:** If content type is text, then styles of the paste context are preserved.
*
* CKEDITOR.config.clipboard_defaultContentType = 'text';
*
* See also the {@link CKEDITOR.editor#paste} event and read more about the integration with clipboard
* in the [Clipboard Deep Dive guide](#!/guide/dev_clipboard).
*
* @since 4.0
* @cfg {'html'/'text'} [clipboard_defaultContentType='html']
* @member CKEDITOR.config
*/
/**
* Fired after the user initiated a paste action, but before the data is inserted into the editor.
* The listeners to this event are able to process the content before its insertion into the document.
*
* Read more about the integration with clipboard in the [Clipboard Deep Dive guide](#!/guide/dev_clipboard).
*
* See also:
*
* * the {@link CKEDITOR.config#pasteFilter} option,
* * the {@link CKEDITOR.editor#drop} event,
* * the {@link CKEDITOR.plugins.clipboard.dataTransfer} class.
*
* @since 3.1
* @event paste
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {String} data.type The type of data in `data.dataValue`. Usually `'html'` or `'text'`, but for listeners
* with a priority smaller than `6` it may also be `'auto'` which means that the content type has not been recognised yet
* (this will be done by the content type sniffer that listens with priority `6`).
* @param {String} data.dataValue HTML to be pasted.
* @param {String} data.method Indicates the data transfer method. It could be drag and drop or copy and paste.
* Possible values: `'drop'`, `'paste'`. Introduced in CKEditor 4.5.
* @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer Facade for the native dataTransfer object
* which provides access to various data types and files, and passes some data between linked events
* (like drag and drop). Introduced in CKEditor 4.5.
* @param {Boolean} [data.dontFilter=false] Whether the {@link CKEDITOR.editor#pasteFilter paste filter} should not
* be applied to data. This option has no effect when `data.type` equals `'text'` which means that for instance
* {@link CKEDITOR.config#forcePasteAsPlainText} has a higher priority. Introduced in CKEditor 4.5.
*/
/**
* Fired before the {@link #paste} event. Allows to preset data type.
*
* **Note:** This event is deprecated. Add a `0` priority listener for the
* {@link #paste} event instead.
*
* @deprecated
* @event beforePaste
* @member CKEDITOR.editor
*/
/**
* Fired after the {@link #paste} event if content was modified. Note that if the paste
* event does not insert any data, the `afterPaste` event will not be fired.
*
* @event afterPaste
* @member CKEDITOR.editor
*/
/**
* Internal event to open the Paste dialog window.
*
* @private
* @event pasteDialog
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param {Function} [data] Callback that will be passed to {@link CKEDITOR.editor#openDialog}.
*/
/**
* Facade for the native `drop` event. Fired when the native `drop` event occurs.
*
* **Note:** To manipulate dropped data, use the {@link CKEDITOR.editor#paste} event.
* Use the `drop` event only to control drag and drop operations (e.g. to prevent the ability to drop some content).
*
* Read more about integration with drag and drop in the [Clipboard Deep Dive guide](#!/guide/dev_clipboard).
*
* See also:
*
* * The {@link CKEDITOR.editor#paste} event,
* * The {@link CKEDITOR.editor#dragstart} and {@link CKEDITOR.editor#dragend} events,
* * The {@link CKEDITOR.plugins.clipboard.dataTransfer} class.
*
* @since 4.5
* @event drop
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {Object} data.$ Native drop event.
* @param {CKEDITOR.dom.node} data.target Drop target.
* @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade.
* @param {CKEDITOR.dom.range} data.dragRange Drag range, lets you manipulate the drag range.
* Note that dragged HTML is saved as `text/html` data on `dragstart` so if you change the drag range
* on drop, dropped HTML will not change. You need to change it manually using
* {@link CKEDITOR.plugins.clipboard.dataTransfer#setData dataTransfer.setData}.
* @param {CKEDITOR.dom.range} data.dropRange Drop range, lets you manipulate the drop range.
*/
/**
* Facade for the native `dragstart` event. Fired when the native `dragstart` event occurs.
*
* This event can be canceled in order to block the drag start operation. It can also be fired to mimic the start of the drag and drop
* operation. For instance, the `widget` plugin uses this option to integrate its custom block widget drag and drop with
* the entire system.
*
* Read more about integration with drag and drop in the [Clipboard Deep Dive guide](#!/guide/dev_clipboard).
*
* See also:
*
* * The {@link CKEDITOR.editor#paste} event,
* * The {@link CKEDITOR.editor#drop} and {@link CKEDITOR.editor#dragend} events,
* * The {@link CKEDITOR.plugins.clipboard.dataTransfer} class.
*
* @since 4.5
* @event dragstart
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {Object} data.$ Native dragstart event.
* @param {CKEDITOR.dom.node} data.target Drag target.
* @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade.
*/
/**
* Facade for the native `dragend` event. Fired when the native `dragend` event occurs.
*
* Read more about integration with drag and drop in the [Clipboard Deep Dive guide](#!/guide/dev_clipboard).
*
* See also:
*
* * The {@link CKEDITOR.editor#paste} event,
* * The {@link CKEDITOR.editor#drop} and {@link CKEDITOR.editor#dragend} events,
* * The {@link CKEDITOR.plugins.clipboard.dataTransfer} class.
*
* @since 4.5
* @event dragend
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {Object} data.$ Native dragend event.
* @param {CKEDITOR.dom.node} data.target Drag target.
* @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade.
*/
/**
* Defines a filter which is applied to external data pasted or dropped into the editor. Possible values are:
*
* * `'plain-text'` – Content will be pasted as a plain text.
* * `'semantic-content'` – Known tags (except `div`, `span`) with all attributes (except
* `style` and `class`) will be kept.
* * `'h1 h2 p div'` – Custom rules compatible with {@link CKEDITOR.filter}.
* * `null` – Content will not be filtered by the paste filter (but it still may be filtered
* by [Advanced Content Filter](#!/guide/dev_advanced_content_filter)). This value can be used to
* disable the paste filter in Chrome and Safari, where this option defaults to `'semantic-content'`.
*
* Example:
*
* config.pasteFilter = 'plain-text';
*
* Custom setting:
*
* config.pasteFilter = 'h1 h2 p ul ol li; img[!src, alt]; a[!href]';
*
* Based on this configuration option, a proper {@link CKEDITOR.filter} instance will be defined and assigned to the editor
* as a {@link CKEDITOR.editor#pasteFilter}. You can tweak the paste filter settings on the fly on this object
* as well as delete or replace it.
*
* var editor = CKEDITOR.replace( 'editor', {
* pasteFilter: 'semantic-content'
* } );
*
* editor.on( 'instanceReady', function() {
* // The result of this will be that all semantic content will be preserved
* // except tables.
* editor.pasteFilter.disallow( 'table' );
* } );
*
* Note that the paste filter is applied only to **external** data. There are three data sources:
*
* * copied and pasted in the same editor (internal),
* * copied from one editor and pasted into another (cross-editor),
* * coming from all other sources like websites, MS Word, etc. (external).
*
* If {@link CKEDITOR.config#allowedContent Advanced Content Filter} is not disabled, then
* it will also be applied to pasted and dropped data. The paste filter job is to "normalize"
* external data which often needs to be handled differently than content produced by the editor.
*
* This setting defaults to `'semantic-content'` in Chrome, Opera and Safari (all Blink and Webkit based browsers)
* due to messy HTML which these browsers keep in the clipboard. In other browsers it defaults to `null`.
*
* @since 4.5
* @cfg {String} [pasteFilter='semantic-content' in Chrome and Safari and `null` in other browsers]
* @member CKEDITOR.config
*/
/**
* {@link CKEDITOR.filter Content filter} which is used when external data is pasted or dropped into the editor
* or a forced paste as plain text occurs.
*
* This object might be used on the fly to define rules for pasted external content.
* This object is available and used if the {@link CKEDITOR.plugins.clipboard clipboard} plugin is enabled and
* {@link CKEDITOR.config#pasteFilter} or {@link CKEDITOR.config#forcePasteAsPlainText} was defined.
*
* To enable the filter:
*
* var editor = CKEDITOR.replace( 'editor', {
* pasteFilter: 'plain-text'
* } );
*
* You can also modify the filter on the fly later on:
*
* editor.pasteFilter = new CKEDITOR.filter( 'p h1 h2; a[!href]' );
*
* Note that the paste filter is only applied to **external** data. There are three data sources:
*
* * copied and pasted in the same editor (internal),
* * copied from one editor and pasted into another (cross-editor),
* * coming from all other sources like websites, MS Word, etc. (external).
*
* If {@link CKEDITOR.config#allowedContent Advanced Content Filter} is not disabled, then
* it will also be applied to pasted and dropped data. The paste filter job is to "normalize"
* external data which often needs to be handled differently than content produced by the editor.
*
* @since 4.5
* @readonly
* @property {CKEDITOR.filter} [pasteFilter]
* @member CKEDITOR.editor
*/