diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index afc50e46ad2d59..c79c440c3a2168 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -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 diff --git a/packages/block-editor/src/components/rich-text/use-paste-handler.js b/packages/block-editor/src/components/rich-text/use-paste-handler.js index 67c932aceddcc1..d86691ced70978 100644 --- a/packages/block-editor/src/components/rich-text/use-paste-handler.js +++ b/packages/block-editor/src/components/rich-text/use-paste-handler.js @@ -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 } ) ) ); @@ -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' ) { diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json index 80f1f0c47b5a8f..9c544e8f95c4f4 100644 --- a/packages/block-library/src/heading/block.json +++ b/packages/block-library/src/heading/block.json @@ -61,7 +61,6 @@ "textTransform": true } }, - "__unstablePasteTextInline": true, "__experimentalSlashInserter": true }, "editorStyle": "wp-block-heading-editor", diff --git a/packages/blocks/README.md b/packages/blocks/README.md index 91cfec30c6a726..01547ca24ef683 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -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_ diff --git a/packages/blocks/src/api/raw-handling/paste-handler.js b/packages/blocks/src/api/raw-handling/paste-handler.js index c4ad40e0b1f509..f0acfac9fa7614 100644 --- a/packages/blocks/src/api/raw-handling/paste-handler.js +++ b/packages/blocks/src/api/raw-handling/paste-handler.js @@ -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'; @@ -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. * @@ -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( { @@ -87,6 +121,7 @@ export function pasteHandler( { mode = 'AUTO', tagName, preserveWhiteSpace, + disableFilters, } ) { // First of all, strip any meta tags. HTML = HTML.replace( /]+>/g, '' ); @@ -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. @@ -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 } ); } diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap index 5d9c235e1c6a2e..7705ff11cbff9d 100644 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap +++ b/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap @@ -24,6 +24,16 @@ exports[`RichText should apply multiple formats when selection is collapsed 1`] " `; +exports[`RichText should copy/paste heading 1`] = ` +" +

Heading

+ + + +

Heading

+" +`; + exports[`RichText should handle Home and End keys 1`] = ` "

-12+

@@ -129,7 +139,11 @@ exports[`RichText should paste paragraph contents into list 1`] = ` " `; diff --git a/packages/e2e-tests/specs/editor/various/rich-text.test.js b/packages/e2e-tests/specs/editor/various/rich-text.test.js index 41b2c7253dfa2e..ff651e61d52ea9 100644 --- a/packages/e2e-tests/specs/editor/various/rich-text.test.js +++ b/packages/e2e-tests/specs/editor/various/rich-text.test.js @@ -556,4 +556,15 @@ describe( 'RichText', () => { // Expect: 1-2 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(); + } ); } ); diff --git a/packages/rich-text/src/component/use-copy-handler.js b/packages/rich-text/src/component/use-copy-handler.js index 45a7e1e0cfb8e2..b25e64428a1b4d 100644 --- a/packages/rich-text/src/component/use-copy-handler.js +++ b/packages/rich-text/src/component/use-copy-handler.js @@ -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 }`; + } + 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' ) { diff --git a/test/integration/blocks-raw-handling.test.js b/test/integration/blocks-raw-handling.test.js index 2a31d0b0ceaa28..733ae308c851ae 100644 --- a/test/integration/blocks-raw-handling.test.js +++ b/test/integration/blocks-raw-handling.test.js @@ -291,9 +291,11 @@ describe( 'Blocks raw handling', () => { HTML: '

FOO

', plainText: 'FOO\n', mode: 'AUTO', - } ); + } ) + .map( getBlockContent ) + .join( '' ); - expect( filtered ).toBe( 'FOO' ); + expect( filtered ).toBe( '

FOO

' ); expect( console ).toHaveLogged(); } );