Skip to content

Commit

Permalink
Rich text: copy tag name on internal paste (#48254)
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix authored Aug 3, 2023
1 parent 11766ff commit e70d419
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 58 deletions.
2 changes: 1 addition & 1 deletion docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ Introduce new sections and organize content to help visitors (and search engines

- **Name:** core/heading
- **Category:** text
- **Supports:** __unstablePasteTextInline, align (full, wide), anchor, className, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight)
- **Supports:** align (full, wide), anchor, className, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight)
- **Attributes:** content, level, placeholder, textAlign

## Home Link
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,29 +129,6 @@ export function usePasteHandler( props ) {
}

const files = [ ...getFilesFromDataTransfer( clipboardData ) ];
const isInternal = clipboardData.getData( 'rich-text' ) === 'true';

// If the data comes from a rich text instance, we can directly use it
// without filtering the data. The filters are only meant for externally
// pasted content and remove inline styles.
if ( isInternal ) {
const pastedMultilineTag =
clipboardData.getData( 'rich-text-multi-line-tag' ) ||
undefined;
let pastedValue = create( {
html,
multilineTag: pastedMultilineTag,
multilineWrapperTags:
pastedMultilineTag === 'li'
? [ 'ul', 'ol' ]
: undefined,
preserveWhiteSpace,
} );
pastedValue = adjustLines( pastedValue, !! multilineTag );
addActiveFormats( pastedValue, value.activeFormats );
onChange( insert( value, pastedValue ) );
return;
}

if ( pastePlainText ) {
onChange( insert( value, create( { text: plainText } ) ) );
Expand Down Expand Up @@ -238,6 +215,10 @@ export function usePasteHandler( props ) {
mode,
tagName,
preserveWhiteSpace,
// If the data comes from a rich text instance, we can directly
// use it without filtering the data. The filters are only meant
// for externally pasted content and remove inline styles.
disableFilters: !! clipboardData.getData( 'rich-text' ),
} );

if ( typeof content === 'string' ) {
Expand Down
1 change: 0 additions & 1 deletion packages/block-library/src/heading/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
"textTransform": true
}
},
"__unstablePasteTextInline": true,
"__experimentalSlashInserter": true
},
"editorStyle": "wp-block-heading-editor",
Expand Down
1 change: 1 addition & 0 deletions packages/blocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ _Parameters_
- _options.mode_ `[string]`: Handle content as blocks or inline content. _ 'AUTO': Decide based on the content passed. _ 'INLINE': Always handle as inline content, and return string. \* 'BLOCKS': Always handle as blocks, and return array of blocks.
- _options.tagName_ `[Array]`: The tag into which content will be inserted.
- _options.preserveWhiteSpace_ `[boolean]`: Whether or not to preserve consequent white space.
- _options.disableFilters_ `[boolean]`: Whether or not to filter non semantic content.

_Returns_

Expand Down
70 changes: 45 additions & 25 deletions packages/blocks/src/api/raw-handling/paste-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { getPhrasingContentSchema, removeInvalidHTML } from '@wordpress/dom';
*/
import { htmlToBlocks } from './html-to-blocks';
import { hasBlockSupport } from '../registration';
import { getBlockInnerHTML } from '../serializer';
import parse from '../parser';
import normaliseBlocks from './normalise-blocks';
import specialCommentConverter from './special-comment-converter';
Expand Down Expand Up @@ -66,6 +65,40 @@ function filterInlineHTML( HTML, preserveWhiteSpace ) {
return HTML;
}

/**
* If we're allowed to return inline content, and there is only one inlineable
* block, and the original plain text content does not have any line breaks,
* then treat it as inline paste.
*
* @param {Object} options
* @param {Array} options.blocks
* @param {string} options.plainText
* @param {string} options.mode
*/
function maybeConvertToInline( { blocks, plainText, mode } ) {
if (
mode === 'AUTO' &&
blocks.length === 1 &&
hasBlockSupport( blocks[ 0 ].name, '__unstablePasteTextInline', false )
) {
const trimRegex = /^[\n]+|[\n]+$/g;
// Don't catch line breaks at the start or end.
const trimmedPlainText = plainText.replace( trimRegex, '' );

if (
trimmedPlainText !== '' &&
trimmedPlainText.indexOf( '\n' ) === -1
) {
const target = blocks[ 0 ].innerBlocks.length
? blocks[ 0 ].innerBlocks[ 0 ]
: blocks[ 0 ];
return target.attributes.content;
}
}

return blocks;
}

