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

Writing flow: Copy whole block if no text is selected #22186

Merged
merged 9 commits into from
May 15, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,15 @@ _Returns_

- `Object`: Action object.

<a name="flashBlock" href="#flashBlock">#</a> **flashBlock**

Yields action objects used in signalling that the block corresponding to the
given clientId should appear to "flash" by rhythmically highlighting it.

_Parameters_

- _clientId_ `string`: Target block client ID.

<a name="hideInsertionPoint" href="#hideInsertionPoint">#</a> **hideInsertionPoint**

Returns an action object hiding the insertion point.
Expand Down
78 changes: 72 additions & 6 deletions packages/block-editor/src/components/copy-handler/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,65 @@
/**
* WordPress dependencies
*/
import { useRef } from '@wordpress/element';
import { useCallback, useRef } from '@wordpress/element';
import { serialize, pasteHandler } from '@wordpress/blocks';
import { documentHasSelection } from '@wordpress/dom';
import { documentHasSelection, documentHasTextSelection } from '@wordpress/dom';
import { useDispatch, useSelect } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { getPasteEventData } from '../../utils/get-paste-event-data';

function useNotifyCopy() {
const { getBlockName } = useSelect(
( select ) => select( 'core/block-editor' ),
[]
);
const { getBlockType } = useSelect(
( select ) => select( 'core/blocks' ),
[]
);
const { createSuccessNotice } = useDispatch( 'core/notices' );

return useCallback( ( eventType, selectedBlockClientIds ) => {
let notice = '';
if ( selectedBlockClientIds.length === 1 ) {
const clientId = selectedBlockClientIds[ 0 ];
const { title } = getBlockType( getBlockName( clientId ) );
notice =
eventType === 'copy'
? sprintf(
// Translators: Name of the block being copied, e.g. "Paragraph"
__( 'Copied "%s" to clipboard.' ),
title
)
: sprintf(
// Translators: Name of the block being cut, e.g. "Paragraph"
__( 'Moved "%s" to clipboard.' ),
title
);
} else {
notice =
eventType === 'copy'
? sprintf(
// Translators: Number of blocks being copied
__( 'Copied %d blocks to clipboard.' ),
selectedBlockClientIds.length
)
: sprintf(
// Translators: Number of blocks being cut
__( 'Moved %d blocks to clipboard.' ),
selectedBlockClientIds.length
);
}
createSuccessNotice( notice, {
type: 'snackbar',
} );
}, [] );
}

