3319 lines
121 KiB
JavaScript
3319 lines
121 KiB
JavaScript
/**
|
|
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
|
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-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'
|
|
// -- 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() {
|
|
var clipboardIdDataType;
|
|
|
|
// Register the plugin.
|
|
CKEDITOR.plugins.add( 'clipboard', {
|
|
requires: 'dialog,notification,toolbar',
|
|
// 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,es-mx,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. https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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 = editor._.nextPasteType || 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 );
|
|
}
|
|
|
|
delete editor._.nextPasteType;
|
|
|
|
// 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 (https://dev.ckeditor.com/ticket/12420).
|
|
// Also, check that editor.toolbox exists, because the toolbar plugin might not be loaded (https://dev.ckeditor.com/ticket/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( function( data ) {
|
|
* if ( data )
|
|
* alert( data.type + ' ' + data.dataValue );
|
|
* } );
|
|
*
|
|
* @member CKEDITOR.editor
|
|
* @param {Function/Object} callbackOrOptions For function, see the `callback` parameter documentation. The object was used before 4.7.0 with the `title` property, to set the paste dialog's title.
|
|
* @param {Function} callback A function that will be executed with the `data` property of the
|
|
* {@link CKEDITOR.editor#event-paste paste event} or `null` if none of the capturing methods succeeded.
|
|
* Since 4.7.0 the `callback` should be provided as a first argument, just like in the example above. This parameter will be removed in
|
|
* an upcoming major release.
|
|
*/
|
|
editor.getClipboardData = function( callbackOrOptions, callback ) {
|
|
var beforePasteNotCanceled = false,
|
|
dataType = 'auto';
|
|
|
|
// Options are optional - args shift.
|
|
if ( !callback ) {
|
|
callback = callbackOrOptions;
|
|
callbackOrOptions = null;
|
|
}
|
|
|
|
// 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 );
|
|
|
|
// 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 );
|
|
|
|
// If command didn't succeed (only IE allows to access clipboard and only if
|
|
// user agrees) invoke callback with null, meaning that paste is not blocked.
|
|
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 ( editor._.forcePasteDialog && beforePasteNotCanceled && editor.fire( 'pasteDialog' ) ) {
|
|
editor.on( 'pasteDialogCommit', onDialogCommit );
|
|
|
|
// 'dialogHide' will be fired after 'pasteDialogCommit'.
|
|
editor.on( 'dialogHide', function( evt ) {
|
|
evt.removeListener();
|
|
evt.data.removeListener( 'pasteDialogCommit', onDialogCommit );
|
|
|
|
// Notify even if user canceled dialog (clicked 'cancel', ESC, etc).
|
|
if ( !evt.data._.committed ) {
|
|
callback( null );
|
|
}
|
|
} );
|
|
} 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();
|
|
|
|
callback( {
|
|
type: dataType,
|
|
dataValue: evt.data.dataValue,
|
|
dataTransfer: evt.data.dataTransfer,
|
|
method: 'paste'
|
|
} );
|
|
}
|
|
};
|
|
|
|
function addButtonsCommands() {
|
|
addButtonCommand( 'Cut', 'cut', createCutCopyCmd( 'cut' ), 10, 1 );
|
|
addButtonCommand( 'Copy', 'copy', createCutCopyCmd( 'copy' ), 20, 4 );
|
|
addButtonCommand( 'Paste', 'paste', createPasteCmd(), 30, 8 );
|
|
|
|
// Force adding touchend handler to paste button (#595).
|
|
if ( !editor._.pasteButtons ) {
|
|
editor._.pasteButtons = [];
|
|
}
|
|
editor._.pasteButtons.push( 'Paste' );
|
|
|
|
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' )
|
|
};
|
|
} );
|
|
|
|
// Adds 'touchend' integration with context menu paste item (#1347).
|
|
var pasteListener = null;
|
|
editor.on( 'menuShow', function() {
|
|
// Remove previous listener.
|
|
if ( pasteListener ) {
|
|
pasteListener.removeListener();
|
|
pasteListener = null;
|
|
}
|
|
|
|
// Attach new 'touchend' listeners to context menu paste items.
|
|
var item = editor.contextMenu.findItemByCommandName( 'paste' );
|
|
if ( item && item.element ) {
|
|
pasteListener = item.element.on( 'touchend', function() {
|
|
editor._.forcePasteDialog = true;
|
|
} );
|
|
}
|
|
} );
|
|
}
|
|
|
|
// Detect if any of paste buttons was touched. In such case we assume that user is using
|
|
// touch device and force displaying paste dialog (#595).
|
|
if ( editor.ui.addButton ) {
|
|
// Waiting for editor instance to be ready seems to be the most reliable way to
|
|
// be sure that paste buttons are already created.
|
|
editor.once( 'instanceReady', function() {
|
|
if ( !editor._.pasteButtons ) {
|
|
return;
|
|
}
|
|
|
|
CKEDITOR.tools.array.forEach( editor._.pasteButtons, function( name ) {
|
|
var pasteButton = editor.ui.get( name );
|
|
// Check if button was not removed by `removeButtons` config.
|
|
if ( pasteButton ) {
|
|
var buttonElement = CKEDITOR.document.getById( pasteButton._.id );
|
|
|
|
buttonElement.on( 'touchend', function() {
|
|
editor._.forcePasteDialog = true;
|
|
} );
|
|
}
|
|
} );
|
|
} );
|
|
}
|
|
}
|
|
|
|
// Add events listeners to editable.
|
|
function addPasteListenersToEditable() {
|
|
var editable = editor.editable();
|
|
|
|
if ( CKEDITOR.plugins.clipboard.isCustomCopyCutSupported ) {
|
|
var initOnCopyCut = function( evt ) {
|
|
// There shouldn't be anything to copy/cut when selection is collapsed (#869).
|
|
if ( editor.getSelection().isCollapsed() ) {
|
|
return;
|
|
}
|
|
|
|
// If user tries to cut in read-only editor, we must prevent default action (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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.
|
|
execIECommand( 'paste' );
|
|
} );
|
|
|
|
// If mainPasteEvent is 'beforePaste' (IE before Edge),
|
|
// dismiss the (wrong) 'beforepaste' event fired on context/toolbar menu open. (https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/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) (https://dev.ckeditor.com/ticket/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*/,
|
|
|
|
/**
|
|
* The default implementation of the paste command.
|
|
*
|
|
* @private
|
|
* @param {CKEDITOR.editor} editor An instance of the editor where the command is being executed.
|
|
* @param {Object/String} data If `data` is a string, then it is considered content that is being pasted.
|
|
* Otherwise it is treated as an object with options.
|
|
* @param {Boolean/String} [data.notification=true] Content for a notification shown after an unsuccessful
|
|
* paste attempt. If `false`, the notification will not be displayed. This parameter was added in 4.7.0.
|
|
* @param {String} [data.type='html'] The type of pasted content. There are two allowed values:
|
|
* * 'html'
|
|
* * 'text'
|
|
* @param {String/Object} data.dataValue Content being pasted. If this parameter is an object, it
|
|
* is supposed to be a `data` property of the {@link CKEDITOR.editor#paste} event.
|
|
* @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer Data transfer instance connected
|
|
* with the current paste action.
|
|
* @member CKEDITOR.editor.commands.paste
|
|
*/
|
|
exec: function( editor, data ) {
|
|
data = typeof data !== 'undefined' && data !== null ? data : {};
|
|
|
|
var cmd = this,
|
|
notification = typeof data.notification !== 'undefined' ? data.notification : true,
|
|
forcedType = data.type,
|
|
keystroke = CKEDITOR.tools.keystrokeToString( editor.lang.common.keyboard,
|
|
editor.getCommandKeystroke( this ) ),
|
|
msg = typeof notification === 'string' ? notification : editor.lang.clipboard.pasteNotification
|
|
.replace( /%1/, '<kbd aria-label="' + keystroke.aria + '">' + keystroke.display + '</kbd>' ),
|
|
pastedContent = typeof data === 'string' ? data : data.dataValue;
|
|
|
|
function callback( data, withBeforePaste ) {
|
|
withBeforePaste = typeof withBeforePaste !== 'undefined' ? withBeforePaste : true;
|
|
|
|
if ( data ) {
|
|
data.method = 'paste';
|
|
|
|
if ( !data.dataTransfer ) {
|
|
data.dataTransfer = clipboard.initPasteDataTransfer();
|
|
}
|
|
|
|
firePasteEvents( editor, data, withBeforePaste );
|
|
} else if ( notification && !editor._.forcePasteDialog ) {
|
|
editor.showNotification( msg, 'info', editor.config.clipboard_notificationDuration );
|
|
}
|
|
|
|
// Reset dialog mode (#595).
|
|
editor._.forcePasteDialog = false;
|
|
|
|
editor.fire( 'afterCommandExec', {
|
|
name: 'paste',
|
|
command: cmd,
|
|
returnValue: !!data
|
|
} );
|
|
}
|
|
|
|
// Force type for the next paste. Do not force if `config.forcePasteAsPlainText` set to true or 'allow-word' (#1013).
|
|
if ( forcedType && editor.config.forcePasteAsPlainText !== true && editor.config.forcePasteAsPlainText !== 'allow-word' ) {
|
|
editor._.nextPasteType = forcedType;
|
|
} else {
|
|
delete editor._.nextPasteType;
|
|
}
|
|
|
|
if ( typeof pastedContent === 'string' ) {
|
|
callback( {
|
|
dataValue: pastedContent
|
|
} );
|
|
} else {
|
|
editor.getClipboardData( callback );
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
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. (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/5730)
|
|
if ( doc.getById( 'cke_pastebin' ) )
|
|
return;
|
|
|
|
var sel = editor.getSelection();
|
|
var bms = sel.createBookmarks();
|
|
|
|
// https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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'. (https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/10644).
|
|
// The same seems to apply to Firefox (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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 https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/8849)
|
|
if ( CKEDITOR.env.ie )
|
|
editable.focus();
|
|
|
|
// IE7: selection must go before removing pastebin. (https://dev.ckeditor.com/ticket/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' ) {
|
|
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();
|
|
|
|
// https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/13011, https://dev.ckeditor.com/ticket/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 ) {
|
|
// Edge requires this handler to have `preventDefault()` regardless of the situation.
|
|
if ( CKEDITOR.env.edge ) {
|
|
evt.data.preventDefault();
|
|
return;
|
|
}
|
|
|
|
var target = evt.data.getTarget();
|
|
|
|
// Prevent reloading page when dragging image on empty document (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/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.version >= 16 ) && !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 || CKEDITOR.env.version >= 16,
|
|
|
|
/**
|
|
* 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',
|
|
|
|
/**
|
|
* Adds a new paste button to the editor.
|
|
*
|
|
* This method should be called for buttons that should display the Paste Dialog fallback in mobile environments.
|
|
* See [the rationale](https://github.com/ckeditor/ckeditor-dev/issues/595#issuecomment-345971174) for more
|
|
* details.
|
|
*
|
|
* @since 4.9.0
|
|
* @param {CKEDITOR.editor} editor The editor instance.
|
|
* @param {String} name Name of the button.
|
|
* @param {Object} definition Definition of the button.
|
|
*/
|
|
addPasteButton: function( editor, name, definition ) {
|
|
if ( !editor.ui.addButton ) {
|
|
return;
|
|
}
|
|
|
|
editor.ui.addButton( name, definition );
|
|
|
|
if ( !editor._.pasteButtons ) {
|
|
editor._.pasteButtons = [];
|
|
}
|
|
editor._.pasteButtons.push( name );
|
|
},
|
|
|
|
/**
|
|
* 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](https://docs.ckeditor.com/ckeditor4/docs/#!/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 (https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/13528, https://bugzilla.mozilla.org/show_bug.cgi?id=1183686).
|
|
if ( CKEDITOR.env.gecko && ( dataTransfer.getData( 'text/html' ) || dataTransfer.getFilesCount() ) ) {
|
|
return true;
|
|
}
|
|
|
|
// Safari fixed clipboard in 10.1 (https://bugs.webkit.org/show_bug.cgi?id=19893) (https://dev.ckeditor.com/ticket/16982).
|
|
// However iOS version still doesn't work well enough (https://bugs.webkit.org/show_bug.cgi?id=19893#c34).
|
|
if ( CKEDITOR.env.safari && CKEDITOR.env.version >= 603 && !CKEDITOR.env.iOS ) {
|
|
return true;
|
|
}
|
|
|
|
// Edge 15 added support for Clipboard API
|
|
// (https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/6515107-clipboard-api), however it is
|
|
// usable for our case starting from Edge 16 (#468).
|
|
if ( CKEDITOR.env.edge && CKEDITOR.env.version >= 16 ) {
|
|
return true;
|
|
}
|
|
|
|
// In older Safari and IE HTML data is not available through the Clipboard API.
|
|
// In older Edge version things are also a bit messy -
|
|
// 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();
|
|
|
|
// https://dev.ckeditor.com/ticket/11123 Firefox needs to listen on document, because otherwise event won't be fired.
|
|
// https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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 && editor.document.$.caretRangeFromPoint( x, y ) ) {
|
|
$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 https://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 );
|
|
|
|
// Set dataTransfer.id only for 'dragstart' event (so for events initializing dataTransfer inside editor) (#962).
|
|
if ( evt.name === 'dragstart' ) {
|
|
dataTransfer.storeId();
|
|
}
|
|
|
|
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 < 16 does not support custom copy/cut, but it has some useful data in the clipboardData (https://dev.ckeditor.com/ticket/13755).
|
|
return new this.dataTransfer( ( CKEDITOR.env.edge && evt && evt.data.$ && evt.data.$.clipboardData ) || null, sourceEditor );
|
|
} else if ( evt && evt.data && evt.data.$ ) {
|
|
var clipboardData = evt.data.$.clipboardData,
|
|
dataTransfer = new this.dataTransfer( clipboardData, sourceEditor );
|
|
|
|
// Set dataTransfer.id only for 'copy'/'cut' events (so for events initializing dataTransfer inside editor) (#962).
|
|
if ( evt.name === 'copy' || evt.name === 'cut' ) {
|
|
dataTransfer.storeId();
|
|
}
|
|
|
|
if ( this.copyCutData && dataTransfer.id == this.copyCutData.id ) {
|
|
dataTransfer = this.copyCutData;
|
|
dataTransfer.$ = 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.
|
|
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: [],
|
|
|
|
// Stores full HTML so it can be accessed asynchronously with `getData( 'text/html', true )`.
|
|
nativeHtmlCache: '',
|
|
|
|
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;
|
|
}
|
|
}
|
|
};
|
|
this._.fallbackDataTransfer = new CKEDITOR.plugins.clipboard.fallbackDataTransfer( this );
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
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.
|
|
* @param {Boolean} [getNative=false] Indicates if the whole, original content of the dataTransfer should be returned.
|
|
* Introduced in CKEditor 4.7.0.
|
|
* @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, getNative ) {
|
|
function isEmpty( data ) {
|
|
return data === undefined || data === null || data === '';
|
|
}
|
|
|
|
function filterUnwantedCharacters( data ) {
|
|
if ( typeof data !== 'string' ) {
|
|
return data;
|
|
}
|
|
|
|
var htmlEnd = data.indexOf( '</html>' );
|
|
|
|
if ( htmlEnd !== -1 ) {
|
|
// Just cut everything after `</html>`, so everything after htmlEnd index + length of `</html>`.
|
|
// Required to workaround bug: https://bugs.chromium.org/p/chromium/issues/detail?id=696978
|
|
return data.substring( 0, htmlEnd + 7 );
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
type = this._.normalizeType( type );
|
|
|
|
var data = type == 'text/html' && getNative ? this._.nativeHtmlCache : this._.data[ type ];
|
|
|
|
if ( isEmpty( data ) ) {
|
|
if ( this._.fallbackDataTransfer.isRequired() ) {
|
|
data = this._.fallbackDataTransfer.getData( type, getNative );
|
|
} else {
|
|
try {
|
|
data = this.$.getData( type ) || '';
|
|
} catch ( e ) {
|
|
data = '';
|
|
}
|
|
}
|
|
|
|
if ( type == 'text/html' && !getNative ) {
|
|
data = this._stripHtml( data );
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
if ( type == 'Text' && CKEDITOR.env.gecko && this.getFilesCount() &&
|
|
data.substring( 0, 7 ) == 'file://' ) {
|
|
data = '';
|
|
}
|
|
|
|
return filterUnwantedCharacters( 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 );
|
|
|
|
if ( type == 'text/html' ) {
|
|
this._.data[ type ] = this._stripHtml( value );
|
|
// If 'text/html' is set manually we also store it in `nativeHtmlCache` without modifications.
|
|
this._.nativeHtmlCache = value;
|
|
} else {
|
|
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. https://dev.ckeditor.com/ticket/13468.
|
|
if ( clipboardIdDataType == 'Text' && type == 'Text' ) {
|
|
this.id = value;
|
|
}
|
|
|
|
if ( this._.fallbackDataTransfer.isRequired() ) {
|
|
this._.fallbackDataTransfer.setData( type, value );
|
|
} else {
|
|
try {
|
|
this.$.setData( type, value );
|
|
} catch ( e ) {}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Stores dataTransfer id in native data transfer object
|
|
* so it can be retrieved by other events.
|
|
*
|
|
* @since 4.8.0
|
|
*/
|
|
storeId: function() {
|
|
if ( clipboardIdDataType !== 'Text' ) {
|
|
this.setData( clipboardIdDataType, this.id );
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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 );
|
|
|
|
// Cache full html.
|
|
if ( type == 'text/html' ) {
|
|
that._.nativeHtmlCache = that.getData( type, true );
|
|
data = that._stripHtml( data );
|
|
}
|
|
|
|
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 (https://dev.ckeditor.com/ticket/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;
|
|
}
|
|
|
|
CKEDITOR.tools.array.forEach( CKEDITOR.tools.objectKeys( this._.data ), function( type ) {
|
|
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();` (https://dev.ckeditor.com/ticket/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;
|
|
},
|
|
|
|
/**
|
|
* This function removes this meta information and returns only the contents of the `<body>` element if found.
|
|
*
|
|
* Various environments use miscellaneous meta tags in HTML clipboard, e.g.
|
|
*
|
|
* * `<meta http-equiv="content-type" content="text/html; charset=utf-8">` at the begging of the HTML data.
|
|
* * Surrounding HTML with `<!--StartFragment-->` and `<!--EndFragment-->` nested within `<html><body>` elements.
|
|
*
|
|
* @private
|
|
* @param {String} html
|
|
* @returns {String}
|
|
*/
|
|
_stripHtml: function( html ) {
|
|
var result = html;
|
|
|
|
// Passed HTML may be empty or null. There is no need to strip such values (#1299).
|
|
if ( result && result.length ) {
|
|
// See https://dev.ckeditor.com/ticket/13583 for more details.
|
|
// Additionally https://dev.ckeditor.com/ticket/16847 adds a flag allowing to get the whole, original content.
|
|
result = result.replace( this._.metaRegExp, '' );
|
|
|
|
// Keep only contents of the <body> element
|
|
var match = this._.bodyRegExp.exec( result );
|
|
if ( match && match.length ) {
|
|
result = match[ 1 ];
|
|
|
|
// Remove also comments.
|
|
result = result.replace( this._.fragmentRegExp, '' );
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Fallback dataTransfer object which is used together with {@link CKEDITOR.plugins.clipboard.dataTransfer}
|
|
* for browsers supporting Clipboard API, but not supporting custom
|
|
* MIME types (Edge 16+, see [ckeditor-dev/issues/#962](https://github.com/ckeditor/ckeditor-dev/issues/962)).
|
|
*
|
|
* @since 4.8.0
|
|
* @class CKEDITOR.plugins.clipboard.fallbackDataTransfer
|
|
* @constructor
|
|
* @param {CKEDITOR.plugins.clipboard.dataTransfer} dataTransfer DataTransfer
|
|
* object which internal cache and
|
|
* {@link CKEDITOR.plugins.clipboard.dataTransfer#$ data transfer} objects will be reused.
|
|
*/
|
|
CKEDITOR.plugins.clipboard.fallbackDataTransfer = function( dataTransfer ) {
|
|
/**
|
|
* DataTransfer object which internal cache and
|
|
* {@link CKEDITOR.plugins.clipboard.dataTransfer#$ data transfer} objects will be modified if needed.
|
|
*
|
|
* @private
|
|
* @property {CKEDITOR.plugins.clipboard.dataTransfer} _dataTransfer
|
|
*/
|
|
this._dataTransfer = dataTransfer;
|
|
|
|
/**
|
|
* A MIME type used for storing custom MIME types.
|
|
*
|
|
* @private
|
|
* @property {String} [_customDataFallbackType='text/html']
|
|
*/
|
|
this._customDataFallbackType = 'text/html';
|
|
};
|
|
|
|
/**
|
|
* True if the environment supports custom MIME types in {@link CKEDITOR.plugins.clipboard.dataTransfer#getData}
|
|
* and {@link CKEDITOR.plugins.clipboard.dataTransfer#setData} methods.
|
|
*
|
|
* Introduced to distinguish between browsers which support only some whitelisted types (like `text/html`, `application/xml`),
|
|
* but do not support custom MIME types (like `cke/id`). When the value of this property equals `null`
|
|
* it means it was not yet initialized.
|
|
*
|
|
* This property should not be accessed directly, use {@link #isRequired} method instead.
|
|
*
|
|
* @private
|
|
* @static
|
|
* @property {Boolean}
|
|
*/
|
|
CKEDITOR.plugins.clipboard.fallbackDataTransfer._isCustomMimeTypeSupported = null;
|
|
|
|
/**
|
|
* Array containing MIME types which are not supported by native `setData`. Those types are
|
|
* recognized by error which is thrown when using native `setData` with a given type
|
|
* (see {@link CKEDITOR.plugins.clipboard.fallbackDataTransfer#_isUnsupportedMimeTypeError}).
|
|
*
|
|
* @private
|
|
* @static
|
|
* @property {String[]}
|
|
*/
|
|
CKEDITOR.plugins.clipboard.fallbackDataTransfer._customTypes = [];
|
|
|
|
CKEDITOR.plugins.clipboard.fallbackDataTransfer.prototype = {
|
|
/**
|
|
* Whether {@link CKEDITOR.plugins.clipboard.fallbackDataTransfer fallbackDataTransfer object} should
|
|
* be used when operating on native `dataTransfer`. If `true` is returned, it means custom MIME types
|
|
* are not supported in the current browser (see {@link #_isCustomMimeTypeSupported}).
|
|
*
|
|
* @returns {Boolean}
|
|
*/
|
|
isRequired: function() {
|
|
var fallbackDataTransfer = CKEDITOR.plugins.clipboard.fallbackDataTransfer,
|
|
nativeDataTransfer = this._dataTransfer.$;
|
|
|
|
if ( fallbackDataTransfer._isCustomMimeTypeSupported === null ) {
|
|
// If there is no `dataTransfer` we cannot detect if fallback is needed.
|
|
// Method returns `false` so regular flow will be applied.
|
|
if ( !nativeDataTransfer ) {
|
|
return false;
|
|
} else {
|
|
var testValue = 'cke test value',
|
|
testType = 'cke/mimetypetest';
|
|
|
|
fallbackDataTransfer._isCustomMimeTypeSupported = false;
|
|
|
|
try {
|
|
nativeDataTransfer.setData( testType, testValue );
|
|
fallbackDataTransfer._isCustomMimeTypeSupported = nativeDataTransfer.getData( testType ) === testValue;
|
|
nativeDataTransfer.clearData( testType );
|
|
} catch ( e ) {}
|
|
}
|
|
}
|
|
return !fallbackDataTransfer._isCustomMimeTypeSupported;
|
|
},
|
|
|
|
/**
|
|
* Returns the data of the given MIME type if stored in a regular way or in a special comment. If given type
|
|
* is the same as {@link #_customDataFallbackType} the whole data without special comment is returned.
|
|
*
|
|
* @param {String} type
|
|
* @param {Boolean} [getNative=false] Indicates if the whole, original content of the dataTransfer should be returned.
|
|
* @returns {String}
|
|
*/
|
|
getData: function( type, getNative ) {
|
|
// As cache is already checked in CKEDITOR.plugins.clipboard.dataTransfer#getData it is skipped
|
|
// here. So the assumption is the given type is not in cache.
|
|
|
|
var nativeData = this._getData( this._customDataFallbackType, true );
|
|
if ( getNative ) {
|
|
return nativeData;
|
|
}
|
|
|
|
var dataComment = this._extractDataComment( nativeData ),
|
|
value = null;
|
|
|
|
// If we are getting the same type which may store custom data we need to extract content only.
|
|
if ( type === this._customDataFallbackType ) {
|
|
value = dataComment.content;
|
|
} else {
|
|
// If we are getting different type we need to check inside data comment if it is stored there.
|
|
if ( dataComment.data && dataComment.data[ type ] ) {
|
|
value = dataComment.data[ type ];
|
|
} else {
|
|
// And then fallback to regular `getData`.
|
|
value = this._getData( type, true );
|
|
}
|
|
}
|
|
|
|
return value !== null ? value : '';
|
|
},
|
|
|
|
/**
|
|
* Sets given data in native `dataTransfer` object. If given MIME type is not supported it uses
|
|
* {@link #_customDataFallbackType} MIME type to save data using special comment format:
|
|
*
|
|
* <!--cke-data:{ type: value }-->
|
|
*
|
|
* It is important to keep in mind that `{ type: value }` object is stringified (using `JSON.stringify`)
|
|
* and encoded (using `encodeURIComponent`).
|
|
*
|
|
* @param {String} type
|
|
* @param {String} value
|
|
* @returns {String} The value which was set.
|
|
*/
|
|
setData: function( type, value ) {
|
|
// In case of fallbackDataTransfer, cache does not reflect native data one-to-one. For example, having
|
|
// types like text/plain, text/html, cke/id will result in cache storing:
|
|
//
|
|
// {
|
|
// text/plain: value1,
|
|
// text/html: value2,
|
|
// cke/id: value3
|
|
// }
|
|
//
|
|
// and native dataTransfer storing:
|
|
//
|
|
// {
|
|
// text/plain: value1,
|
|
// text/html: <!--cke-data:{ cke/id: value3 }-->value2
|
|
// }
|
|
//
|
|
// This way, accessing cache will always return proper value for a given type without a need for further processing.
|
|
// Cache is already set in CKEDITOR.plugins.clipboard.dataTransfer#setData so it is skipped here.
|
|
var isFallbackDataType = type === this._customDataFallbackType;
|
|
|
|
if ( isFallbackDataType ) {
|
|
value = this._applyDataComment( value, this._getFallbackTypeData() );
|
|
}
|
|
|
|
var data = value,
|
|
nativeDataTransfer = this._dataTransfer.$;
|
|
|
|
try {
|
|
nativeDataTransfer.setData( type, data );
|
|
|
|
if ( isFallbackDataType ) {
|
|
// If fallback type used, the native data is different so we overwrite `nativeHtmlCache` here.
|
|
this._dataTransfer._.nativeHtmlCache = data;
|
|
}
|
|
} catch ( e ) {
|
|
if ( this._isUnsupportedMimeTypeError( e ) ) {
|
|
var fallbackDataTransfer = CKEDITOR.plugins.clipboard.fallbackDataTransfer;
|
|
|
|
if ( CKEDITOR.tools.indexOf( fallbackDataTransfer._customTypes, type ) === -1 ) {
|
|
fallbackDataTransfer._customTypes.push( type );
|
|
}
|
|
|
|
var fallbackTypeContent = this._getFallbackTypeContent(),
|
|
fallbackTypeData = this._getFallbackTypeData();
|
|
|
|
fallbackTypeData[ type ] = data;
|
|
|
|
try {
|
|
data = this._applyDataComment( fallbackTypeContent, fallbackTypeData );
|
|
nativeDataTransfer.setData( this._customDataFallbackType, data );
|
|
// Again, fallback type was changed, so we need to refresh the cache.
|
|
this._dataTransfer._.nativeHtmlCache = data;
|
|
} catch ( e ) {
|
|
data = '';
|
|
// Some dev logger should be added here.
|
|
}
|
|
}
|
|
}
|
|
|
|
return data;
|
|
},
|
|
|
|
/**
|
|
* Native getData wrapper.
|
|
*
|
|
* @private
|
|
* @param {String} type
|
|
* @param {Boolean} [skipCache=false]
|
|
* @returns {String|null}
|
|
*/
|
|
_getData: function( type, skipCache ) {
|
|
var cache = this._dataTransfer._.data;
|
|
|
|
if ( !skipCache && cache[ type ] ) {
|
|
return cache[ type ];
|
|
} else {
|
|
try {
|
|
return this._dataTransfer.$.getData( type );
|
|
} catch ( e ) {
|
|
return null;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns content stored in {@link #\_customDataFallbackType}. Content is always first retrieved
|
|
* from {@link #_dataTransfer} cache and then from native `dataTransfer` object.
|
|
*
|
|
* @private
|
|
* @returns {String}
|
|
*/
|
|
_getFallbackTypeContent: function() {
|
|
var fallbackTypeContent = this._dataTransfer._.data[ this._customDataFallbackType ];
|
|
|
|
if ( !fallbackTypeContent ) {
|
|
fallbackTypeContent = this._extractDataComment( this._getData( this._customDataFallbackType, true ) ).content;
|
|
}
|
|
return fallbackTypeContent;
|
|
},
|
|
|
|
/**
|
|
* Returns custom data stored in {@link #\_customDataFallbackType}. Custom data is always first retrieved
|
|
* from {@link #_dataTransfer} cache and then from native `dataTransfer` object.
|
|
*
|
|
* @private
|
|
* @returns {Object}
|
|
*/
|
|
_getFallbackTypeData: function() {
|
|
var fallbackTypes = CKEDITOR.plugins.clipboard.fallbackDataTransfer._customTypes,
|
|
fallbackTypeData = this._extractDataComment( this._getData( this._customDataFallbackType, true ) ).data || {},
|
|
cache = this._dataTransfer._.data;
|
|
|
|
CKEDITOR.tools.array.forEach( fallbackTypes, function( type ) {
|
|
if ( cache[ type ] !== undefined ) {
|
|
fallbackTypeData[ type ] = cache[ type ];
|
|
|
|
} else if ( fallbackTypeData[ type ] !== undefined ) {
|
|
fallbackTypeData[ type ] = fallbackTypeData[ type ];
|
|
}
|
|
}, this );
|
|
|
|
return fallbackTypeData;
|
|
},
|
|
|
|
/**
|
|
* Whether provided error means that unsupported MIME type was used when calling native `dataTransfer.setData` method.
|
|
*
|
|
* @private
|
|
* @param {Error} error
|
|
* @returns {Boolean}
|
|
*/
|
|
_isUnsupportedMimeTypeError: function( error ) {
|
|
return error.message && error.message.search( /element not found/gi ) !== -1;
|
|
},
|
|
|
|
/**
|
|
* Extracts `cke-data` comment from the given content.
|
|
*
|
|
* @private
|
|
* @param {String} content
|
|
* @returns {Object} Returns an object containing extracted data as `data`
|
|
* and content (without `cke-data` comment) as `content`.
|
|
* @returns {Object|null} return.data Object containing `MIME type : value` pairs
|
|
* or null if `cke-data` comment is not present.
|
|
* @returns {String} return.content Regular content without `cke-data` comment.
|
|
*/
|
|
_extractDataComment: function( content ) {
|
|
var result = {
|
|
data: null,
|
|
content: content || ''
|
|
};
|
|
|
|
// At least 17 characters length: <!--cke-data:-->.
|
|
if ( content && content.length > 16 ) {
|
|
var matcher = /<!--cke-data:(.*?)-->/g,
|
|
matches;
|
|
|
|
matches = matcher.exec( content );
|
|
if ( matches && matches[ 1 ] ) {
|
|
result.data = JSON.parse( decodeURIComponent( matches[ 1 ] ) );
|
|
result.content = content.replace( matches[ 0 ], '' );
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Creates `cke-data` comment containing stringified and encoded data object which is prepended to a given content.
|
|
*
|
|
* @private
|
|
* @param {String} content
|
|
* @param {Object} data
|
|
* @returns {String}
|
|
*/
|
|
_applyDataComment: function( content, data ) {
|
|
var customData = '';
|
|
if ( data && CKEDITOR.tools.objectKeys( data ).length ) {
|
|
customData = '<!--cke-data:' + encodeURIComponent( JSON.stringify( data ) ) + '-->';
|
|
}
|
|
return customData + ( content && content.length ? content : '' );
|
|
}
|
|
};
|
|
|
|
} )();
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
*
|
|
* This event was not available in 4.7.0-4.8.0 versions.
|
|
*
|
|
* @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
|
|
*/
|
|
|
|
/**
|
|
* Duration of the notification displayed after pasting was blocked by the browser.
|
|
*
|
|
* @since 4.7.0
|
|
* @cfg {Number} [clipboard_notificationDuration=10000]
|
|
* @member CKEDITOR.config
|
|
*/
|
|
CKEDITOR.config.clipboard_notificationDuration = 10000;
|