diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index c5abcdc5deee36..a3623b2d32656f 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -247,15 +247,27 @@ export class BlockListBlock extends Component { } /** - * Prevents default dragging behavior within a block to allow for multi- - * selection to take effect unhampered. + * Handle dragging behavior within a block to allow for multi-selection or + * dragging selected text to take effect unhampered. If dragging selected + * text, current multi-selection should revert and stop. Any other drag + * behaviour should be default prevented. * * @param {DragEvent} event Drag event. * * @return {void} */ preventDrag( event ) { - event.preventDefault(); + // If there is plain text being transferred, this means that there is a + // text selection being dragged. Images, for example, have HTML transfer + // data, but no plain text. + if ( event.dataTransfer.getData( 'text/plain' ) ) { + // Cancel multi-selection. + this.props.onSelectionEnd(); + // Revert to selecting the block if there is multi-selection. + this.props.onSelect(); + } else { + event.preventDefault(); + } } /** diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index 5231dcb71749b7..dde57d3cab8621 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -218,6 +218,7 @@ class BlockList extends Component { isFirst={ blockIndex === 0 } isLast={ blockIndex === blockClientIds.length - 1 } isDraggable={ isDraggable } + onSelectionEnd={ this.onSelectionEnd } /> ); diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 1227dd23d4b540..ea3335e42de48c 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -104,6 +104,46 @@ function createPrepareEditableTree( props ) { }, value.formats ); } +function extractData( dataTransfer ) { + let plainText = ''; + let html = ''; + + // IE11 only supports `Text` as an argument for `getData` and will + // otherwise throw an invalid argument error, so we try the standard + // arguments first, then fallback to `Text` if they fail. + try { + plainText = dataTransfer.getData( 'text/plain' ); + html = dataTransfer.getData( 'text/html' ); + } catch ( error1 ) { + try { + html = dataTransfer.getData( 'Text' ); + } catch ( error2 ) { + // Some browsers like UC Browser paste plain text by default and + // don't support clipboardData at all, so allow default + // behaviour. + return; + } + } + + let { items, files } = dataTransfer; + + // In Edge these properties can be null instead of undefined, so a more + // rigorous test is required over using default values. + files = isNil( files ) ? [] : [ ...files ]; + items = isNil( items ) ? [] : [ ...items ]; + + items.forEach( ( item ) => { + if ( + item.type.indexOf( 'text/' ) !== 0 && + item.getAsFile + ) { + files.push( item.getAsFile() ); + } + } ); + + return { plainText, html, files }; +} + export class RichText extends Component { constructor( { value, onReplace, multiline } ) { super( ...arguments ); @@ -133,6 +173,7 @@ export class RichText extends Component { this.onDeleteKeyDown = this.onDeleteKeyDown.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); this.onPaste = this.onPaste.bind( this ); + this.onDrop = this.onDrop.bind( this ); this.onCreateUndoLevel = this.onCreateUndoLevel.bind( this ); this.setFocusedElement = this.setFocusedElement.bind( this ); this.onInput = this.onInput.bind( this ); @@ -243,45 +284,27 @@ export class RichText extends Component { * @param {PasteEvent} event The paste event. */ onPaste( event ) { - const clipboardData = event.clipboardData; - let { items, files } = clipboardData; - - // In Edge these properties can be null instead of undefined, so a more - // rigorous test is required over using default values. - items = isNil( items ) ? [] : items; - files = isNil( files ) ? [] : files; + const data = extractData( event.clipboardData ); - let plainText = ''; - let html = ''; - - // IE11 only supports `Text` as an argument for `getData` and will - // otherwise throw an invalid argument error, so we try the standard - // arguments first, then fallback to `Text` if they fail. - try { - plainText = clipboardData.getData( 'text/plain' ); - html = clipboardData.getData( 'text/html' ); - } catch ( error1 ) { - try { - html = clipboardData.getData( 'Text' ); - } catch ( error2 ) { - // Some browsers like UC Browser paste plain text by default and - // don't support clipboardData at all, so allow default - // behaviour. - return; - } + if ( ! data ) { + return; } event.preventDefault(); + this.onProcessData( this.getRecord(), data ); + } + + onProcessData( record, { plainText, html, files } ) { // Allows us to ask for this information when we get a report. window.console.log( 'Received HTML:\n\n', html ); window.console.log( 'Received plain text:\n\n', plainText ); // Only process file if no HTML is present. // Note: a pasted file may have the URL as plain text. - const item = find( [ ...items, ...files ], ( { type } ) => /^image\/(?:jpe?g|png|gif)$/.test( type ) ); - if ( item && ! html ) { - const file = item.getAsFile ? item.getAsFile() : item; + const file = find( files, ( { type } ) => /^image\/(?:jpe?g|png|gif)$/.test( type ) ); + + if ( file && ! html ) { const content = pasteHandler( { HTML: ``, mode: 'BLOCKS', @@ -301,8 +324,6 @@ export class RichText extends Component { return; } - const record = this.getRecord(); - // There is a selection, check if a URL is pasted. if ( ! isCollapsed( record ) ) { const pastedText = ( html || plainText ).replace( /<[^>]+>/g, '' ).trim(); @@ -357,6 +378,16 @@ export class RichText extends Component { } } + onDrop( event ) { + const data = extractData( event.dataTransfer ); + + // We have to wait until the browser inserts content and updates + // selection, so we can overwrite the content at the selection. + this.props.setTimeout( + () => this.onProcessData( this.createRecord(), data ) + ); + } + /** * Handles a focus event on the contenteditable field, calling the * `unstableOnFocus` prop callback if one is defined. The callback does not @@ -408,6 +439,18 @@ export class RichText extends Component { if ( event && event.nativeEvent.inputType ) { const { inputType } = event.nativeEvent; + // When the browser inserts from drop, discard the content, but keep + // the selection. The content is cleaned and inserted by us. + if ( inputType === 'insertFromDrop' ) { + const { start } = this.createRecord(); + this.applyRecord( { + ...this.getRecord(), + start, + end: start, + } ); + return; + } + // The browser formatted something or tried to insert HTML. // Overwrite it. It will be handled later by the format library if // needed. @@ -1095,6 +1138,7 @@ export class RichText extends Component { className={ className } key={ key } onPaste={ this.onPaste } + onDrop={ this.onDrop } onInput={ this.onInput } onCompositionEnd={ this.onCompositionEnd } onKeyDown={ this.onKeyDown } diff --git a/packages/components/src/drop-zone/provider.js b/packages/components/src/drop-zone/provider.js index 43fc3d76c562c6..74fc0a786ca26f 100644 --- a/packages/components/src/drop-zone/provider.js +++ b/packages/components/src/drop-zone/provider.js @@ -194,17 +194,28 @@ class DropZoneProvider extends Component { } onDragOver( event ) { + const dragEventType = getDragEventType( event ); + + if ( dragEventType === 'html' ) { + return; + } + this.toggleDraggingOverDocument( event, getDragEventType( event ) ); event.preventDefault(); } onDrop( event ) { + const dragEventType = getDragEventType( event ); + + if ( dragEventType === 'html' ) { + return; + } + // This seemingly useless line has been shown to resolve a Safari issue // where files dragged directly from the dock are not recognized event.dataTransfer && event.dataTransfer.files.length; // eslint-disable-line no-unused-expressions const { position, hoveredDropZone } = this.state; - const dragEventType = getDragEventType( event ); const dropZone = this.dropZones[ hoveredDropZone ]; this.resetDragState(); @@ -213,9 +224,6 @@ class DropZoneProvider extends Component { case 'file': dropZone.onFilesDrop( [ ...event.dataTransfer.files ], position ); break; - case 'html': - dropZone.onHTMLDrop( event.dataTransfer.getData( 'text/html' ), position ); - break; case 'default': dropZone.onDrop( event, position ); }