function CopyHandler( { children } ) {
const containerRef = useRef();

Expand All @@ -21,7 +70,11 @@ function CopyHandler( { children } ) {
getSettings,
} = useSelect( ( select ) => select( 'core/block-editor' ), [] );

const { removeBlocks, replaceBlocks } = useDispatch( 'core/block-editor' );
const { flashBlock, removeBlocks, replaceBlocks } = useDispatch(
'core/block-editor'
);

const notifyCopy = useNotifyCopy();

const {
__experimentalCanUserUseUnfilteredHTML: canUserUseUnfilteredHTML,
Expand All @@ -35,9 +88,18 @@ function CopyHandler( { children } ) {
}

// Always handle multiple selected blocks.
// Let native copy behaviour take over in input fields.
if ( ! hasMultiSelection() && documentHasSelection() ) {
return;
if ( ! hasMultiSelection() ) {
// If copying, only consider actual text selection as selection.
// Otherwise, any focus on an input field is considered.
const hasSelection =
event.type === 'copy' || event.type === 'cut'
? documentHasTextSelection()
: documentHasSelection();

// Let native copy behaviour take over in input fields.
if ( hasSelection ) {
return;
}
}

if ( ! containerRef.current.contains( event.target ) ) {
Expand All @@ -46,6 +108,10 @@ function CopyHandler( { children } ) {
event.preventDefault();

if ( event.type === 'copy' || event.type === 'cut' ) {
if ( selectedBlockClientIds.length === 1 ) {
flashBlock( selectedBlockClientIds[ 0 ] );
}
notifyCopy( event.type, selectedBlockClientIds );
const blocks = getBlocksByClientId( selectedBlockClientIds );
const serialized = serialize( blocks );

Expand Down
15 changes: 15 additions & 0 deletions packages/block-editor/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,21 @@ export function toggleBlockHighlight( clientId, isHighlighted ) {
};
}

/**
* Yields action objects used in signalling that the block corresponding to the
* given clientId should appear to "flash" by rhythmically highlighting it.
*
* @param {string} clientId Target block client ID.
*/
export function* flashBlock( clientId ) {
yield toggleBlockHighlight( clientId, true );
yield {
type: 'SLEEP',
duration: 150,
};
yield toggleBlockHighlight( clientId, false );
}

/**
* Returns an action object that sets whether the block has controlled innerblocks.
*
Expand Down
5 changes: 5 additions & 0 deletions packages/block-editor/src/store/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ const controls = {
return registry.select( storeName )[ selectorName ]( ...args );
}
),
SLEEP( { duration } ) {
return new Promise( ( resolve ) => {
setTimeout( resolve, duration );
} );
},
};

export default controls;
4 changes: 4 additions & 0 deletions packages/dom/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New Feature

- Add `documentHasTextSelection` to inquire specifically about ranges of selected text, in addition to the existing `documentHasSelection`.

## 2.1.0 (2019-03-06)

### Bug Fix
Expand Down
12 changes: 10 additions & 2 deletions packages/dom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,16 @@ _Returns_

<a name="documentHasSelection" href="#documentHasSelection">#</a> **documentHasSelection**

Check wether the current document has a selection.
This checks both for focus in an input field and general text selection.
Check whether the current document has a selection. This checks for both
focus in an input field and general text selection.

_Returns_

- `boolean`: True if there is selection, false if not.

<a name="documentHasTextSelection" href="#documentHasTextSelection">#</a> **documentHasTextSelection**

Check whether the current document has selected text.

_Returns_

Expand Down
28 changes: 16 additions & 12 deletions packages/dom/src/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -484,26 +484,30 @@ export function isNumberInput( element ) {
}

/**
* Check wether the current document has a selection.
* This checks both for focus in an input field and general text selection.
* Check whether the current document has selected text.
*
* @return {boolean} True if there is selection, false if not.
*/
export function documentHasSelection() {
if ( isTextField( document.activeElement ) ) {
return true;
}

if ( isNumberInput( document.activeElement ) ) {
return true;
}

export function documentHasTextSelection() {
const selection = window.getSelection();
const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;

return range && ! range.collapsed;
}

/**
* Check whether the current document has a selection. This checks for both
* focus in an input field and general text selection.
*
* @return {boolean} True if there is selection, false if not.
*/
export function documentHasSelection() {
return (
isTextField( document.activeElement ) ||
isNumberInput( document.activeElement ) ||
documentHasTextSelection()
);
}

/**
* Check whether the contents of the element have been entirely selected.
* Returns true if there is no possibility of selection.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Multi-block selection should copy and paste individual blocks 1`] = `
"<!-- wp:paragraph -->
<p>Here is a unique string so we can test copying.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>2</p>
<!-- /wp:paragraph -->"
`;

exports[`Multi-block selection should copy and paste individual blocks 2`] = `
"<!-- wp:paragraph -->
<p>Here is a unique string so we can test copying.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>2</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Here is a unique string so we can test copying.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p></p>
<!-- /wp:paragraph -->"
Comment on lines +26 to +28
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the record, I don't like these empty paragraphs that the pasting yields, but this is tangent to the PR itself.

`;

exports[`Multi-block selection should cut and paste individual blocks 1`] = `
"<!-- wp:paragraph -->
<p>2</p>
<!-- /wp:paragraph -->"
`;

exports[`Multi-block selection should cut and paste individual blocks 2`] = `
"<!-- wp:paragraph -->
<p>2</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Yet another unique string.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p></p>
<!-- /wp:paragraph -->"
`;

exports[`Multi-block selection should respect inline copy when text is selected 1`] = `
"<!-- wp:paragraph -->
<p>First block</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Second block</p>
<!-- /wp:paragraph -->"
`;

exports[`Multi-block selection should respect inline copy when text is selected 2`] = `
"<!-- wp:paragraph -->
<p>First block</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>ck</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Second block</p>
<!-- /wp:paragraph -->"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* WordPress dependencies
*/
import {
clickBlockAppender,
createNewPost,
pressKeyWithModifier,
getEditedPostContent,
} from '@wordpress/e2e-test-utils';

describe( 'Multi-block selection', () => {
beforeEach( async () => {
await createNewPost();
} );

it( 'should copy and paste individual blocks', async () => {
await clickBlockAppender();
await page.keyboard.type(
'Here is a unique string so we can test copying.'
);
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '2' );
await page.keyboard.press( 'ArrowUp' );

await pressKeyWithModifier( 'primary', 'c' );
expect( await getEditedPostContent() ).toMatchSnapshot();

await page.keyboard.press( 'ArrowDown' );
await pressKeyWithModifier( 'primary', 'v' );
expect( await getEditedPostContent() ).toMatchSnapshot();
} );

it( 'should cut and paste individual blocks', async () => {
await clickBlockAppender();
await page.keyboard.type( 'Yet another unique string.' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '2' );
await page.keyboard.press( 'ArrowUp' );

await pressKeyWithModifier( 'primary', 'x' );
expect( await getEditedPostContent() ).toMatchSnapshot();

await page.keyboard.press( 'Tab' );
await page.keyboard.press( 'ArrowDown' );
await pressKeyWithModifier( 'primary', 'v' );
expect( await getEditedPostContent() ).toMatchSnapshot();
} );

it( 'should respect inline copy when text is selected', async () => {
await clickBlockAppender();
await page.keyboard.type( 'First block' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'Second block' );
await page.keyboard.press( 'ArrowUp' );
await pressKeyWithModifier( 'shift', 'ArrowLeft' );
await pressKeyWithModifier( 'shift', 'ArrowLeft' );

await pressKeyWithModifier( 'primary', 'c' );
await page.keyboard.press( 'ArrowRight' );
expect( await getEditedPostContent() ).toMatchSnapshot();

await page.keyboard.press( 'Enter' );
await pressKeyWithModifier( 'primary', 'v' );
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
} );