X-Git-Url: https://git.immae.eu/?p=perso%2FImmae%2FProjets%2Fpackagist%2Fpiedsjaloux-ckeditor-component.git;a=blobdiff_plain;f=sources%2Fplugins%2Fclipboard%2Fplugin.js;fp=sources%2Fplugins%2Fclipboard%2Fplugin.js;h=433f547366860502fcc91e013e45148b955a4c5c;hp=0000000000000000000000000000000000000000;hb=317f8f8f0651488f226b5280a8f036c7c135c639;hpb=1096cdefb1c9a3f3c4ca6807e272da6c92e5ed9c
diff --git a/sources/plugins/clipboard/plugin.js b/sources/plugins/clipboard/plugin.js
new file mode 100644
index 0000000..433f547
--- /dev/null
+++ b/sources/plugins/clipboard/plugin.js
@@ -0,0 +1,2780 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * @ignore
+ * File overview: Clipboard support.
+ */
+
+//
+// COPY & PASTE EXECUTION FLOWS:
+// -- CTRL+C
+// * if ( isCustomCopyCutSupported )
+// * dataTransfer.setData( 'text/html', getSelectedHtml )
+// * else
+// * browser's default behavior
+// -- CTRL+X
+// * listen onKey (onkeydown)
+// * fire 'saveSnapshot' on editor
+// * if ( isCustomCopyCutSupported )
+// * dataTransfer.setData( 'text/html', getSelectedHtml )
+// * extractSelectedHtml // remove selected contents
+// * else
+// * browser's default behavior
+// * deferred second 'saveSnapshot' event
+// -- CTRL+V
+// * listen onKey (onkeydown)
+// * simulate 'beforepaste' for non-IEs on editable
+// * listen 'onpaste' on editable ('onbeforepaste' for IE)
+// * fire 'beforePaste' on editor
+// * if ( !canceled && ( htmlInDataTransfer || !external paste) && dataTransfer is not empty ) getClipboardDataByPastebin
+// * fire 'paste' on editor
+// * !canceled && fire 'afterPaste' on editor
+// -- Copy command
+// * tryToCutCopy
+// * execCommand
+// * !success && notification
+// -- Cut command
+// * fixCut
+// * tryToCutCopy
+// * execCommand
+// * !success && notification
+// -- Paste command
+// * fire 'paste' on editable ('beforepaste' for IE)
+// * !canceled && execCommand 'paste'
+// -- Paste from native context menu & menubar
+// (Fx & Webkits are handled in 'paste' default listener.
+// Opera cannot be handled at all because it doesn't fire any events
+// Special treatment is needed for IE, for which is this part of doc)
+// * listen 'onpaste'
+// * cancel native event
+// * fire 'beforePaste' on editor
+// * if ( !canceled && ( htmlInDataTransfer || !external paste) && dataTransfer is not empty ) getClipboardDataByPastebin
+// * execIECommand( 'paste' ) -> this fires another 'paste' event, so cancel it
+// * fire 'paste' on editor
+// * !canceled && fire 'afterPaste' on editor
+//
+//
+// PASTE EVENT - PREPROCESSING:
+// -- Possible dataValue types: auto, text, html.
+// -- Possible dataValue contents:
+// * text (possible \n\r)
+// * htmlified text (text + br,div,p - no presentational markup & attrs - depends on browser)
+// * html
+// -- Possible flags:
+// * htmlified - if true then content is a HTML even if no markup inside. This flag is set
+// for content from editable pastebins, because they 'htmlify' pasted content.
+//
+// -- Type: auto:
+// * content: htmlified text -> filter, unify text markup (brs, ps, divs), set type: text
+// * content: html -> filter, set type: html
+// -- Type: text:
+// * content: htmlified text -> filter, unify text markup
+// * content: html -> filter, strip presentational markup, unify text markup
+// -- Type: html:
+// * content: htmlified text -> filter, unify text markup
+// * content: html -> filter
+//
+// -- Phases:
+// * if dataValue is empty copy data from dataTransfer to dataValue (priority 1)
+// * filtering (priorities 3-5) - e.g. pastefromword filters
+// * content type sniffing (priority 6)
+// * markup transformations for text (priority 6)
+//
+// DRAG & DROP EXECUTION FLOWS:
+// -- Drag
+// * save to the global object:
+// * drag timestamp (with 'cke-' prefix),
+// * selected html,
+// * drag range,
+// * editor instance.
+// * put drag timestamp into event.dataTransfer.text
+// -- Drop
+// * if events text == saved timestamp && editor == saved editor
+// internal drag & drop occurred
+// * getRangeAtDropPosition
+// * create bookmarks for drag and drop ranges starting from the end of the document
+// * dragRange.deleteContents()
+// * fire 'paste' with saved html and drop range
+// * if events text == saved timestamp && editor != saved editor
+// cross editor drag & drop occurred
+// * getRangeAtDropPosition
+// * fire 'paste' with saved html
+// * dragRange.deleteContents()
+// * FF: refreshCursor on afterPaste
+// * if events text != saved timestamp
+// drop form external source occurred
+// * getRangeAtDropPosition
+// * if event contains html data then fire 'paste' with html
+// * else if event contains text data then fire 'paste' with encoded text
+// * FF: refreshCursor on afterPaste
+
+'use strict';
+
+( function() {
+ // Register the plugin.
+ CKEDITOR.plugins.add( 'clipboard', {
+ requires: '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 );
+
+ // 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. http://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 = '';
+ 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>/gi, ' ' );
+
+ // Strip around white-spaces when not in forced 'html' content type.
+ // This spans are created only when pasting plain text into Webkit,
+ // but for safety reasons remove them always.
+ if ( evt.data.type != 'html' ) {
+ data = data.replace( /]*>([^<]*)<\/span>/gi, function( all, spaces ) {
+ // Replace tabs with 4 spaces like Fx does.
+ return spaces.replace( /\t/g, ' ' );
+ } );
+ }
+
+ // This br is produced only when copying & pasting HTML content.
+ if ( data.indexOf( ' -> (br.cke-pasted-remove will be removed later)
+ data = data.replace( /^ (?: |\r\n)?<(\w+)/g, function( match, elementName ) {
+ if ( elementName.toLowerCase() in blockElements ) {
+ evt.data.preSniffing = 'html'; // Mark as not a text.
+ return '<' + elementName;
+ }
+ return match;
+ } );
+ } else if ( CKEDITOR.env.webkit ) {
+ //
' ) > -1 ) {
+ evt.data.startsWithEOL = 1;
+ evt.data.preSniffing = 'html'; // Mark as not text.
+ data = data.replace( /
/, '' );
+ }
+
+ // Remove all other classes.
+ data = data.replace( /(<[^>]+) class="Apple-[^"]*"/gi, '$1' );
+ }
+
+ // Strip editable that was copied from inside. (http://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. (http://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
(added on FF).
+ if ( editable_wrapper )
+ data = editable_wrapper.getHtml().replace( /
$/i, '' );
+ }
+
+ if ( CKEDITOR.env.ie ) {
+ //
- paragraphs can be separated by new \r\n ).
+ if ( !data.match( /^([^<]|
)*$/gi ) && !data.match( /^(
([^<]|
)*<\/p>|(\r\n))*$/gi ) )
+ return 'html';
+ } else if ( CKEDITOR.env.gecko ) {
+ // Text or
.
+ if ( !data.match( /^([^<]|
)*$/gi ) )
+ return 'html';
+ } else {
+ return 'html';
+ }
+
+ return 'htmlifiedtext';
+ }
+
+ // This function transforms what browsers produce when
+ // pasting plain text into editable element (see clipboard/paste.html TCs
+ // for more info) into correct HTML (similar to that produced by text2Html).
+ function htmlifiedTextHtmlification( config, data ) {
+ function repeatParagraphs( repeats ) {
+ // Repeat blocks floor((n+1)/2) times.
+ // Even number of repeats - add
at the beginning of last
. + return CKEDITOR.tools.repeat( '
', ~~( repeats / 2 ) ) + ( repeats % 2 == 1 ? '
' : '' );
+ }
+
+ // Replace adjacent white-spaces (EOLs too - Fx sometimes keeps them) with one space.
+ data = data.replace( /\s+/g, ' ' )
+ // Remove spaces from between tags.
+ .replace( /> +<' )
+ // Normalize XHTML syntax and upper cased
tags.
+ .replace( /
/gi, '
' );
+
+ // IE - lower cased tags.
+ data = data.replace( /<\/?[A-Z]+>/g, function( match ) {
+ return match.toLowerCase();
+ } );
+
+ // Don't touch single lines (no
) - nothing to do here.
+ if ( data.match( /^[^<]$/ ) )
+ return data;
+
+ // Webkit.
+ if ( CKEDITOR.env.webkit && data.indexOf( '
' + data.replace( /(
' + data.replace( /(
){2,}/g, function( match ) {
+ return repeatParagraphs( match.length / 4 );
+ } ) + '
element completely, because it's a basic structural element, + // so it tries to replace it with an element created based on the active enter mode, eventually doing nothing. + // + // Now you can sleep well. + return filters.plainText || ( filters.plainText = new CKEDITOR.filter( 'br' ) ); + } else if ( type == 'semantic-content' ) { + return filters.semanticContent || ( filters.semanticContent = createSemanticContentFilter() ); + } else if ( type ) { + // Create filter based on rules (string or object). + return new CKEDITOR.filter( type ); + } + + return null; + } + }; + } + + function filterContent( editor, data, filter ) { + var fragment = CKEDITOR.htmlParser.fragment.fromHtml( data ), + writer = new CKEDITOR.htmlParser.basicWriter(); + + filter.applyTo( fragment, true, false, editor.activeEnterMode ); + fragment.writeHtml( writer ); + + return writer.getHtml(); + } + + function switchEnterMode( config, data ) { + if ( config.enterMode == CKEDITOR.ENTER_BR ) { + data = data.replace( /(<\/p>
)+/g, function( match ) {
+ return CKEDITOR.tools.repeat( '
', match.length / 7 * 2 );
+ } ).replace( /<\/?p>/g, '' );
+ } else if ( config.enterMode == CKEDITOR.ENTER_DIV ) {
+ data = data.replace( /<(\/)?p>/g, '<$1div>' );
+ }
+
+ return data;
+ }
+
+ function preventDefaultSetDropEffectToNone( evt ) {
+ evt.data.preventDefault();
+ evt.data.$.dataTransfer.dropEffect = 'none';
+ }
+
+ function initDragDrop( editor ) {
+ var clipboard = CKEDITOR.plugins.clipboard;
+
+ editor.on( 'contentDom', function() {
+ var editable = editor.editable(),
+ dropTarget = CKEDITOR.plugins.clipboard.getDropTarget( editor ),
+ top = editor.ui.space( 'top' ),
+ bottom = editor.ui.space( 'bottom' );
+
+ // -------------- DRAGOVER TOP & BOTTOM --------------
+
+ // Not allowing dragging on toolbar and bottom (http://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. (http://dev.ckeditor.com/ticket/13011, http://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 (http://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. (http://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 (http://dev.ckeditor.com/ticket/13015).
+ // The tag isn't editable (body is), but we want to allow drop on it
+ // (so it is possible to drop below editor contents).
+ if ( readOnly && !( target.type == CKEDITOR.NODE_ELEMENT && target.is( 'html' ) ) ) {
+ return;
+ }
+
+ // Getting drop position is one of the most complex parts.
+ var dropRange = clipboard.getRangeAtDropPosition( evt, editor ),
+ dragRange = clipboard.dragRange;
+
+ // Do nothing if it was not possible to get drop range.
+ if ( !dropRange ) {
+ return;
+ }
+
+ // Fire drop.
+ fireDragEvent( evt, dragRange, dropRange );
+ }, null, null, 9999 );
+
+ // Create dataTransfer or get it, if it was created before.
+ editable.attachListener( editor, 'drop', clipboard.initDragDataTransfer, clipboard, null, 1 );
+
+ // Execute drop action, fire paste.
+ editable.attachListener( editor, 'drop', function( evt ) {
+ var data = evt.data;
+
+ if ( !data ) {
+ return;
+ }
+
+ // Let user modify drag and drop range.
+ var dropRange = data.dropRange,
+ dragRange = data.dragRange,
+ dataTransfer = data.dataTransfer;
+
+ if ( dataTransfer.getTransferType( editor ) == CKEDITOR.DATA_TRANSFER_INTERNAL ) {
+ // Execute drop with a timeout because otherwise selection, after drop,
+ // on IE is in the drag position, instead of drop position.
+ setTimeout( function() {
+ clipboard.internalDrop( dragRange, dropRange, dataTransfer, editor );
+ }, 0 );
+ } else if ( dataTransfer.getTransferType( editor ) == CKEDITOR.DATA_TRANSFER_CROSS_EDITORS ) {
+ crossEditorDrop( dragRange, dropRange, dataTransfer );
+ } else {
+ externalDrop( dropRange, dataTransfer );
+ }
+ }, null, null, 9999 );
+
+ // Cross editor drag and drop (drag in one Editor and drop in the other).
+ function crossEditorDrop( dragRange, dropRange, dataTransfer ) {
+ // Paste event should be fired before delete contents because otherwise
+ // Chrome have a problem with drop range (Chrome split the drop
+ // range container so the offset is bigger then container length).
+ dropRange.select();
+ firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop' }, 1 );
+
+ // Remove dragged content and make a snapshot.
+ dataTransfer.sourceEditor.fire( 'saveSnapshot' );
+
+ dataTransfer.sourceEditor.editable().extractHtmlFromRange( dragRange );
+
+ // Make some selection before saving snapshot, otherwise error will be thrown, because
+ // there will be no valid selection after content is removed.
+ dataTransfer.sourceEditor.getSelection().selectRanges( [ dragRange ] );
+ dataTransfer.sourceEditor.fire( 'saveSnapshot' );
+ }
+
+ // Drop from external source.
+ function externalDrop( dropRange, dataTransfer ) {
+ // Paste content into the drop position.
+ dropRange.select();
+
+ firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop' }, 1 );
+
+ // Usually we reset DataTranfer on dragend,
+ // but dragend is called on the same element as dragstart
+ // so it will not be called on on external drop.
+ clipboard.resetDragDataTransfer();
+ }
+
+ // Fire drag/drop events (dragstart, dragend, drop).
+ function fireDragEvent( evt, dragRange, dropRange ) {
+ var eventData = {
+ $: evt.data.$,
+ target: evt.data.getTarget()
+ };
+
+ if ( dragRange ) {
+ eventData.dragRange = dragRange;
+ }
+ if ( dropRange ) {
+ eventData.dropRange = dropRange;
+ }
+
+ if ( editor.fire( evt.name, eventData ) === false ) {
+ evt.data.preventDefault();
+ }
+ }
+
+ function getContainerChildCount( container ) {
+ if ( container.type != CKEDITOR.NODE_ELEMENT ) {
+ container = container.getParent();
+ }
+
+ return container.getChildCount();
+ }
+ } );
+ }
+
+ /**
+ * @singleton
+ * @class CKEDITOR.plugins.clipboard
+ */
+ CKEDITOR.plugins.clipboard = {
+ /**
+ * True if the environment allows to set data on copy or cut manually. This value is false in IE, because this browser
+ * shows the security dialog window when the script tries to set clipboard data and on iOS, because custom data is
+ * not saved to clipboard there.
+ *
+ * @since 4.5
+ * @readonly
+ * @property {Boolean}
+ */
+ isCustomCopyCutSupported: !CKEDITOR.env.ie && !CKEDITOR.env.iOS,
+
+ /**
+ * True if the environment supports MIME types and custom data types in dataTransfer/cliboardData getData/setData methods.
+ *
+ * @since 4.5
+ * @readonly
+ * @property {Boolean}
+ */
+ isCustomDataTypesSupported: !CKEDITOR.env.ie,
+
+ /**
+ * True if the environment supports File API.
+ *
+ * @since 4.5
+ * @readonly
+ * @property {Boolean}
+ */
+ isFileApiSupported: !CKEDITOR.env.ie || CKEDITOR.env.version > 9,
+
+ /**
+ * Main native paste event editable should listen to.
+ *
+ * **Note:** Safari does not like the {@link CKEDITOR.editor#beforePaste} event — it sometimes does not
+ * handle Ctrl+C properly. This is probably caused by some race condition between events.
+ * Chrome, Firefox and Edge work well with both events, so it is better to use {@link CKEDITOR.editor#paste}
+ * which will handle pasting from e.g. browsers' menu bars.
+ * IE7/8 does not like the {@link CKEDITOR.editor#paste} event for which it is throwing random errors.
+ *
+ * @since 4.5
+ * @readonly
+ * @property {String}
+ */
+ mainPasteEvent: ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) ? 'beforepaste' : 'paste',
+
+ /**
+ * Returns `true` if it is expected that a browser provides HTML data through the Clipboard API.
+ * If not, this method returns `false` and as a result CKEditor will use the paste bin. Read more in
+ * the [Clipboard Integration](http://docs.ckeditor.com/#!/guide/dev_clipboard-section-clipboard-api) guide.
+ *
+ * @since 4.5.2
+ * @returns {Boolean}
+ */
+ canClipboardApiBeTrusted: function( dataTransfer, editor ) {
+ // If it's an internal or cross-editor data transfer, then it means that custom cut/copy/paste support works
+ // and that the data were put manually on the data transfer so we can be sure that it's available.
+ if ( dataTransfer.getTransferType( editor ) != CKEDITOR.DATA_TRANSFER_EXTERNAL ) {
+ return true;
+ }
+
+ // In Chrome we can trust Clipboard API, with the exception of Chrome on Android (in both - mobile and desktop modes), where
+ // clipboard API is not available so we need to check it (http://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 (http://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) (http://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;
+ }
+
+ // In older Safari and IE HTML data is not available though the Clipboard API.
+ // In Edge things are a bit messy at the moment -
+ // https://connect.microsoft.com/IE/feedback/details/1572456/edge-clipboard-api-text-html-content-messed-up-in-event-clipboarddata
+ // It is safer to use the paste bin in unknown cases.
+ return false;
+ },
+
+ /**
+ * Returns the element that should be used as the target for the drop event.
+ *
+ * @since 4.5
+ * @param {CKEDITOR.editor} editor The editor instance.
+ * @returns {CKEDITOR.dom.domObject} the element that should be used as the target for the drop event.
+ */
+ getDropTarget: function( editor ) {
+ var editable = editor.editable();
+
+ // http://dev.ckeditor.com/ticket/11123 Firefox needs to listen on document, because otherwise event won't be fired.
+ // http://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. (http://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 http://dev.ckeditor.com/ticket/13472#comment:6.
+ else if ( CKEDITOR.env.ie && CKEDITOR.env.version > 8 && defaultRange && editor.editable().hasFocus ) {
+ // On IE 9+ range by default is where we expected it.
+ // defaultRange may be undefined if dragover was canceled (file drop).
+ return defaultRange;
+ }
+ // IE 8 and all IEs if !defaultRange or external DnD.
+ else if ( document.body.createTextRange ) {
+ // To use this method we need a focus (which may be somewhere else in case of external drop).
+ editor.focus();
+
+ $range = editor.document.getBody().$.createTextRange();
+ try {
+ var sucess = false;
+
+ // If user drop between text line IEs moveToPoint throws exception:
+ //
+ // Lorem ipsum pulvinar purus et euismod
+ //
+ // dolor sit amet,| consectetur adipiscing
+ // *
+ // vestibulum tincidunt augue eget tempus.
+ //
+ // * - drop position
+ // | - expected cursor position
+ //
+ // So we try to call moveToPoint with +-1px up to +-20px above or
+ // below original drop position to find nearest good drop position.
+ for ( var i = 0; i < 20 && !sucess; i++ ) {
+ if ( !sucess ) {
+ try {
+ $range.moveToPoint( x, y - i );
+ sucess = true;
+ } catch ( err ) {
+ }
+ }
+ if ( !sucess ) {
+ try {
+ $range.moveToPoint( x, y + i );
+ sucess = true;
+ } catch ( err ) {
+ }
+ }
+ }
+
+ if ( sucess ) {
+ var id = 'cke-temp-' + ( new Date() ).getTime();
+ $range.pasteHTML( '\u200b' );
+
+ var span = editor.document.getById( id );
+ range.moveToPosition( span, CKEDITOR.POSITION_BEFORE_START );
+ span.remove();
+ } else {
+ // If the fist method does not succeed we might be next to
+ // the short element (like header):
+ //
+ // Lorem ipsum pulvinar purus et euismod.
+ //
+ //
+ // SOME HEADER| *
+ //
+ //
+ // vestibulum tincidunt augue eget tempus.
+ //
+ // * - drop position
+ // | - expected cursor position
+ //
+ // In such situation elementFromPoint returns proper element. Using getClientRect
+ // it is possible to check if the cursor should be at the beginning or at the end
+ // of paragraph.
+ var $element = editor.document.$.elementFromPoint( x, y ),
+ element = new CKEDITOR.dom.element( $element ),
+ rect;
+
+ if ( !element.equals( editor.editable() ) && element.getName() != 'html' ) {
+ rect = element.getClientRect();
+
+ if ( x < rect.left ) {
+ range.setStartAt( element, CKEDITOR.POSITION_AFTER_START );
+ range.collapse( true );
+ } else {
+ range.setStartAt( element, CKEDITOR.POSITION_BEFORE_END );
+ range.collapse( true );
+ }
+ }
+ // If drop happens on no element elementFromPoint returns html or body.
+ //
+ // * |Lorem ipsum pulvinar purus et euismod.
+ //
+ // vestibulum tincidunt augue eget tempus.
+ //
+ // * - drop position
+ // | - expected cursor position
+ //
+ // In such case we can try to use default selection. If startContainer is not
+ // 'editable' element it is probably proper selection.
+ else if ( defaultRange && defaultRange.startContainer &&
+ !defaultRange.startContainer.equals( editor.editable() ) ) {
+ return defaultRange;
+
+ // Otherwise we can not find any drop position and we have to return null
+ // and cancel drop event.
+ } else {
+ return null;
+ }
+
+ }
+ } catch ( err ) {
+ return null;
+ }
+ } else {
+ return null;
+ }
+
+ return range;
+ },
+
+ /**
+ * This function tries to link the `evt.data.dataTransfer` property of the {@link CKEDITOR.editor#dragstart},
+ * {@link CKEDITOR.editor#dragend} and {@link CKEDITOR.editor#drop} events to a single
+ * {@link CKEDITOR.plugins.clipboard.dataTransfer} object.
+ *
+ * This method is automatically used by the core of the drag and drop functionality and
+ * usually does not have to be called manually when using the drag and drop events.
+ *
+ * This method behaves differently depending on whether the drag and drop events were fired
+ * artificially (to represent a non-native drag and drop) or whether they were caused by the native drag and drop.
+ *
+ * If the native event is not available, then it will create a new {@link CKEDITOR.plugins.clipboard.dataTransfer}
+ * instance (if it does not exist already) and will link it to this and all following event objects until
+ * the {@link #resetDragDataTransfer} method is called. It means that all three drag and drop events must be fired
+ * in order to ensure that the data transfer is bound correctly.
+ *
+ * If the native event is available, then the {@link CKEDITOR.plugins.clipboard.dataTransfer} is identified
+ * by its ID and a new instance is assigned to the `evt.data.dataTransfer` only if the ID changed or
+ * the {@link #resetDragDataTransfer} method was called.
+ *
+ * @since 4.5
+ * @param {CKEDITOR.dom.event} [evt] A drop event object.
+ * @param {CKEDITOR.editor} [sourceEditor] The source editor instance.
+ */
+ initDragDataTransfer: function( evt, sourceEditor ) {
+ // Create a new dataTransfer object based on the drop event.
+ // If this event was used on dragstart to create dataTransfer
+ // both dataTransfer objects will have the same id.
+ var nativeDataTransfer = evt.data.$ ? evt.data.$.dataTransfer : null,
+ dataTransfer = new this.dataTransfer( nativeDataTransfer, sourceEditor );
+
+ if ( !nativeDataTransfer ) {
+ // No native event.
+ if ( this.dragData ) {
+ dataTransfer = this.dragData;
+ } else {
+ this.dragData = dataTransfer;
+ }
+ } else {
+ // Native event. If there is the same id we will replace dataTransfer with the one
+ // created on drag, because it contains drag editor, drag content and so on.
+ // Otherwise (in case of drag from external source) we save new object to
+ // the global clipboard.dragData.
+ if ( this.dragData && dataTransfer.id == this.dragData.id ) {
+ dataTransfer = this.dragData;
+ } else {
+ this.dragData = dataTransfer;
+ }
+ }
+
+ evt.data.dataTransfer = dataTransfer;
+ },
+
+ /**
+ * Removes the global {@link #dragData} so the next call to {@link #initDragDataTransfer}
+ * always creates a new instance of {@link CKEDITOR.plugins.clipboard.dataTransfer}.
+ *
+ * @since 4.5
+ */
+ resetDragDataTransfer: function() {
+ this.dragData = null;
+ },
+
+ /**
+ * Global object storing the data transfer of the current drag and drop operation.
+ * Do not use it directly, use {@link #initDragDataTransfer} and {@link #resetDragDataTransfer}.
+ *
+ * Note: This object is global (meaning that it is not related to a single editor instance)
+ * in order to handle drag and drop from one editor into another.
+ *
+ * @since 4.5
+ * @private
+ * @property {CKEDITOR.plugins.clipboard.dataTransfer} dragData
+ */
+
+ /**
+ * Range object to save the drag range and remove its content after the drop.
+ *
+ * @since 4.5
+ * @private
+ * @property {CKEDITOR.dom.range} dragRange
+ */
+
+ /**
+ * Initializes and links data transfer objects based on the paste event. If the data
+ * transfer object was already initialized on this event, the function will
+ * return that object. In IE it is not possible to link copy/cut and paste events
+ * so the method always returns a new object. The same happens if there is no paste event
+ * passed to the method.
+ *
+ * @since 4.5
+ * @param {CKEDITOR.dom.event} [evt] A paste event object.
+ * @param {CKEDITOR.editor} [sourceEditor] The source editor instance.
+ * @returns {CKEDITOR.plugins.clipboard.dataTransfer} The data transfer object.
+ */
+ initPasteDataTransfer: function( evt, sourceEditor ) {
+ if ( !this.isCustomCopyCutSupported ) {
+ // Edge does not support custom copy/cut, but it have some useful data in the clipboardData (http://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 dataTransfer = new this.dataTransfer( evt.data.$.clipboardData, sourceEditor );
+
+ if ( this.copyCutData && dataTransfer.id == this.copyCutData.id ) {
+ dataTransfer = this.copyCutData;
+ dataTransfer.$ = evt.data.$.clipboardData;
+ } else {
+ this.copyCutData = dataTransfer;
+ }
+
+ return dataTransfer;
+ } else {
+ return new this.dataTransfer( null, sourceEditor );
+ }
+ },
+
+ /**
+ * Prevents dropping on the specified element.
+ *
+ * @since 4.5
+ * @param {CKEDITOR.dom.element} element The element on which dropping should be disabled.
+ */
+ preventDefaultDropOnElement: function( element ) {
+ element && element.on( 'dragover', preventDefaultSetDropEffectToNone );
+ }
+ };
+
+ // Data type used to link drag and drop events.
+ //
+ // In IE URL data type is buggie and there is no way to mark drag & drop without
+ // modifying text data (which would be displayed if user drop content to the textarea)
+ // so we just read dragged text.
+ //
+ // In Chrome and Firefox we can use custom data types.
+ var clipboardIdDataType = CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ? 'cke/id' : 'Text';
+ /**
+ * Facade for the native `dataTransfer`/`clipboadData` object to hide all differences
+ * between browsers.
+ *
+ * @since 4.5
+ * @class CKEDITOR.plugins.clipboard.dataTransfer
+ * @constructor Creates a class instance.
+ * @param {Object} [nativeDataTransfer] A native data transfer object.
+ * @param {CKEDITOR.editor} [editor] The source editor instance. If the editor is defined, dataValue will
+ * be created based on the editor content and the type will be 'html'.
+ */
+ CKEDITOR.plugins.clipboard.dataTransfer = function( nativeDataTransfer, editor ) {
+ if ( nativeDataTransfer ) {
+ this.$ = nativeDataTransfer;
+ }
+
+ this._ = {
+ metaRegExp: /^