Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Input Interaction: allow text to be dragged inside and across blocks #14574

Open
wants to merge 1 commit into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do these two calls constantly or check if needed (trying to be mindful of rerenders)

} 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 ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this is unit testable?

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 ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to use Lodash here:

Suggested change
const file = find( files, ( { type } ) => /^image\/(?:jpe?g|png|gif)$/.test( type ) );
const file = files.find( ( { type } ) => /^image\/(?:jpe?g|png|gif)$/.test( type ) );

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or if files may be nullish:

Suggested change
const file = find( files, ( { type } ) => /^image\/(?:jpe?g|png|gif)$/.test( type ) );
const file = files?.find( ( { 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