/**
* Converts an HTML string to known blocks. Strips everything else.
*
Expand All @@ -79,6 +112,7 @@ function filterInlineHTML( HTML, preserveWhiteSpace ) {
* @param {Array} [options.tagName] The tag into which content will be inserted.
* @param {boolean} [options.preserveWhiteSpace] Whether or not to preserve consequent white space.
*
* @param {boolean} [options.disableFilters] Whether or not to filter non semantic content.
* @return {Array|string} A list of blocks or a string, depending on `handlerMode`.
*/
export function pasteHandler( {
Expand All @@ -87,6 +121,7 @@ export function pasteHandler( {
mode = 'AUTO',
tagName,
preserveWhiteSpace,
disableFilters,
} ) {
// First of all, strip any meta tags.
HTML = HTML.replace( /<meta[^>]+>/g, '' );
Expand Down Expand Up @@ -121,6 +156,14 @@ export function pasteHandler( {
HTML = HTML.normalize();
}

if ( disableFilters ) {
return maybeConvertToInline( {
blocks: htmlToBlocks( normaliseBlocks( HTML ), pasteHandler ),
plainText,
mode,
} );
}

// Parse Markdown (and encoded HTML) if:
// * There is a plain text version.
// * There is no HTML version, or it has no formatting.
Expand Down Expand Up @@ -219,28 +262,5 @@ export function pasteHandler( {
.flat()
.filter( Boolean );

// If we're allowed to return inline content, and there is only one
// inlineable block, and the original plain text content does not have any
// line breaks, then treat it as inline paste.
if (
mode === 'AUTO' &&
blocks.length === 1 &&
hasBlockSupport( blocks[ 0 ].name, '__unstablePasteTextInline', false )
) {
const trimRegex = /^[\n]+|[\n]+$/g;
// Don't catch line breaks at the start or end.
const trimmedPlainText = plainText.replace( trimRegex, '' );

if (
trimmedPlainText !== '' &&
trimmedPlainText.indexOf( '\n' ) === -1
) {
return removeInvalidHTML(
getBlockInnerHTML( blocks[ 0 ] ),
phrasingContentSchema
).replace( trimRegex, '' );
}
}

return blocks;
return maybeConvertToInline( { blocks, plainText, mode } );
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ exports[`RichText should apply multiple formats when selection is collapsed 1`]
<!-- /wp:paragraph -->"
`;

exports[`RichText should copy/paste heading 1`] = `
"<!-- wp:heading -->
<h2 class="wp-block-heading">Heading</h2>
<!-- /wp:heading -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Heading</h2>
<!-- /wp:heading -->"
`;

exports[`RichText should handle Home and End keys 1`] = `
"<!-- wp:paragraph -->
<p>-<strong>12</strong>+</p>
Expand Down Expand Up @@ -129,7 +139,11 @@ exports[`RichText should paste paragraph contents into list 1`] = `
<!-- wp:list -->
<ul><!-- wp:list-item -->
<li>1<br>2</li>
<li>1</li>
<!-- /wp:list-item -->
<!-- wp:list-item -->
<li>2</li>
<!-- /wp:list-item --></ul>
<!-- /wp:list -->"
`;
Expand Down
11 changes: 11 additions & 0 deletions packages/e2e-tests/specs/editor/various/rich-text.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -556,4 +556,15 @@ describe( 'RichText', () => {
// Expect: <strong>1</strong>-<em>2</em>
expect( await getEditedPostContent() ).toMatchSnapshot();
} );

test( 'should copy/paste heading', async () => {
await insertBlock( 'Heading' );
await page.keyboard.type( 'Heading' );
await pressKeyWithModifier( 'primary', 'a' );
await pressKeyWithModifier( 'primary', 'c' );
await page.keyboard.press( 'ArrowRight' );
await page.keyboard.press( 'Enter' );
await pressKeyWithModifier( 'primary', 'v' );
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
} );
13 changes: 8 additions & 5 deletions packages/rich-text/src/component/use-copy-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,21 @@ export function useCopyHandler( props ) {

const selectedRecord = slice( record.current );
const plainText = getTextContent( selectedRecord );
const html = toHTMLString( {
const tagName = element.tagName.toLowerCase();

let html = toHTMLString( {
value: selectedRecord,
multilineTag,
preserveWhiteSpace,
} );

if ( tagName && tagName !== 'span' && tagName !== 'div' ) {
html = `<${ tagName }>${ html }</${ tagName }>`;
}

event.clipboardData.setData( 'text/plain', plainText );
event.clipboardData.setData( 'text/html', html );
event.clipboardData.setData( 'rich-text', 'true' );
event.clipboardData.setData(
'rich-text-multi-line-tag',
multilineTag || ''
);
event.preventDefault();

if ( event.type === 'cut' ) {
Expand Down
6 changes: 4 additions & 2 deletions test/integration/blocks-raw-handling.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,11 @@ describe( 'Blocks raw handling', () => {
HTML: '<h1>FOO</h1>',
plainText: 'FOO\n',
mode: 'AUTO',
} );
} )
.map( getBlockContent )
.join( '' );

expect( filtered ).toBe( 'FOO' );
expect( filtered ).toBe( '<h1 class="wp-block-heading">FOO</h1>' );
expect( console ).toHaveLogged();
} );

Expand Down

0 comments on commit e70d419

Please sign in to comment.