Skip to content

Commit

Permalink
Input Interaction: allow text to be dragged inside and across blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed Apr 4, 2019
1 parent 5cf4e14 commit 3ce88ca
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 37 deletions.
18 changes: 15 additions & 3 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/block-editor/src/components/block-list/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ class BlockList extends Component {
isFirst={ blockIndex === 0 }
isLast={ blockIndex === blockClientIds.length - 1 }
isDraggable={ isDraggable }
onSelectionEnd={ this.onSelectionEnd }
/>
</AsyncModeProvider>
);
Expand Down
104 changes: 74 additions & 30 deletions packages/block-editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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: `<img src="${ createBlobURL( file ) }">`,
mode: 'BLOCKS',
Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 }
Expand Down
16 changes: 12 additions & 4 deletions packages/components/src/drop-zone/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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 );
}
Expand Down

0 comments on commit 3ce88ca

Please sign in to comment.