diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index ef3eedc014633f..bbbcfb026a2a8e 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -1,9 +1,30 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { __, _x, sprintf } from '@wordpress/i18n'; +import { useState, useEffect } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { + useBlockProps, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { store as coreStore } from '@wordpress/core-data'; +import { View } from '@wordpress/primitives'; +import { Caption } from '../utils/caption'; + /** * Internal dependencies */ import { createUpgradedEmbedBlock, + findMoreSuitableBlock, getClassNames, + rewriteXToTwitter, removeAspectRatioClasses, fallback, getEmbedInfoByProvider, @@ -15,23 +36,6 @@ import EmbedLoading from './embed-loading'; import EmbedPlaceholder from './embed-placeholder'; import EmbedPreview from './embed-preview'; -/** - * External dependencies - */ -import clsx from 'clsx'; - -/** - * WordPress dependencies - */ -import { __, _x, sprintf } from '@wordpress/i18n'; -import { useState, useEffect } from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { useBlockProps } from '@wordpress/block-editor'; -import { store as coreStore } from '@wordpress/core-data'; -import { View } from '@wordpress/primitives'; -import { getAuthority } from '@wordpress/url'; -import { Caption } from '../utils/caption'; - const EmbedEdit = ( props ) => { const { attributes: { @@ -58,6 +62,8 @@ const EmbedEdit = ( props ) => { const [ url, setURL ] = useState( attributesUrl ); const [ isEditingURL, setIsEditingURL ] = useState( false ); const { invalidateResolution } = useDispatch( coreStore ); + const { __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); const { preview, @@ -131,63 +137,61 @@ const EmbedEdit = ( props ) => { } ); } + // When the preview can't be embedded, retry without any trailing slash. useEffect( () => { - if ( preview?.html || ! cannotEmbed || ! hasResolved ) { + if ( ! cannotEmbed || ! hasResolved || ! attributesUrl ) { return; } - // At this stage, we're not fetching the preview and know it can't be embedded, - // so try removing any trailing slash, and resubmit. const newURL = attributesUrl.replace( /\/$/, '' ); + if ( newURL === attributesUrl ) { + return; + } + setURL( newURL ); setIsEditingURL( false ); + __unstableMarkNextChangeAsNotPersistent(); setAttributes( { url: newURL } ); }, [ - preview?.html, attributesUrl, cannotEmbed, hasResolved, setAttributes, + __unstableMarkNextChangeAsNotPersistent, ] ); - // Try a different provider in case the embed url is not supported. + // Apply preview-derived attributes once the preview resolves. useEffect( () => { - if ( ! cannotEmbed || fetching || ! url ) { + if ( ! preview || isEditingURL ) { return; } - // Until X provider is supported in WordPress, as a workaround we use Twitter provider. - if ( getAuthority( url ) === 'x.com' ) { - const newURL = new URL( url ); - newURL.host = 'twitter.com'; - setAttributes( { url: newURL.toString() } ); - } - }, [ url, cannotEmbed, fetching, setAttributes ] ); + const mergedAttributes = getMergedAttributes(); - // Handle incoming preview. - useEffect( () => { - if ( preview && ! isEditingURL ) { - // When obtaining an incoming preview, - // we set the attributes derived from the preview data. - const mergedAttributes = getMergedAttributes(); - const hasChanges = Object.keys( mergedAttributes ).some( - ( key ) => mergedAttributes[ key ] !== attributes[ key ] + if ( onReplace ) { + const upgradedBlock = createUpgradedEmbedBlock( + props, + mergedAttributes ); - if ( hasChanges ) { - setAttributes( mergedAttributes ); + if ( upgradedBlock ) { + // Mutate via setAttributes; onReplace would remount the + // block and clear the URL textbox on undo. + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( upgradedBlock.attributes ); + return; } + } - if ( onReplace ) { - const upgradedBlock = createUpgradedEmbedBlock( - props, - mergedAttributes - ); + const hasChanges = Object.keys( mergedAttributes ).some( + ( key ) => mergedAttributes[ key ] !== attributes[ key ] + ); - if ( upgradedBlock ) { - onReplace( upgradedBlock ); - } - } + if ( hasChanges ) { + // Merge into the URL-submit undo level so a single undo + // reverts both the submit and the preview-driven attributes. + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( mergedAttributes ); } }, [ preview, isEditingURL ] ); @@ -219,14 +223,19 @@ const EmbedEdit = ( props ) => { event.preventDefault(); } - // If the embed URL was changed, we need to reset the aspect ratio class. - // To do this we have to remove the existing ratio class so it can be recalculated. + const rewrittenURL = rewriteXToTwitter( url ); const blockClass = removeAspectRatioClasses( attributes.className ); + setURL( rewrittenURL ); + setAttributes( { + url: rewrittenURL, + ...findMoreSuitableBlock( rewrittenURL ) + ?.attributes, + className: blockClass, + } ); setIsEditingURL( false ); - setAttributes( { url, className: blockClass } ); } } value={ url } cannotEmbed={ cannotEmbed } diff --git a/packages/block-library/src/embed/edit.native.js b/packages/block-library/src/embed/edit.native.js index 2144d2f2eba729..b9cb0ca45d5642 100644 --- a/packages/block-library/src/embed/edit.native.js +++ b/packages/block-library/src/embed/edit.native.js @@ -1,21 +1,3 @@ -/** - * Internal dependencies - */ -import { - createUpgradedEmbedBlock, - getClassNames, - removeAspectRatioClasses, - fallback, - getEmbedInfoByProvider, - getMergedAttributesWithPreview, -} from './util'; -import EmbedControls from './embed-controls'; -import { embedContentIcon } from './icons'; -import EmbedLoading from './embed-loading'; -import EmbedPlaceholder from './embed-placeholder'; -import EmbedPreview from './embed-preview'; -import EmbedLinkSettings from './embed-link-settings'; - /** * External dependencies */ @@ -33,7 +15,25 @@ import { } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { View } from '@wordpress/primitives'; -import { getAuthority } from '@wordpress/url'; +/** + * Internal dependencies + */ +import { + createUpgradedEmbedBlock, + findMoreSuitableBlock, + getClassNames, + rewriteXToTwitter, + removeAspectRatioClasses, + fallback, + getEmbedInfoByProvider, + getMergedAttributesWithPreview, +} from './util'; +import EmbedControls from './embed-controls'; +import { embedContentIcon } from './icons'; +import EmbedLoading from './embed-loading'; +import EmbedPlaceholder from './embed-placeholder'; +import EmbedPreview from './embed-preview'; +import EmbedLinkSettings from './embed-link-settings'; // The inline preview feature will be released progressible, for this reason // the embed will only be considered previewable for the following providers list. @@ -77,6 +77,8 @@ const EmbedEdit = ( props ) => { const [ showEmbedBottomSheet, setShowEmbedBottomSheet ] = useState( isEditingURL ); const { invalidateResolution } = useDispatch( coreStore ); + const { __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); const { preview, fetching, themeSupportsResponsive, cannotEmbed } = useSelect( @@ -150,50 +152,61 @@ const EmbedEdit = ( props ) => { } ); }; + // When the preview can't be embedded, retry without any trailing slash. useEffect( () => { - if ( ! preview?.html || ! cannotEmbed || fetching ) { + if ( ! cannotEmbed || fetching || ! url ) { return; } - // At this stage, we're not fetching the preview and know it can't be embedded, - // so try removing any trailing slash, and resubmit. + const newURL = url.replace( /\/$/, '' ); + if ( newURL === url ) { + return; + } + setIsEditingURL( false ); + __unstableMarkNextChangeAsNotPersistent(); setAttributes( { url: newURL } ); - }, [ preview?.html, url, cannotEmbed, fetching ] ); + }, [ + url, + cannotEmbed, + fetching, + setAttributes, + __unstableMarkNextChangeAsNotPersistent, + ] ); - // Try a different provider in case the embed url is not supported. + // Apply preview-derived attributes once the preview resolves. useEffect( () => { - if ( ! cannotEmbed || fetching || ! url ) { + if ( ! preview || isEditingURL ) { return; } - // Until X provider is supported in WordPress, as a workaround we use Twitter provider. - if ( getAuthority( url ) === 'x.com' ) { - const newURL = new URL( url ); - newURL.host = 'twitter.com'; - setAttributes( { url: newURL.toString() } ); - } - }, [ url, cannotEmbed, fetching, setAttributes ] ); + const mergedAttributes = getMergedAttributes(); - // Handle incoming preview. - useEffect( () => { - if ( preview && ! isEditingURL ) { - // When obtaining an incoming preview, - // we set the attributes derived from the preview data. - const mergedAttributes = getMergedAttributes(); - setAttributes( mergedAttributes ); + if ( onReplace ) { + const upgradedBlock = createUpgradedEmbedBlock( + props, + mergedAttributes + ); - if ( onReplace ) { - const upgradedBlock = createUpgradedEmbedBlock( - props, - mergedAttributes - ); - - if ( upgradedBlock ) { - onReplace( upgradedBlock ); - } + if ( upgradedBlock ) { + // Mutate via setAttributes; onReplace would remount the + // block and clear the URL textbox on undo. + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( upgradedBlock.attributes ); + return; } } + + const hasChanges = Object.keys( mergedAttributes ).some( + ( key ) => mergedAttributes[ key ] !== attributes[ key ] + ); + + if ( hasChanges ) { + // Merge into the URL-submit undo level so a single undo + // reverts both the submit and the preview-driven attributes. + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( mergedAttributes ); + } }, [ preview, isEditingURL ] ); useEffect( @@ -203,19 +216,20 @@ const EmbedEdit = ( props ) => { const onEditURL = useCallback( ( value ) => { - // If the embed URL was changed, we need to reset the aspect ratio class. - // To do this we have to remove the existing ratio class so it can be recalculated. - if ( attributes.url !== value ) { - const blockClass = removeAspectRatioClasses( - attributes.className - ); - setAttributes( { className: blockClass } ); - } + const rewrittenURL = rewriteXToTwitter( value ); + const urlChanged = attributes.url !== rewrittenURL; // The order of the following calls is important, we need to update the URL attribute before changing `isEditingURL`, // otherwise the side-effect that potentially replaces the block when updating the local state won't use the new URL // for creating the new block. - setAttributes( { url: value } ); + setAttributes( { + url: rewrittenURL, + ...findMoreSuitableBlock( rewrittenURL )?.attributes, + // If the embed URL was changed, reset the aspect ratio class so it can be recalculated. + ...( urlChanged && { + className: removeAspectRatioClasses( attributes.className ), + } ), + } ); setIsEditingURL( false ); }, [ attributes, setAttributes ] diff --git a/packages/block-library/src/embed/transforms.js b/packages/block-library/src/embed/transforms.js index 82c46da1fa7f94..c49657612914b2 100644 --- a/packages/block-library/src/embed/transforms.js +++ b/packages/block-library/src/embed/transforms.js @@ -7,7 +7,11 @@ import { createBlock } from '@wordpress/blocks'; * Internal dependencies */ import metadata from './block.json'; -import { removeAspectRatioClasses } from './util'; +import { + findMoreSuitableBlock, + rewriteXToTwitter, + removeAspectRatioClasses, +} from './util'; const { name: EMBED_BLOCK } = metadata; @@ -23,8 +27,10 @@ const transforms = { /^\s*(https?:\/\/\S+)\s*$/i.test( node.textContent ) && node.textContent?.match( /https/gi )?.length === 1, transform: ( node ) => { + const url = rewriteXToTwitter( node.textContent.trim() ); return createBlock( EMBED_BLOCK, { - url: node.textContent.trim(), + url, + ...findMoreSuitableBlock( url )?.attributes, } ); }, }, diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js index 9491b7d689b1b1..bd83292cc404e7 100644 --- a/packages/block-library/src/embed/util.js +++ b/packages/block-library/src/embed/util.js @@ -14,6 +14,7 @@ import { getBlockType, getBlockVariations, } from '@wordpress/blocks'; +import { getAuthority } from '@wordpress/url'; /** * Internal dependencies @@ -60,6 +61,22 @@ export const findMoreSuitableBlock = ( url ) => matchesPatterns( url, patterns ) ); +/** + * Rewrites `x.com` URLs to `twitter.com` as a workaround while the + * WordPress oEmbed registry lacks an X provider. See: https://core.trac.wordpress.org/ticket/59142. + * + * @param {string} url The URL to rewrite. + * @return {string} The (possibly) rewritten URL. + */ +export function rewriteXToTwitter( url ) { + if ( ! url || getAuthority( url ) !== 'x.com' ) { + return url; + } + const rewritten = new URL( url ); + rewritten.host = 'twitter.com'; + return rewritten.toString(); +} + export const isFromWordPress = ( html ) => html && html.includes( 'class="wp-embedded-content"' ); diff --git a/test/e2e/specs/editor/various/embedding.spec.js b/test/e2e/specs/editor/various/embedding.spec.js index 158864ded78ee0..e61f8c1a17f235 100644 --- a/test/e2e/specs/editor/various/embedding.spec.js +++ b/test/e2e/specs/editor/various/embedding.spec.js @@ -256,6 +256,36 @@ test.describe( 'Embedding content', () => { editor.canvas.getByRole( 'document', { name: 'Block: Embed' } ) ).toBeVisible(); } ); + + test( 'should undo URL submit and preview-driven upgrade in a single step', async ( { + editor, + embedUtils, + pageUtils, + } ) => { + await embedUtils.interceptRequests( { + 'https://www.youtube.com/watch?v=lXMskKTw3Bc': + MOCK_EMBED_VIDEO_SUCCESS_RESPONSE, + } ); + + await embedUtils.insertEmbed( + 'https://www.youtube.com/watch?v=lXMskKTw3Bc' + ); + + // Wait for the block to upgrade to the YouTube variant. + await expect( + editor.canvas.getByRole( 'document', { + name: 'Block: YouTube Embed', + } ) + ).toBeVisible(); + + await pageUtils.pressKeys( 'primary+z' ); + + await expect( + editor.canvas + .getByRole( 'document', { name: 'Block: Embed' } ) + .getByRole( 'textbox', { name: 'Embed URL' } ) + ).toHaveValue( 'https://www.youtube.com/watch?v=lXMskKTw3Bc' ); + } ); } ); class EmbedUtils {