Plugin Tabs noticias

This commit is contained in:
root
2026-04-14 13:50:04 -06:00
parent 299099d006
commit 19d08e5694
2334 changed files with 628926 additions and 113 deletions

View File

@@ -0,0 +1,42 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "core/embed",
"title": "Embed",
"category": "embed",
"description": "Add a block that displays content pulled from other sites, like Twitter or YouTube.",
"textdomain": "default",
"attributes": {
"url": {
"type": "string"
},
"caption": {
"type": "string",
"source": "html",
"selector": "figcaption"
},
"type": {
"type": "string"
},
"providerNameSlug": {
"type": "string"
},
"allowResponsive": {
"type": "boolean",
"default": true
},
"responsive": {
"type": "boolean",
"default": false
},
"previewable": {
"type": "boolean",
"default": true
}
},
"supports": {
"align": true
},
"editorStyle": "wp-block-embed-editor",
"style": "wp-block-embed"
}

View File

@@ -0,0 +1,13 @@
export const ASPECT_RATIOS = [
// Common video resolutions.
{ ratio: '2.33', className: 'wp-embed-aspect-21-9' },
{ ratio: '2.00', className: 'wp-embed-aspect-18-9' },
{ ratio: '1.78', className: 'wp-embed-aspect-16-9' },
{ ratio: '1.33', className: 'wp-embed-aspect-4-3' },
// Vertical video and instagram square video support.
{ ratio: '1.00', className: 'wp-embed-aspect-1-1' },
{ ratio: '0.56', className: 'wp-embed-aspect-9-16' },
{ ratio: '0.50', className: 'wp-embed-aspect-1-2' },
];
export const WP_EMBED_TYPE = 'wp-embed';

View File

@@ -0,0 +1,46 @@
/**
* External dependencies
*/
import classnames from 'classnames';
/**
* Internal dependencies
*/
import metadata from './block.json';
/**
* WordPress dependencies
*/
import { RichText } from '@wordpress/block-editor';
const { attributes: blockAttributes } = metadata;
const deprecated = [
{
attributes: blockAttributes,
save( { attributes: { url, caption, type, providerNameSlug } } ) {
if ( ! url ) {
return null;
}
const embedClassName = classnames( 'wp-block-embed', {
[ `is-type-${ type }` ]: type,
[ `is-provider-${ providerNameSlug }` ]: providerNameSlug,
} );
return (
<figure className={ embedClassName }>
{ `\n${ url }\n` /* URL needs to be on its own line. */ }
{ ! RichText.isEmpty( caption ) && (
<RichText.Content
tagName="figcaption"
value={ caption }
/>
) }
</figure>
);
},
},
];
export default deprecated;

View File

@@ -0,0 +1,260 @@
/**
* Internal dependencies
*/
import {
createUpgradedEmbedBlock,
getClassNames,
fallback,
getAttributesFromPreview,
getEmbedInfoByProvider,
} 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';
/**
* External dependencies
*/
import classnames from 'classnames';
/**
* 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';
const EmbedEdit = ( props ) => {
const {
attributes: {
providerNameSlug,
previewable,
responsive,
url: attributesUrl,
},
attributes,
isSelected,
onReplace,
setAttributes,
insertBlocksAfter,
onFocus,
} = props;
const defaultEmbedInfo = {
title: _x( 'Embed', 'block title' ),
icon: embedContentIcon,
};
const { icon, title } =
getEmbedInfoByProvider( providerNameSlug ) || defaultEmbedInfo;
const [ url, setURL ] = useState( attributesUrl );
const [ isEditingURL, setIsEditingURL ] = useState( false );
const { invalidateResolution } = useDispatch( coreStore );
const {
preview,
fetching,
themeSupportsResponsive,
cannotEmbed,
} = useSelect(
( select ) => {
const {
getEmbedPreview,
isPreviewEmbedFallback,
isRequestingEmbedPreview,
getThemeSupports,
} = select( coreStore );
if ( ! attributesUrl ) {
return { fetching: false, cannotEmbed: false };
}
const embedPreview = getEmbedPreview( attributesUrl );
const previewIsFallback = isPreviewEmbedFallback( attributesUrl );
// The external oEmbed provider does not exist. We got no type info and no html.
const badEmbedProvider =
embedPreview?.html === false &&
embedPreview?.type === undefined;
// Some WordPress URLs that can't be embedded will cause the API to return
// a valid JSON response with no HTML and `data.status` set to 404, rather
// than generating a fallback response as other embeds do.
const wordpressCantEmbed = embedPreview?.data?.status === 404;
const validPreview =
!! embedPreview && ! badEmbedProvider && ! wordpressCantEmbed;
return {
preview: validPreview ? embedPreview : undefined,
fetching: isRequestingEmbedPreview( attributesUrl ),
themeSupportsResponsive: getThemeSupports()[
'responsive-embeds'
],
cannotEmbed: ! validPreview || previewIsFallback,
};
},
[ attributesUrl ]
);
/**
* @return {Object} Attributes derived from the preview, merged with the current attributes.
*/
const getMergedAttributes = () => {
const { allowResponsive, className } = attributes;
return {
...attributes,
...getAttributesFromPreview(
preview,
title,
className,
responsive,
allowResponsive
),
};
};
const toggleResponsive = () => {
const { allowResponsive, className } = attributes;
const { html } = preview;
const newAllowResponsive = ! allowResponsive;
setAttributes( {
allowResponsive: newAllowResponsive,
className: getClassNames(
html,
className,
responsive && newAllowResponsive
),
} );
};
useEffect( () => {
if ( ! preview?.html || ! cannotEmbed || fetching ) {
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( /\/$/, '' );
setURL( newURL );
setIsEditingURL( false );
setAttributes( { url: newURL } );
}, [ preview?.html, attributesUrl ] );
// Handle incoming preview.
useEffect( () => {
if ( preview && ! isEditingURL ) {
// Even though we set attributes that get derived from the preview,
// we don't access them directly because for the initial render,
// the `setAttributes` call will not have taken effect. If we're
// rendering responsive content, setting the responsive classes
// after the preview has been rendered can result in unwanted
// clipping or scrollbars. The `getAttributesFromPreview` function
// that `getMergedAttributes` uses is memoized so that we're not
// calculating them on every render.
setAttributes( getMergedAttributes() );
if ( onReplace ) {
const upgradedBlock = createUpgradedEmbedBlock(
props,
getMergedAttributes()
);
if ( upgradedBlock ) {
onReplace( upgradedBlock );
}
}
}
}, [ preview, isEditingURL ] );
const blockProps = useBlockProps();
if ( fetching ) {
return (
<View { ...blockProps }>
<EmbedLoading />
</View>
);
}
// translators: %s: type of embed e.g: "YouTube", "Twitter", etc. "Embed" is used when no specific type exists
const label = sprintf( __( '%s URL' ), title );
// No preview, or we can't embed the current URL, or we've clicked the edit button.
const showEmbedPlaceholder = ! preview || cannotEmbed || isEditingURL;
if ( showEmbedPlaceholder ) {
return (
<View { ...blockProps }>
<EmbedPlaceholder
icon={ icon }
label={ label }
onFocus={ onFocus }
onSubmit={ ( event ) => {
if ( event ) {
event.preventDefault();
}
setIsEditingURL( false );
setAttributes( { url } );
} }
value={ url }
cannotEmbed={ cannotEmbed }
onChange={ ( event ) => setURL( event.target.value ) }
fallback={ () => fallback( url, onReplace ) }
tryAgain={ () => {
invalidateResolution( 'getEmbedPreview', [ url ] );
} }
/>
</View>
);
}
// Even though we set attributes that get derived from the preview,
// we don't access them directly because for the initial render,
// the `setAttributes` call will not have taken effect. If we're
// rendering responsive content, setting the responsive classes
// after the preview has been rendered can result in unwanted
// clipping or scrollbars. The `getAttributesFromPreview` function
// that `getMergedAttributes` uses is memoized so that we're not
// calculating them on every render.
const {
caption,
type,
allowResponsive,
className: classFromPreview,
} = getMergedAttributes();
const className = classnames( classFromPreview, props.className );
return (
<>
<EmbedControls
showEditButton={ preview && ! cannotEmbed }
themeSupportsResponsive={ themeSupportsResponsive }
blockSupportsResponsive={ responsive }
allowResponsive={ allowResponsive }
toggleResponsive={ toggleResponsive }
switchBackToURLInput={ () => setIsEditingURL( true ) }
/>
<View { ...blockProps }>
<EmbedPreview
preview={ preview }
previewable={ previewable }
className={ className }
url={ url }
type={ type }
caption={ caption }
onCaptionChange={ ( value ) =>
setAttributes( { caption: value } )
}
isSelected={ isSelected }
icon={ icon }
label={ label }
insertBlocksAfter={ insertBlocksAfter }
/>
</View>
</>
);
};
export default EmbedEdit;

View File

@@ -0,0 +1,314 @@
/**
* Internal dependencies
*/
import {
createUpgradedEmbedBlock,
getClassNames,
fallback,
getAttributesFromPreview,
getEmbedInfoByProvider,
} 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
*/
import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { _x } from '@wordpress/i18n';
import { useCallback, 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';
// The inline preview feature will be released progressible, for this reason
// the embed will only be considered previewable for the following providers list.
const PREVIEWABLE_PROVIDERS = [ 'youtube', 'twitter', 'instagram', 'vimeo' ];
// Some providers are rendering the inline preview as a WordPress embed and
// are not supported yet, so we need to disallow them with a fixed providers list.
const NOT_PREVIEWABLE_WP_EMBED_PROVIDERS = [ 'pinterest' ];
const WP_EMBED_TYPE = 'wp-embed';
const EmbedEdit = ( props ) => {
const {
attributes: { align, providerNameSlug, previewable, responsive, url },
attributes,
isSelected,
onReplace,
setAttributes,
insertBlocksAfter,
onFocus,
clientId,
} = props;
const defaultEmbedInfo = {
title: _x( 'Embed', 'block title' ),
icon: embedContentIcon,
};
const embedInfoByProvider = getEmbedInfoByProvider( providerNameSlug );
const { icon, title } = embedInfoByProvider || defaultEmbedInfo;
const { wasBlockJustInserted } = useSelect(
( select ) => ( {
wasBlockJustInserted: select(
blockEditorStore
).wasBlockJustInserted( clientId, 'inserter_menu' ),
} ),
[ clientId ]
);
const [ isEditingURL, setIsEditingURL ] = useState(
isSelected && wasBlockJustInserted && ! url
);
const [ showEmbedBottomSheet, setShowEmbedBottomSheet ] = useState(
isEditingURL
);
const { invalidateResolution } = useDispatch( coreStore );
const {
preview,
fetching,
themeSupportsResponsive,
cannotEmbed,
} = useSelect(
( select ) => {
const {
getEmbedPreview,
hasFinishedResolution,
isPreviewEmbedFallback,
getThemeSupports,
} = select( coreStore );
if ( ! url ) {
return { fetching: false, cannotEmbed: false };
}
const embedPreview = getEmbedPreview( url );
const hasResolvedEmbedPreview = hasFinishedResolution(
'getEmbedPreview',
[ url ]
);
const previewIsFallback = isPreviewEmbedFallback( url );
// The external oEmbed provider does not exist. We got no type info and no html.
const badEmbedProvider =
embedPreview?.html === false &&
embedPreview?.type === undefined;
// Some WordPress URLs that can't be embedded will cause the API to return
// a valid JSON response with no HTML and `code` set to 404, rather
// than generating a fallback response as other embeds do.
const wordpressCantEmbed = embedPreview?.code === '404';
const validPreview =
!! embedPreview && ! badEmbedProvider && ! wordpressCantEmbed;
return {
preview: validPreview ? embedPreview : undefined,
fetching: ! hasResolvedEmbedPreview,
themeSupportsResponsive: getThemeSupports()[
'responsive-embeds'
],
cannotEmbed: ! validPreview || previewIsFallback,
};
},
[ url ]
);
/**
* Returns the attributes derived from the preview, merged with the current attributes.
*
* @param {boolean} ignorePreviousClassName Determines if the previous className attribute should be ignored when merging.
* @return {Object} Merged attributes.
*/
const getMergedAttributes = ( ignorePreviousClassName = false ) => {
const { allowResponsive, className } = attributes;
return {
...attributes,
...getAttributesFromPreview(
preview,
title,
ignorePreviousClassName ? undefined : className,
responsive,
allowResponsive
),
};
};
const toggleResponsive = () => {
const { allowResponsive, className } = attributes;
const { html } = preview;
const newAllowResponsive = ! allowResponsive;
setAttributes( {
allowResponsive: newAllowResponsive,
className: getClassNames(
html,
className,
responsive && newAllowResponsive
),
} );
};
useEffect( () => {
if ( ! preview?.html || ! cannotEmbed || fetching ) {
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( /\/$/, '' );
setIsEditingURL( false );
setAttributes( { url: newURL } );
}, [ preview?.html, url ] );
// Handle incoming preview.
useEffect( () => {
if ( preview && ! isEditingURL ) {
// When obtaining an incoming preview, we set the attributes derived from
// the preview data. In this case when getting the merged attributes,
// we ignore the previous classname because it might not match the expected
// classes by the new preview.
setAttributes( getMergedAttributes( true ) );
if ( onReplace ) {
const upgradedBlock = createUpgradedEmbedBlock(
props,
getMergedAttributes()
);
if ( upgradedBlock ) {
onReplace( upgradedBlock );
}
}
}
}, [ preview, isEditingURL ] );
useEffect( () => setShowEmbedBottomSheet( isEditingURL ), [
isEditingURL,
] );
const onEditURL = useCallback( ( value ) => {
// 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 } );
setIsEditingURL( false );
}, [] );
const blockProps = useBlockProps();
if ( fetching ) {
return (
<View { ...blockProps }>
<EmbedLoading />
</View>
);
}
const showEmbedPlaceholder = ! preview || cannotEmbed;
// Even though we set attributes that get derived from the preview,
// we don't access them directly because for the initial render,
// the `setAttributes` call will not have taken effect. If we're
// rendering responsive content, setting the responsive classes
// after the preview has been rendered can result in unwanted
// clipping or scrollbars. The `getAttributesFromPreview` function
// that `getMergedAttributes` uses is memoized so that we're not
// calculating them on every render.
const {
type,
allowResponsive,
className: classFromPreview,
} = getMergedAttributes();
const className = classnames( classFromPreview, props.className );
const isProviderPreviewable =
PREVIEWABLE_PROVIDERS.includes( providerNameSlug ) ||
// For WordPress embeds, we enable the inline preview for all its providers
// except the ones that are not supported yet.
( WP_EMBED_TYPE === type &&
! NOT_PREVIEWABLE_WP_EMBED_PROVIDERS.includes( providerNameSlug ) );
const linkLabel = WP_EMBED_TYPE === type ? 'WordPress' : title;
return (
<>
{ showEmbedPlaceholder ? (
<>
<View { ...blockProps }>
<EmbedPlaceholder
icon={ icon }
isSelected={ isSelected }
label={ title }
onPress={ ( event ) => {
onFocus( event );
setIsEditingURL( true );
} }
cannotEmbed={ cannotEmbed }
fallback={ () => fallback( url, onReplace ) }
tryAgain={ () => {
invalidateResolution( 'getEmbedPreview', [
url,
] );
} }
openEmbedLinkSettings={ () =>
setShowEmbedBottomSheet( true )
}
/>
</View>
</>
) : (
<>
<EmbedControls
themeSupportsResponsive={ themeSupportsResponsive }
blockSupportsResponsive={ responsive }
allowResponsive={ allowResponsive }
toggleResponsive={ toggleResponsive }
url={ url }
linkLabel={ linkLabel }
onEditURL={ onEditURL }
/>
<View { ...blockProps }>
<EmbedPreview
align={ align }
className={ className }
clientId={ clientId }
icon={ icon }
insertBlocksAfter={ insertBlocksAfter }
isSelected={ isSelected }
label={ title }
onFocus={ onFocus }
preview={ preview }
isProviderPreviewable={ isProviderPreviewable }
previewable={ previewable }
type={ type }
url={ url }
isDefaultEmbedInfo={ ! embedInfoByProvider }
/>
</View>
</>
) }
<EmbedLinkSettings
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
value={ url }
label={ linkLabel }
isVisible={ showEmbedBottomSheet }
onClose={ () => setShowEmbedBottomSheet( false ) }
onSubmit={ onEditURL }
withBottomSheet
/>
</>
);
};
export default EmbedEdit;

View File

@@ -0,0 +1,45 @@
.wp-block-embed {
// Remove the left and right margin the figure is born with.
margin-left: 0;
margin-right: 0;
// Necessary because we use responsive trickery to set width/height,
// therefore the video doesn't intrinsically clear floats like an image does.
clear: both;
&.is-loading {
display: flex;
justify-content: center;
}
// Stops long URLs from breaking out of the no preview available screen
.components-placeholder__error {
word-break: break-word;
}
.components-placeholder__learn-more {
margin-top: 1em;
}
}
.block-library-embed__interactive-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
}
.wp-block[data-align="left"],
.wp-block[data-align="right"] {
> .wp-block-embed {
max-width: 360px;
width: 100%;
// Unless these have a min-width, they collapse when floated.
.wp-block-embed__wrapper {
min-width: $break-zoomed-in;
}
}
}

View File

@@ -0,0 +1,63 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import {
ToolbarButton,
PanelBody,
ToggleControl,
ToolbarGroup,
} from '@wordpress/components';
import { BlockControls, InspectorControls } from '@wordpress/block-editor';
import { edit } from '@wordpress/icons';
function getResponsiveHelp( checked ) {
return checked
? __(
'This embed will preserve its aspect ratio when the browser is resized.'
)
: __(
'This embed may not preserve its aspect ratio when the browser is resized.'
);
}
const EmbedControls = ( {
blockSupportsResponsive,
showEditButton,
themeSupportsResponsive,
allowResponsive,
toggleResponsive,
switchBackToURLInput,
} ) => (
<>
<BlockControls>
<ToolbarGroup>
{ showEditButton && (
<ToolbarButton
className="components-toolbar__control"
label={ __( 'Edit URL' ) }
icon={ edit }
onClick={ switchBackToURLInput }
/>
) }
</ToolbarGroup>
</BlockControls>
{ themeSupportsResponsive && blockSupportsResponsive && (
<InspectorControls>
<PanelBody
title={ __( 'Media settings' ) }
className="blocks-responsive"
>
<ToggleControl
label={ __( 'Resize for smaller devices' ) }
checked={ allowResponsive }
help={ getResponsiveHelp }
onChange={ toggleResponsive }
/>
</PanelBody>
</InspectorControls>
) }
</>
);
export default EmbedControls;

View File

@@ -0,0 +1,65 @@
/**
* Internal dependencies
*/
import EmbedLinkSettings from './embed-link-settings';
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { InspectorControls } from '@wordpress/block-editor';
import { useDispatch } from '@wordpress/data';
import { store as editPostStore } from '@wordpress/edit-post';
function getResponsiveHelp( checked ) {
return checked
? __(
'This embed will preserve its aspect ratio when the browser is resized.'
)
: __(
'This embed may not preserve its aspect ratio when the browser is resized.'
);
}
const EmbedControls = ( {
blockSupportsResponsive,
themeSupportsResponsive,
allowResponsive,
toggleResponsive,
url,
linkLabel,
onEditURL,
} ) => {
const { closeGeneralSidebar: closeSettingsBottomSheet } = useDispatch(
editPostStore
);
return (
<>
<InspectorControls>
{ themeSupportsResponsive && blockSupportsResponsive && (
<PanelBody title={ __( 'Media settings' ) }>
<ToggleControl
label={ __( 'Resize for smaller devices' ) }
checked={ allowResponsive }
help={ getResponsiveHelp }
onChange={ toggleResponsive }
/>
</PanelBody>
) }
<PanelBody title={ __( 'Link settings' ) }>
<EmbedLinkSettings
value={ url }
label={ linkLabel }
onSubmit={ ( value ) => {
closeSettingsBottomSheet();
onEditURL( value );
} }
/>
</PanelBody>
</InspectorControls>
</>
);
};
export default EmbedControls;

View File

@@ -0,0 +1,100 @@
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import {
LinkSettingsNavigation,
FooterMessageLink,
} from '@wordpress/components';
import { isURL } from '@wordpress/url';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
const EmbedLinkSettings = ( {
autoFocus,
value,
label,
isVisible,
onClose,
onSubmit,
withBottomSheet,
} ) => {
const url = useRef( value );
const [ inputURL, setInputURL ] = useState( value );
const { createErrorNotice } = useDispatch( noticesStore );
const linkSettingsOptions = {
url: {
label: sprintf(
// translators: %s: embed block variant's label e.g: "Twitter".
__( '%s link' ),
label
),
placeholder: __( 'Add link' ),
autoFocus,
autoFill: true,
},
footer: {
label: (
<FooterMessageLink
href={ __(
'https://wordpress.org/support/article/embeds/'
) }
value={ __( 'Learn more about embeds' ) }
/>
),
separatorType: 'topFullWidth',
},
};
const onDismiss = useCallback( () => {
if ( ! isURL( url.current ) && url.current !== '' ) {
createErrorNotice( __( 'Invalid URL. Please enter a valid URL.' ) );
// If the URL was already defined, we submit it to stop showing the embed placeholder.
onSubmit( value );
return;
}
onSubmit( url.current );
}, [ onSubmit, value ] );
useEffect( () => {
url.current = value;
setInputURL( value );
}, [ value ] );
/**
* If the Embed Bottom Sheet component does not utilize a bottom sheet then the onDismiss action is not
* called. Here we are wiring the onDismiss to the onClose callback that gets triggered when input is submitted.
*/
const performOnCloseOperations = useCallback( () => {
if ( onClose ) {
onClose();
}
if ( ! withBottomSheet ) {
onDismiss();
}
}, [ onClose ] );
const onSetAttributes = useCallback( ( attributes ) => {
url.current = attributes.url;
setInputURL( attributes.url );
}, [] );
return (
<LinkSettingsNavigation
isVisible={ isVisible }
url={ inputURL }
onClose={ performOnCloseOperations }
onDismiss={ onDismiss }
setAttributes={ onSetAttributes }
options={ linkSettingsOptions }
testID="embed-edit-url-modal"
withBottomSheet={ withBottomSheet }
showIcon
/>
);
};
export default EmbedLinkSettings;

View File

@@ -0,0 +1,12 @@
/**
* WordPress dependencies
*/
import { Spinner } from '@wordpress/components';
const EmbedLoading = () => (
<div className="wp-block-embed is-loading">
<Spinner />
</div>
);
export default EmbedLoading;

View File

@@ -0,0 +1,29 @@
/**
* External dependencies
*/
import { ActivityIndicator, View } from 'react-native';
/**
* WordPress dependencies
*/
import { usePreferredColorSchemeStyle } from '@wordpress/compose';
/**
* Internal dependencies
*/
import styles from './styles.scss';
const EmbedLoading = () => {
const style = usePreferredColorSchemeStyle(
styles[ 'embed-preview__loading' ],
styles[ 'embed-preview__loading--dark' ]
);
return (
<View style={ style }>
<ActivityIndicator animating />
</View>
);
};
export default EmbedLoading;

View File

@@ -0,0 +1,226 @@
/**
* External dependencies
*/
import { TouchableOpacity, TouchableWithoutFeedback, Text } from 'react-native';
/**
* WordPress dependencies
*/
import { View } from '@wordpress/primitives';
import { __, sprintf } from '@wordpress/i18n';
import { useRef, useState } from '@wordpress/element';
import { usePreferredColorSchemeStyle } from '@wordpress/compose';
import { requestPreview } from '@wordpress/react-native-bridge';
import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { BottomSheet, Icon, TextControl } from '@wordpress/components';
import { help } from '@wordpress/icons';
import { BlockIcon } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import styles from './styles.scss';
const EmbedNoPreview = ( {
label,
icon,
isSelected,
onPress,
previewable,
isDefaultEmbedInfo,
} ) => {
const shouldRequestReview = useRef( false );
const [ isSheetVisible, setIsSheetVisible ] = useState( false );
const { postType } = useSelect( ( select ) => ( {
postType: select( editorStore ).getEditedPostAttribute( 'type' ),
} ) );
const containerStyle = usePreferredColorSchemeStyle(
styles.embed__container,
styles[ 'embed__container--dark' ]
);
const labelStyle = usePreferredColorSchemeStyle(
styles.embed__label,
styles[ 'embed__label--dark' ]
);
const descriptionStyle = usePreferredColorSchemeStyle(
styles.embed__description,
styles[ 'embed__description--dark' ]
);
const helpIconStyle = usePreferredColorSchemeStyle(
styles[ 'embed-no-preview__help-icon' ],
styles[ 'embed-no-preview__help-icon--dark' ]
);
const sheetIconStyle = usePreferredColorSchemeStyle(
styles[ 'embed-no-preview__sheet-icon' ],
styles[ 'embed-no-preview__sheet-icon--dark' ]
);
const sheetTitleStyle = usePreferredColorSchemeStyle(
styles[ 'embed-no-preview__sheet-title' ],
styles[ 'embed-no-preview__sheet-title--dark' ]
);
const sheetDescriptionStyle = usePreferredColorSchemeStyle(
styles[ 'embed-no-preview__sheet-description' ],
styles[ 'embed-no-preview__sheet-description--dark' ]
);
const sheetButtonStyle = usePreferredColorSchemeStyle(
styles[ 'embed-no-preview__sheet-button' ],
styles[ 'embed-no-preview__sheet-button--dark' ]
);
const previewButtonA11yHint =
postType === 'page'
? __( 'Double tap to preview page.' )
: __( 'Double tap to preview post.' );
const previewButtonText =
postType === 'page' ? __( 'Preview page' ) : __( 'Preview post' );
const comingSoonDescription =
postType === 'page'
? sprintf(
// translators: %s: embed block variant's label e.g: "Twitter".
__(
'Were working hard on adding support for %s previews. In the meantime, you can preview the embedded content on the page.'
),
label
)
: sprintf(
// translators: %s: embed block variant's label e.g: "Twitter".
__(
'Were working hard on adding support for %s previews. In the meantime, you can preview the embedded content on the post.'
),
label
);
function onOpenSheet() {
setIsSheetVisible( true );
}
function onCloseSheet() {
setIsSheetVisible( false );
}
function onDismissSheet() {
// The preview request has to be done after the bottom sheet modal is dismissed,
// otherwise the preview native modal is not displayed.
if ( shouldRequestReview.current ) {
requestPreview();
}
shouldRequestReview.current = false;
}
function onPressContainer() {
onPress();
onOpenSheet();
}
function onPressHelp() {
onPressContainer();
}
const embedNoProviderPreview = (
<>
<TouchableWithoutFeedback
accessibilityRole={ 'button' }
accessibilityHint={ previewButtonA11yHint }
disabled={ ! isSelected }
onPress={ onPressContainer }
>
<View style={ containerStyle }>
<BlockIcon icon={ icon } />
<Text style={ labelStyle }>{ label }</Text>
<Text style={ descriptionStyle }>
{ sprintf(
// translators: %s: embed block variant's label e.g: "Twitter".
__( '%s previews not yet available' ),
label
) }
</Text>
<Text style={ styles.embed__action }>
{ previewButtonText.toUpperCase() }
</Text>
<TouchableOpacity
accessibilityHint={ __( 'Tap here to show help' ) }
accessibilityLabel={ __( 'Help button' ) }
accessibilityRole={ 'button' }
disabled={ ! isSelected }
onPress={ onPressHelp }
style={ helpIconStyle }
>
<Icon
icon={ help }
fill={ helpIconStyle.fill }
size={ helpIconStyle.width }
/>
</TouchableOpacity>
</View>
</TouchableWithoutFeedback>
<BottomSheet
isVisible={ isSheetVisible }
hideHeader
onDismiss={ onDismissSheet }
onClose={ onCloseSheet }
testID="embed-no-preview-modal"
>
<View style={ styles[ 'embed-no-preview__container' ] }>
<View style={ sheetIconStyle }>
<Icon
icon={ help }
fill={ sheetIconStyle.fill }
size={ sheetIconStyle.width }
/>
</View>
<Text style={ sheetTitleStyle }>
{ isDefaultEmbedInfo
? __( 'Embed block previews are coming soon' )
: sprintf(
// translators: %s: embed block variant's label e.g: "Twitter".
__(
'%s embed block previews are coming soon'
),
label
) }
</Text>
<Text style={ sheetDescriptionStyle }>
{ comingSoonDescription }
</Text>
</View>
<TextControl
label={ previewButtonText }
separatorType="topFullWidth"
onPress={ () => {
shouldRequestReview.current = true;
onCloseSheet();
} }
labelStyle={ sheetButtonStyle }
/>
<TextControl
label={ __( 'Dismiss' ) }
separatorType="topFullWidth"
onPress={ onCloseSheet }
labelStyle={ sheetButtonStyle }
/>
</BottomSheet>
</>
);
return (
<>
{ previewable ? (
embedNoProviderPreview
) : (
<View style={ containerStyle }>
<BlockIcon icon={ icon } />
<Text style={ labelStyle }>
{ __( 'No preview available' ) }
</Text>
</View>
) }
</>
);
};
export default EmbedNoPreview;

View File

@@ -0,0 +1,66 @@
/**
* WordPress dependencies
*/
import { __, _x } from '@wordpress/i18n';
import { Button, Placeholder, ExternalLink } from '@wordpress/components';
import { BlockIcon } from '@wordpress/block-editor';
const EmbedPlaceholder = ( {
icon,
label,
value,
onSubmit,
onChange,
cannotEmbed,
fallback,
tryAgain,
} ) => {
return (
<Placeholder
icon={ <BlockIcon icon={ icon } showColors /> }
label={ label }
className="wp-block-embed"
instructions={ __(
'Paste a link to the content you want to display on your site.'
) }
>
<form onSubmit={ onSubmit }>
<input
type="url"
value={ value || '' }
className="components-placeholder__input"
aria-label={ label }
placeholder={ __( 'Enter URL to embed here…' ) }
onChange={ onChange }
/>
<Button variant="primary" type="submit">
{ _x( 'Embed', 'button label' ) }
</Button>
</form>
<div className="components-placeholder__learn-more">
<ExternalLink
href={ __(
'https://wordpress.org/support/article/embeds/'
) }
>
{ __( 'Learn more about embeds' ) }
</ExternalLink>
</div>
{ cannotEmbed && (
<div className="components-placeholder__error">
<div className="components-placeholder__instructions">
{ __( 'Sorry, this content could not be embedded.' ) }
</div>
<Button variant="secondary" onClick={ tryAgain }>
{ _x( 'Try again', 'button label' ) }
</Button>{ ' ' }
<Button variant="secondary" onClick={ fallback }>
{ _x( 'Convert to link', 'button label' ) }
</Button>
</div>
) }
</Placeholder>
);
};
export default EmbedPlaceholder;

View File

@@ -0,0 +1,146 @@
/**
* External dependencies
*/
import { View, Text, TouchableWithoutFeedback } from 'react-native';
import { compact } from 'lodash';
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { usePreferredColorSchemeStyle } from '@wordpress/compose';
import { Icon, Picker } from '@wordpress/components';
import { BlockIcon } from '@wordpress/block-editor';
import { useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
import styles from './styles.scss';
import { noticeOutline } from '../../../components/src/mobile/gridicons';
const EmbedPlaceholder = ( {
icon,
isSelected,
label,
onPress,
cannotEmbed,
fallback,
tryAgain,
openEmbedLinkSettings,
} ) => {
const containerStyle = usePreferredColorSchemeStyle(
styles.embed__container,
styles[ 'embed__container--dark' ]
);
const labelStyle = usePreferredColorSchemeStyle(
styles.embed__label,
styles[ 'embed__label--dark' ]
);
const descriptionStyle = styles.embed__description;
const descriptionErrorStyle = styles[ 'embed__description--error' ];
const actionStyle = usePreferredColorSchemeStyle(
styles.embed__action,
styles[ 'embed__action--dark' ]
);
const embedIconErrorStyle = styles[ 'embed__icon--error' ];
const cannotEmbedMenuPickerRef = useRef();
const errorPickerOptions = {
retry: {
id: 'retryOption',
label: __( 'Retry' ),
value: 'retryOption',
onSelect: tryAgain,
},
convertToLink: {
id: 'convertToLinkOption',
label: __( 'Convert to link' ),
value: 'convertToLinkOption',
onSelect: fallback,
},
editLink: {
id: 'editLinkOption',
label: __( 'Edit link' ),
value: 'editLinkOption',
onSelect: openEmbedLinkSettings,
},
};
const options = compact( [
cannotEmbed && errorPickerOptions.retry,
cannotEmbed && errorPickerOptions.convertToLink,
cannotEmbed && errorPickerOptions.editLink,
] );
function onPickerSelect( value ) {
const selectedItem = options.find( ( item ) => item.value === value );
selectedItem.onSelect();
}
// When the content cannot be embedded the onPress should trigger the Picker instead of the onPress prop.
function resolveOnPressEvent() {
if ( cannotEmbed ) {
cannotEmbedMenuPickerRef.current?.presentPicker();
} else {
onPress();
}
}
return (
<>
<TouchableWithoutFeedback
accessibilityRole={ 'button' }
accessibilityHint={
cannotEmbed
? __( 'Double tap to view embed options.' )
: __( 'Double tap to add a link.' )
}
onPress={ resolveOnPressEvent }
disabled={ ! isSelected }
>
<View style={ containerStyle }>
{ cannotEmbed ? (
<>
<Icon
icon={ noticeOutline }
fill={ embedIconErrorStyle.fill }
style={ embedIconErrorStyle }
/>
<Text
style={ [
descriptionStyle,
descriptionErrorStyle,
] }
>
{ __( 'Unable to embed media' ) }
</Text>
<Text style={ actionStyle }>
{ __( 'More options' ) }
</Text>
<Picker
title={ __( 'Embed options' ) }
ref={ cannotEmbedMenuPickerRef }
options={ options }
onChange={ onPickerSelect }
hideCancelButton
leftAlign
/>
</>
) : (
<>
<BlockIcon icon={ icon } />
<Text style={ labelStyle }>{ label }</Text>
<Text style={ actionStyle }>
{ __( 'ADD LINK' ) }
</Text>
</>
) }
</View>
</TouchableWithoutFeedback>
</>
);
};
export default EmbedPlaceholder;

View File

@@ -0,0 +1,157 @@
/**
* Internal dependencies
*/
import { getPhotoHtml } from './util';
/**
* External dependencies
*/
import classnames from 'classnames/dedupe';
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Placeholder, SandBox } from '@wordpress/components';
import { RichText, BlockIcon } from '@wordpress/block-editor';
import { Component } from '@wordpress/element';
import { createBlock } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import WpEmbedPreview from './wp-embed-preview';
class EmbedPreview extends Component {
constructor() {
super( ...arguments );
this.hideOverlay = this.hideOverlay.bind( this );
this.state = {
interactive: false,
};
}
static getDerivedStateFromProps( nextProps, state ) {
if ( ! nextProps.isSelected && state.interactive ) {
// We only want to change this when the block is not selected, because changing it when
// the block becomes selected makes the overlap disappear too early. Hiding the overlay
// happens on mouseup when the overlay is clicked.
return { interactive: false };
}
return null;
}
hideOverlay() {
// This is called onMouseUp on the overlay. We can't respond to the `isSelected` prop
// changing, because that happens on mouse down, and the overlay immediately disappears,
// and the mouse event can end up in the preview content. We can't use onClick on
// the overlay to hide it either, because then the editor misses the mouseup event, and
// thinks we're multi-selecting blocks.
this.setState( { interactive: true } );
}
render() {
const {
preview,
previewable,
url,
type,
caption,
onCaptionChange,
isSelected,
className,
icon,
label,
insertBlocksAfter,
} = this.props;
const { scripts } = preview;
const { interactive } = this.state;
const html = 'photo' === type ? getPhotoHtml( preview ) : preview.html;
const parsedHost = new URL( url ).host.split( '.' );
const parsedHostBaseUrl = parsedHost
.splice( parsedHost.length - 2, parsedHost.length - 1 )
.join( '.' );
const iframeTitle = sprintf(
// translators: %s: host providing embed content e.g: www.youtube.com
__( 'Embedded content from %s' ),
parsedHostBaseUrl
);
const sandboxClassnames = classnames(
type,
className,
'wp-block-embed__wrapper'
);
// Disabled because the overlay div doesn't actually have a role or functionality
// as far as the user is concerned. We're just catching the first click so that
// the block can be selected without interacting with the embed preview that the overlay covers.
/* eslint-disable jsx-a11y/no-static-element-interactions */
const embedWrapper =
'wp-embed' === type ? (
<WpEmbedPreview html={ html } />
) : (
<div className="wp-block-embed__wrapper">
<SandBox
html={ html }
scripts={ scripts }
title={ iframeTitle }
type={ sandboxClassnames }
onFocus={ this.hideOverlay }
/>
{ ! interactive && (
<div
className="block-library-embed__interactive-overlay"
onMouseUp={ this.hideOverlay }
/>
) }
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
return (
<figure
className={ classnames( className, 'wp-block-embed', {
'is-type-video': 'video' === type,
} ) }
>
{ previewable ? (
embedWrapper
) : (
<Placeholder
icon={ <BlockIcon icon={ icon } showColors /> }
label={ label }
>
<p className="components-placeholder__error">
<a href={ url }>{ url }</a>
</p>
<p className="components-placeholder__error">
{ sprintf(
/* translators: %s: host providing embed content e.g: www.youtube.com */
__(
"Embedded content from %s can't be previewed in the editor."
),
parsedHostBaseUrl
) }
</p>
</Placeholder>
) }
{ ( ! RichText.isEmpty( caption ) || isSelected ) && (
<RichText
tagName="figcaption"
placeholder={ __( 'Add caption' ) }
value={ caption }
onChange={ onCaptionChange }
inlineToolbar
__unstableOnSplitAtEnd={ () =>
insertBlocksAfter( createBlock( 'core/paragraph' ) )
}
/>
) }
</figure>
);
}
}
export default EmbedPreview;

View File

@@ -0,0 +1,157 @@
/**
* External dependencies
*/
import { TouchableWithoutFeedback } from 'react-native';
import { isEmpty } from 'lodash';
import classnames from 'classnames/dedupe';
/**
* WordPress dependencies
*/
import { View } from '@wordpress/primitives';
import {
BlockCaption,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { __, sprintf } from '@wordpress/i18n';
import { memo, useState } from '@wordpress/element';
import { SandBox } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { getPhotoHtml } from './util';
import EmbedNoPreview from './embed-no-preview';
import WpEmbedPreview from './wp-embed-preview';
import styles from './styles.scss';
const EmbedPreview = ( {
align,
className,
clientId,
icon,
insertBlocksAfter,
isSelected,
label,
onFocus,
preview,
previewable,
isProviderPreviewable,
type,
url,
isDefaultEmbedInfo,
} ) => {
const [ isCaptionSelected, setIsCaptionSelected ] = useState( false );
const { locale } = useSelect( blockEditorStore ).getSettings();
const wrapperStyle = styles[ 'embed-preview__wrapper' ];
const wrapperAlignStyle =
styles[ `embed-preview__wrapper--align-${ align }` ];
const sandboxAlignStyle =
styles[ `embed-preview__sandbox--align-${ align }` ];
function accessibilityLabelCreator( caption ) {
return isEmpty( caption )
? /* translators: accessibility text. Empty Embed caption. */
__( 'Embed caption. Empty' )
: sprintf(
/* translators: accessibility text. %s: Embed caption. */
__( 'Embed caption. %s' ),
caption
);
}
function onEmbedPreviewPress() {
setIsCaptionSelected( false );
}
function onFocusCaption() {
if ( onFocus ) {
onFocus();
}
if ( ! isCaptionSelected ) {
setIsCaptionSelected( true );
}
}
const { provider_url: providerUrl } = preview;
const html = 'photo' === type ? getPhotoHtml( preview ) : preview.html;
const parsedHost = new URL( url ).host.split( '.' );
const parsedHostBaseUrl = parsedHost
.splice( parsedHost.length - 2, parsedHost.length - 1 )
.join( '.' );
const iframeTitle = sprintf(
// translators: %s: host providing embed content e.g: www.youtube.com
__( 'Embedded content from %s' ),
parsedHostBaseUrl
);
const sandboxClassnames = classnames(
type,
className,
'wp-block-embed__wrapper'
);
const PreviewContent = 'wp-embed' === type ? WpEmbedPreview : SandBox;
const embedWrapper = (
<>
<TouchableWithoutFeedback
onPress={ () => {
if ( onFocus ) {
onFocus();
}
if ( isCaptionSelected ) {
setIsCaptionSelected( false );
}
} }
>
<View
pointerEvents="box-only"
style={ [ wrapperStyle, wrapperAlignStyle ] }
>
<PreviewContent
html={ html }
lang={ locale }
title={ iframeTitle }
type={ sandboxClassnames }
providerUrl={ providerUrl }
url={ url }
containerStyle={ sandboxAlignStyle }
/>
</View>
</TouchableWithoutFeedback>
</>
);
return (
<TouchableWithoutFeedback
accessible={ ! isSelected }
onPress={ onEmbedPreviewPress }
disabled={ ! isSelected }
>
<View>
{ isProviderPreviewable && previewable ? (
embedWrapper
) : (
<EmbedNoPreview
label={ label }
icon={ icon }
isSelected={ isSelected }
onPress={ () => setIsCaptionSelected( false ) }
previewable={ previewable }
isDefaultEmbedInfo={ isDefaultEmbedInfo }
/>
) }
<BlockCaption
accessibilityLabelCreator={ accessibilityLabelCreator }
accessible
clientId={ clientId }
insertBlocksAfter={ insertBlocksAfter }
isSelected={ isCaptionSelected }
onFocus={ onFocusCaption }
/>
</View>
</TouchableWithoutFeedback>
);
};
export default memo( EmbedPreview );

View File

@@ -0,0 +1,155 @@
/**
* WordPress dependencies
*/
import { G, Path, SVG } from '@wordpress/components';
export const embedContentIcon = (
<SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<Path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm.5 16c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5V9.8l4.7-5.3H19c.3 0 .5.2.5.5v14zm-6-9.5L16 12l-2.5 2.8 1.1 1L18 12l-3.5-3.5-1 1zm-3 0l-1-1L6 12l3.5 3.8 1.1-1L8 12l2.5-2.5z" />
</SVG>
);
export const embedAudioIcon = (
<SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<Path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm.5 16c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5V9.8l4.7-5.3H19c.3 0 .5.2.5.5v14zM13.2 7.7c-.4.4-.7 1.1-.7 1.9v3.7c-.4-.3-.8-.4-1.3-.4-1.2 0-2.2 1-2.2 2.2 0 1.2 1 2.2 2.2 2.2.5 0 1-.2 1.4-.5.9-.6 1.4-1.6 1.4-2.6V9.6c0-.4.1-.6.2-.8.3-.3 1-.3 1.6-.3h.2V7h-.2c-.7 0-1.8 0-2.6.7z" />
</SVG>
);
export const embedPhotoIcon = (
<SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<Path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9.2 4.5H19c.3 0 .5.2.5.5v8.4l-3-2.9c-.3-.3-.8-.3-1 0L11.9 14 9 12c-.3-.2-.6-.2-.8 0l-3.6 2.6V9.8l4.6-5.3zm9.8 15H5c-.3 0-.5-.2-.5-.5v-2.4l4.1-3 3 1.9c.3.2.7.2.9-.1L16 12l3.5 3.4V19c0 .3-.2.5-.5.5z" />
</SVG>
);
export const embedVideoIcon = (
<SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<Path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm.5 16c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5V9.8l4.7-5.3H19c.3 0 .5.2.5.5v14zM10 15l5-3-5-3v6z" />
</SVG>
);
export const embedTwitterIcon = {
foreground: '#1da1f2',
src: (
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<G>
<Path d="M22.23 5.924c-.736.326-1.527.547-2.357.646.847-.508 1.498-1.312 1.804-2.27-.793.47-1.67.812-2.606.996C18.325 4.498 17.258 4 16.078 4c-2.266 0-4.103 1.837-4.103 4.103 0 .322.036.635.106.935-3.41-.17-6.433-1.804-8.457-4.287-.353.607-.556 1.312-.556 2.064 0 1.424.724 2.68 1.825 3.415-.673-.022-1.305-.207-1.86-.514v.052c0 1.988 1.415 3.647 3.293 4.023-.344.095-.707.145-1.08.145-.265 0-.522-.026-.773-.074.522 1.63 2.038 2.817 3.833 2.85-1.404 1.1-3.174 1.757-5.096 1.757-.332 0-.66-.02-.98-.057 1.816 1.164 3.973 1.843 6.29 1.843 7.547 0 11.675-6.252 11.675-11.675 0-.178-.004-.355-.012-.53.802-.578 1.497-1.3 2.047-2.124z"></Path>
</G>
</SVG>
),
};
export const embedYouTubeIcon = {
foreground: '#ff0000',
src: (
<SVG viewBox="0 0 24 24">
<Path d="M21.8 8s-.195-1.377-.795-1.984c-.76-.797-1.613-.8-2.004-.847-2.798-.203-6.996-.203-6.996-.203h-.01s-4.197 0-6.996.202c-.39.046-1.242.05-2.003.846C2.395 6.623 2.2 8 2.2 8S2 9.62 2 11.24v1.517c0 1.618.2 3.237.2 3.237s.195 1.378.795 1.985c.76.797 1.76.77 2.205.855 1.6.153 6.8.2 6.8.2s4.203-.005 7-.208c.392-.047 1.244-.05 2.005-.847.6-.607.795-1.985.795-1.985s.2-1.618.2-3.237v-1.517C22 9.62 21.8 8 21.8 8zM9.935 14.595v-5.62l5.403 2.82-5.403 2.8z" />
</SVG>
),
};
export const embedFacebookIcon = {
foreground: '#3b5998',
src: (
<SVG viewBox="0 0 24 24">
<Path d="M20 3H4c-.6 0-1 .4-1 1v16c0 .5.4 1 1 1h8.6v-7h-2.3v-2.7h2.3v-2c0-2.3 1.4-3.6 3.5-3.6 1 0 1.8.1 2.1.1v2.4h-1.4c-1.1 0-1.3.5-1.3 1.3v1.7h2.7l-.4 2.8h-2.3v7H20c.5 0 1-.4 1-1V4c0-.6-.4-1-1-1z" />
</SVG>
),
};
export const embedInstagramIcon = (
<SVG viewBox="0 0 24 24">
<G>
<Path d="M12 4.622c2.403 0 2.688.01 3.637.052.877.04 1.354.187 1.67.31.42.163.72.358 1.036.673.315.315.51.615.673 1.035.123.317.27.794.31 1.67.043.95.052 1.235.052 3.638s-.01 2.688-.052 3.637c-.04.877-.187 1.354-.31 1.67-.163.42-.358.72-.673 1.036-.315.315-.615.51-1.035.673-.317.123-.794.27-1.67.31-.95.043-1.234.052-3.638.052s-2.688-.01-3.637-.052c-.877-.04-1.354-.187-1.67-.31-.42-.163-.72-.358-1.036-.673-.315-.315-.51-.615-.673-1.035-.123-.317-.27-.794-.31-1.67-.043-.95-.052-1.235-.052-3.638s.01-2.688.052-3.637c.04-.877.187-1.354.31-1.67.163-.42.358-.72.673-1.036.315-.315.615-.51 1.035-.673.317-.123.794-.27 1.67-.31.95-.043 1.235-.052 3.638-.052M12 3c-2.444 0-2.75.01-3.71.054s-1.613.196-2.185.418c-.592.23-1.094.538-1.594 1.04-.5.5-.807 1-1.037 1.593-.223.572-.375 1.226-.42 2.184C3.01 9.25 3 9.555 3 12s.01 2.75.054 3.71.196 1.613.418 2.186c.23.592.538 1.094 1.038 1.594s1.002.808 1.594 1.038c.572.222 1.227.375 2.185.418.96.044 1.266.054 3.71.054s2.75-.01 3.71-.054 1.613-.196 2.186-.418c.592-.23 1.094-.538 1.594-1.038s.808-1.002 1.038-1.594c.222-.572.375-1.227.418-2.185.044-.96.054-1.266.054-3.71s-.01-2.75-.054-3.71-.196-1.613-.418-2.186c-.23-.592-.538-1.094-1.038-1.594s-1.002-.808-1.594-1.038c-.572-.222-1.227-.375-2.185-.418C14.75 3.01 14.445 3 12 3zm0 4.378c-2.552 0-4.622 2.07-4.622 4.622s2.07 4.622 4.622 4.622 4.622-2.07 4.622-4.622S14.552 7.378 12 7.378zM12 15c-1.657 0-3-1.343-3-3s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3zm4.804-8.884c-.596 0-1.08.484-1.08 1.08s.484 1.08 1.08 1.08c.596 0 1.08-.484 1.08-1.08s-.483-1.08-1.08-1.08z"></Path>
</G>
</SVG>
);
export const embedWordPressIcon = {
foreground: '#0073AA',
src: (
<SVG viewBox="0 0 24 24">
<G>
<Path d="M12.158 12.786l-2.698 7.84c.806.236 1.657.365 2.54.365 1.047 0 2.05-.18 2.986-.51-.024-.037-.046-.078-.065-.123l-2.762-7.57zM3.008 12c0 3.56 2.07 6.634 5.068 8.092L3.788 8.342c-.5 1.117-.78 2.354-.78 3.658zm15.06-.454c0-1.112-.398-1.88-.74-2.48-.456-.74-.883-1.368-.883-2.11 0-.825.627-1.595 1.51-1.595.04 0 .078.006.116.008-1.598-1.464-3.73-2.36-6.07-2.36-3.14 0-5.904 1.613-7.512 4.053.21.008.41.012.58.012.94 0 2.395-.114 2.395-.114.484-.028.54.684.057.74 0 0-.487.058-1.03.086l3.275 9.74 1.968-5.902-1.4-3.838c-.485-.028-.944-.085-.944-.085-.486-.03-.43-.77.056-.742 0 0 1.484.114 2.368.114.94 0 2.397-.114 2.397-.114.486-.028.543.684.058.74 0 0-.488.058-1.03.086l3.25 9.665.897-2.997c.456-1.17.684-2.137.684-2.907zm1.82-3.86c.04.286.06.593.06.924 0 .912-.17 1.938-.683 3.22l-2.746 7.94c2.672-1.558 4.47-4.454 4.47-7.77 0-1.564-.4-3.033-1.1-4.314zM12 22C6.486 22 2 17.514 2 12S6.486 2 12 2s10 4.486 10 10-4.486 10-10 10z"></Path>
</G>
</SVG>
),
};
export const embedSpotifyIcon = {
foreground: '#1db954',
src: (
<SVG viewBox="0 0 24 24">
<Path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2m4.586 14.424c-.18.295-.563.387-.857.207-2.35-1.434-5.305-1.76-8.786-.963-.335.077-.67-.133-.746-.47-.077-.334.132-.67.47-.745 3.808-.87 7.076-.496 9.712 1.115.293.18.386.563.206.857M17.81 13.7c-.226.367-.706.482-1.072.257-2.687-1.652-6.785-2.13-9.965-1.166-.413.127-.848-.106-.973-.517-.125-.413.108-.848.52-.973 3.632-1.102 8.147-.568 11.234 1.328.366.226.48.707.256 1.072m.105-2.835C14.692 8.95 9.375 8.775 6.297 9.71c-.493.15-1.016-.13-1.166-.624-.148-.495.13-1.017.625-1.167 3.532-1.073 9.404-.866 13.115 1.337.445.264.59.838.327 1.282-.264.443-.838.59-1.282.325" />
</SVG>
),
};
export const embedFlickrIcon = (
<SVG viewBox="0 0 24 24">
<Path d="m6.5 7c-2.75 0-5 2.25-5 5s2.25 5 5 5 5-2.25 5-5-2.25-5-5-5zm11 0c-2.75 0-5 2.25-5 5s2.25 5 5 5 5-2.25 5-5-2.25-5-5-5z" />
</SVG>
);
export const embedVimeoIcon = {
foreground: '#1ab7ea',
src: (
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<G>
<Path d="M22.396 7.164c-.093 2.026-1.507 4.8-4.245 8.32C15.323 19.16 12.93 21 10.97 21c-1.214 0-2.24-1.12-3.08-3.36-.56-2.052-1.118-4.105-1.68-6.158-.622-2.24-1.29-3.36-2.004-3.36-.156 0-.7.328-1.634.98l-.978-1.26c1.027-.903 2.04-1.806 3.037-2.71C6 3.95 7.03 3.328 7.716 3.265c1.62-.156 2.616.95 2.99 3.32.404 2.558.685 4.148.84 4.77.468 2.12.982 3.18 1.543 3.18.435 0 1.09-.687 1.963-2.064.872-1.376 1.34-2.422 1.402-3.142.125-1.187-.343-1.782-1.4-1.782-.5 0-1.013.115-1.542.34 1.023-3.35 2.977-4.976 5.862-4.883 2.14.063 3.148 1.45 3.024 4.16z"></Path>
</G>
</SVG>
),
};
export const embedRedditIcon = (
<SVG viewBox="0 0 24 24">
<Path d="M22 11.816c0-1.256-1.02-2.277-2.277-2.277-.593 0-1.122.24-1.526.613-1.48-.965-3.455-1.594-5.647-1.69l1.17-3.702 3.18.75c.01 1.027.847 1.86 1.877 1.86 1.035 0 1.877-.84 1.877-1.877 0-1.035-.842-1.877-1.877-1.877-.77 0-1.43.466-1.72 1.13L13.55 3.92c-.204-.047-.4.067-.46.26l-1.35 4.27c-2.317.037-4.412.67-5.97 1.67-.402-.355-.917-.58-1.493-.58C3.02 9.54 2 10.56 2 11.815c0 .814.433 1.523 1.078 1.925-.037.222-.06.445-.06.673 0 3.292 4.01 5.97 8.94 5.97s8.94-2.678 8.94-5.97c0-.214-.02-.424-.052-.632.687-.39 1.154-1.12 1.154-1.964zm-3.224-7.422c.606 0 1.1.493 1.1 1.1s-.493 1.1-1.1 1.1-1.1-.494-1.1-1.1.493-1.1 1.1-1.1zm-16 7.422c0-.827.673-1.5 1.5-1.5.313 0 .598.103.838.27-.85.675-1.477 1.478-1.812 2.36-.32-.274-.525-.676-.525-1.13zm9.183 7.79c-4.502 0-8.165-2.33-8.165-5.193S7.457 9.22 11.96 9.22s8.163 2.33 8.163 5.193-3.663 5.193-8.164 5.193zM20.635 13c-.326-.89-.948-1.7-1.797-2.383.247-.186.55-.3.882-.3.827 0 1.5.672 1.5 1.5 0 .482-.23.91-.586 1.184zm-11.64 1.704c-.76 0-1.397-.616-1.397-1.376 0-.76.636-1.397 1.396-1.397.76 0 1.376.638 1.376 1.398 0 .76-.616 1.376-1.376 1.376zm7.405-1.376c0 .76-.615 1.376-1.375 1.376s-1.4-.616-1.4-1.376c0-.76.64-1.397 1.4-1.397.76 0 1.376.638 1.376 1.398zm-1.17 3.38c.15.152.15.398 0 .55-.675.674-1.728 1.002-3.22 1.002l-.01-.002-.012.002c-1.492 0-2.544-.328-3.218-1.002-.152-.152-.152-.398 0-.55.152-.152.4-.15.55 0 .52.52 1.394.775 2.67.775l.01.002.01-.002c1.276 0 2.15-.253 2.67-.775.15-.152.398-.152.55 0z" />
</SVG>
);
export const embedTumblrIcon = {
foreground: '#35465c',
src: (
<SVG viewBox="0 0 24 24">
<Path d="M19 3H5a2 2 0 00-2 2v14c0 1.1.9 2 2 2h14a2 2 0 002-2V5a2 2 0 00-2-2zm-5.69 14.66c-2.72 0-3.1-1.9-3.1-3.16v-3.56H8.49V8.99c1.7-.62 2.54-1.99 2.64-2.87 0-.06.06-.41.06-.58h1.9v3.1h2.17v2.3h-2.18v3.1c0 .47.13 1.3 1.2 1.26h1.1v2.36c-1.01.02-2.07 0-2.07 0z" />
</SVG>
),
};
export const embedAmazonIcon = (
<SVG viewBox="0 0 24 24">
<Path d="M18.42 14.58c-.51-.66-1.05-1.23-1.05-2.5V7.87c0-1.8.15-3.45-1.2-4.68-1.05-1.02-2.79-1.35-4.14-1.35-2.6 0-5.52.96-6.12 4.14-.06.36.18.54.4.57l2.66.3c.24-.03.42-.27.48-.5.24-1.12 1.17-1.63 2.2-1.63.56 0 1.22.21 1.55.7.4.56.33 1.31.33 1.97v.36c-1.59.18-3.66.27-5.16.93a4.63 4.63 0 0 0-2.93 4.44c0 2.82 1.8 4.23 4.1 4.23 1.95 0 3.03-.45 4.53-1.98.51.72.66 1.08 1.59 1.83.18.09.45.09.63-.1v.04l2.1-1.8c.24-.21.2-.48.03-.75zm-5.4-1.2c-.45.75-1.14 1.23-1.92 1.23-1.05 0-1.65-.81-1.65-1.98 0-2.31 2.1-2.73 4.08-2.73v.6c0 1.05.03 1.92-.5 2.88z" />
<Path d="M21.69 19.2a17.62 17.62 0 0 1-21.6-1.57c-.23-.2 0-.5.28-.33a23.88 23.88 0 0 0 20.93 1.3c.45-.19.84.3.39.6z" />
<Path d="M22.8 17.96c-.36-.45-2.22-.2-3.1-.12-.23.03-.3-.18-.05-.36 1.5-1.05 3.96-.75 4.26-.39.3.36-.1 2.82-1.5 4.02-.21.18-.42.1-.3-.15.3-.8 1.02-2.58.69-3z" />
</SVG>
);
export const embedAnimotoIcon = (
<SVG viewBox="0 0 24 24">
<Path
d="m.0206909 21 19.8160091-13.07806 3.5831 6.20826z"
fill="#4bc7ee"
/>
<Path
d="m23.7254 19.0205-10.1074-17.18468c-.6421-1.114428-1.7087-1.114428-2.3249 0l-11.2931 19.16418h22.5655c1.279 0 1.8019-.8905 1.1599-1.9795z"
fill="#d4cdcb"
/>
<Path
d="m.0206909 21 15.2439091-16.38571 4.3029 7.32271z"
fill="#c3d82e"
/>
<Path
d="m13.618 1.83582c-.6421-1.114428-1.7087-1.114428-2.3249 0l-11.2931 19.16418 15.2646-16.38573z"
fill="#e4ecb0"
/>
<Path d="m.0206909 21 19.5468091-9.063 1.6621 2.8344z" fill="#209dbd" />
<Path
d="m.0206909 21 17.9209091-11.82623 1.6259 2.76323z"
fill="#7cb3c9"
/>
</SVG>
);
export const embedDailymotionIcon = (
<SVG viewBox="0 0 24 24">
<Path
d="m12.1479 18.5957c-2.4949 0-4.28131-1.7558-4.28131-4.0658 0-2.2176 1.78641-4.0965 4.09651-4.0965 2.2793 0 4.0349 1.7864 4.0349 4.1581 0 2.2794-1.7556 4.0042-3.8501 4.0042zm8.3521-18.5957-4.5329 1v7c-1.1088-1.41691-2.8028-1.8787-4.8049-1.8787-2.09443 0-3.97329.76993-5.5133 2.27917-1.72483 1.66323-2.6489 3.78863-2.6489 6.16033 0 2.5873.98562 4.8049 2.89526 6.499 1.44763 1.2936 3.17251 1.9402 5.17454 1.9402 1.9713 0 3.4498-.5236 4.8973-1.9402v1.9402h4.5329c0-7.6359 0-15.3641 0-23z"
fill="#333436"
/>
</SVG>
);
export const embedPinterestIcon = (
<SVG width="24" height="24" viewBox="0 0 24 24" version="1.1">
<Path d="M12.289,2C6.617,2,3.606,5.648,3.606,9.622c0,1.846,1.025,4.146,2.666,4.878c0.25,0.111,0.381,0.063,0.439-0.169 c0.044-0.175,0.267-1.029,0.365-1.428c0.032-0.128,0.017-0.237-0.091-0.362C6.445,11.911,6.01,10.75,6.01,9.668 c0-2.777,2.194-5.464,5.933-5.464c3.23,0,5.49,2.108,5.49,5.122c0,3.407-1.794,5.768-4.13,5.768c-1.291,0-2.257-1.021-1.948-2.277 c0.372-1.495,1.089-3.112,1.089-4.191c0-0.967-0.542-1.775-1.663-1.775c-1.319,0-2.379,1.309-2.379,3.059 c0,1.115,0.394,1.869,0.394,1.869s-1.302,5.279-1.54,6.261c-0.405,1.666,0.053,4.368,0.094,4.604 c0.021,0.126,0.167,0.169,0.25,0.063c0.129-0.165,1.699-2.419,2.142-4.051c0.158-0.59,0.817-2.995,0.817-2.995 c0.43,0.784,1.681,1.446,3.013,1.446c3.963,0,6.822-3.494,6.822-7.833C20.394,5.112,16.849,2,12.289,2" />
</SVG>
);
export const embedWolframIcon = (
<SVG viewBox="0 0 44 44">
<Path d="M32.59521,22.001l4.31885-4.84473-6.34131-1.38379.646-6.459-5.94336,2.61035L22,6.31934l-3.27344,5.60351L12.78418,9.3125l.645,6.458L7.08643,17.15234,11.40479,21.999,7.08594,26.84375l6.34131,1.38379-.64551,6.458,5.94287-2.60938L22,37.68066l3.27344-5.60351,5.94287,2.61035-.64551-6.458,6.34277-1.38183Zm.44385,2.75244L30.772,23.97827l-1.59558-2.07391,1.97888.735Zm-8.82147,6.1579L22.75,33.424V30.88977l1.52228-2.22168ZM18.56226,13.48816,19.819,15.09534l-2.49219-.88642L15.94037,12.337Zm6.87719.00116,2.62043-1.15027-1.38654,1.86981L24.183,15.0946Zm3.59357,2.6029-1.22546,1.7381.07525-2.73486,1.44507-1.94867ZM22,29.33008l-2.16406-3.15686L22,23.23688l2.16406,2.93634Zm-4.25458-9.582-.10528-3.836,3.60986,1.284v3.73242Zm5.00458-2.552,3.60986-1.284-.10528,3.836L22.75,20.92853Zm-7.78174-1.10559-.29352-2.94263,1.44245,1.94739.07519,2.73321Zm2.30982,5.08319,3.50817,1.18164-2.16247,2.9342-3.678-1.08447Zm2.4486,7.49285L21.25,30.88977v2.53485L19.78052,30.91Zm3.48707-6.31121,3.50817-1.18164,2.33228,3.03137-3.678,1.08447Zm10.87219-4.28113-2.714,3.04529L28.16418,19.928l1.92176-2.72565ZM24.06036,12.81769l-2.06012,2.6322-2.059-2.63318L22,9.292ZM9.91455,18.07227l4.00079-.87195,1.921,2.72735-3.20794,1.19019Zm2.93024,4.565,1.9801-.73462L13.228,23.97827l-2.26838.77429Zm-1.55591,3.58819L13.701,25.4021l2.64935.78058-2.14447.67853Zm3.64868,1.977L18.19,27.17334l.08313,3.46332L14.52979,32.2793Zm10.7876,2.43549.08447-3.464,3.25165,1.03052.407,4.07684Zm4.06824-3.77478-2.14545-.68,2.65063-.781,2.41266.825Z" />
</SVG>
);

View File

@@ -0,0 +1,22 @@
/**
* Internal dependencies
*/
import edit from './edit';
import save from './save';
import metadata from './block.json';
import transforms from './transforms';
import variations from './variations';
import deprecated from './deprecated';
import { embedContentIcon } from './icons';
const { name } = metadata;
export { metadata, name };
export const settings = {
icon: embedContentIcon,
edit,
save,
transforms,
variations,
deprecated,
};

View File

@@ -0,0 +1,34 @@
/**
* External dependencies
*/
import classnames from 'classnames/dedupe';
/**
* WordPress dependencies
*/
import { RichText, useBlockProps } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const { url, caption, type, providerNameSlug } = attributes;
if ( ! url ) {
return null;
}
const className = classnames( 'wp-block-embed', {
[ `is-type-${ type }` ]: type,
[ `is-provider-${ providerNameSlug }` ]: providerNameSlug,
[ `wp-block-embed-${ providerNameSlug }` ]: providerNameSlug,
} );
return (
<figure { ...useBlockProps.save( { className } ) }>
<div className="wp-block-embed__wrapper">
{ `\n${ url }\n` /* URL needs to be on its own line. */ }
</div>
{ ! RichText.isEmpty( caption ) && (
<RichText.Content tagName="figcaption" value={ caption } />
) }
</figure>
);
}

View File

@@ -0,0 +1,90 @@
// Apply max-width to floated items that have no intrinsic width
.wp-block[data-align="left"] > [data-type="core/embed"],
.wp-block[data-align="right"] > [data-type="core/embed"],
.wp-block-embed.alignleft,
.wp-block-embed.alignright {
// Instagram widgets have a min-width of 326px, so go a bit beyond that.
max-width: 360px;
width: 100%;
// Unless these have a min-width, they collapse when floated.
.wp-block-embed__wrapper {
min-width: $break-zoomed-in;
}
}
// Supply a min-width when inside a cover block, to prevent it from collapsing.
.wp-block-cover .wp-block-embed {
min-width: 320px;
min-height: 240px;
}
.wp-block-embed {
margin: 0 0 1em 0;
overflow-wrap: break-word; // Break long strings of text without spaces so they don't overflow the block.
// Supply caption styles to embeds, even if the theme hasn't opted in.
// Reason being: the new markup, figcaptions, are not likely to be styled in the majority of existing themes,
// so we supply the styles so as to not appear broken or unstyled in those.
figcaption {
@include caption-style();
}
// Don't allow iframe to overflow it's container.
iframe {
max-width: 100%;
}
}
.wp-block-embed__wrapper {
position: relative;
}
// Add responsiveness to embeds with aspect ratios.
.wp-embed-responsive .wp-has-aspect-ratio {
.wp-block-embed__wrapper::before {
content: "";
display: block;
padding-top: 50%; // Default to 2:1 aspect ratio.
}
iframe {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
height: 100%;
width: 100%;
}
}
.wp-embed-responsive {
.wp-embed-aspect-21-9 .wp-block-embed__wrapper::before {
padding-top: 42.85%; // 9 / 21 * 100
}
.wp-embed-aspect-18-9 .wp-block-embed__wrapper::before {
padding-top: 50%; // 9 / 18 * 100
}
.wp-embed-aspect-16-9 .wp-block-embed__wrapper::before {
padding-top: 56.25%; // 9 / 16 * 100
}
.wp-embed-aspect-4-3 .wp-block-embed__wrapper::before {
padding-top: 75%; // 3 / 4 * 100
}
.wp-embed-aspect-1-1 .wp-block-embed__wrapper::before {
padding-top: 100%; // 1 / 1 * 100
}
.wp-embed-aspect-9-16 .wp-block-embed__wrapper::before {
padding-top: 177.77%; // 16 / 9 * 100
}
.wp-embed-aspect-1-2 .wp-block-embed__wrapper::before {
padding-top: 200%; // 2 / 1 * 100
}
}

View File

@@ -0,0 +1,160 @@
.embed__container {
flex: 1;
min-height: 142;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: $gray-lighten-30;
padding-left: 12;
padding-right: 12;
padding-top: 12;
padding-bottom: 12;
border-top-left-radius: 4;
border-top-right-radius: 4;
border-bottom-left-radius: 4;
border-bottom-right-radius: 4;
}
.embed__container--dark {
background-color: $background-dark-secondary;
}
.embed__icon--error {
margin-bottom: 6;
fill: $alert-red;
}
.embed__label {
text-align: center;
margin-top: 4;
margin-bottom: 4;
font-size: 14;
font-weight: 500;
color: $gray-90;
}
.embed__label--dark {
color: $gray-10;
}
.embed__description {
font-size: $default-font-size;
text-align: center;
margin-bottom: 4;
color: $light-secondary;
}
.embed__description--dark {
color: $dark-secondary;
}
.embed__description--error {
color: $alert-red;
}
.embed__action {
width: 100%;
text-align: center;
color: $blue-wordpress;
font-size: 14;
font-weight: 500;
margin-top: 4;
}
.embed-preview__loading {
flex: 1;
padding-left: 12px;
padding-right: 12px;
padding-top: 40px;
padding-bottom: 40px;
background-color: $gray-lighten-30;
justify-content: center;
align-items: center;
}
.embed-preview__wrapper {
flex: 1;
}
.embed-preview__wrapper--align-left {
align-items: flex-start;
}
.embed-preview__wrapper--align-right {
align-items: flex-end;
}
.embed-preview__sandbox--align-left {
max-width: 360px;
}
.embed-preview__sandbox--align-right {
max-width: 360px;
}
.embed-preview__loading--dark {
background-color: $background-dark-secondary;
}
.embed-no-preview__container {
flex-direction: column;
align-items: center;
justify-content: flex-end;
}
.embed-no-preview__help-icon {
position: absolute;
top: 12;
right: 12;
width: 16;
height: 16;
fill: $light-secondary;
}
.embed-no-preview__help-icon--dark {
fill: $gray-20;
}
.embed-no-preview__sheet-icon {
width: 36;
height: 36;
fill: $light-quaternary;
}
.embed-no-preview__sheet-icon--dark {
fill: $gray-20;
}
.embed-no-preview__sheet-title {
font-weight: 600;
font-size: 20;
line-height: 28px;
text-align: center;
color: $gray-90;
padding-top: 8;
padding-bottom: 8;
}
.embed-no-preview__sheet-title--dark {
color: $white;
}
.embed-no-preview__sheet-description {
font-size: 16;
line-height: 24px;
text-align: center;
color: $gray-50;
padding-bottom: 24;
}
.embed-no-preview__sheet-description--dark {
color: $gray-20;
}
.embed-no-preview__sheet-button {
color: $blue-50;
}
.embed-no-preview__sheet-button--dark {
color: $blue-30;
}

View File

@@ -0,0 +1,191 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Embed block alignment options sets Align center option 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true,\\"align\\":\\"center\\"} -->
<figure class=\\"wp-block-embed aligncenter is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block alignment options sets Align left option 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true,\\"align\\":\\"left\\"} -->
<figure class=\\"wp-block-embed alignleft is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block alignment options sets Align right option 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true,\\"align\\":\\"right\\"} -->
<figure class=\\"wp-block-embed alignright is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block alignment options sets Full width option 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true,\\"align\\":\\"full\\"} -->
<figure class=\\"wp-block-embed alignfull is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block alignment options sets Wide width option 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true,\\"align\\":\\"wide\\"} -->
<figure class=\\"wp-block-embed alignwide is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block block settings toggles resize for smaller devices media settings 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"allowResponsive\\":false,\\"responsive\\":true} -->
<figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block create by pasting URL creates embed block when pasting URL in paragraph block 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://www.youtube.com/watch?v=lXMskKTw3Bc\\",\\"type\\":\\"video\\",\\"providerNameSlug\\":\\"youtube\\",\\"responsive\\":true,\\"className\\":\\"wp-embed-aspect-16-9 wp-has-aspect-ratio\\"} -->
<figure class=\\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\\"><div class=\\"wp-block-embed__wrapper\\">
https://www.youtube.com/watch?v=lXMskKTw3Bc
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block create by pasting URL creates link when pasting URL in paragraph block 1`] = `
"<!-- wp:paragraph -->
<p><a href=\\"https://www.youtube.com/watch?v=lXMskKTw3Bc\\">https://www.youtube.com/watch?v=lXMskKTw3Bc</a></p>
<!-- /wp:paragraph -->"
`;
exports[`Embed block displays cannot embed on the placeholder if preview data is null 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/testing\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} -->
<figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/testing
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block edit URL edits URL when edited after setting a bad URL of a provider 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} -->
<figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block edit URL keeps the previous URL if an invalid URL is set 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} -->
<figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block edit URL keeps the previous URL if no URL is set 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} -->
<figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block edit URL replaces URL 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://www.youtube.com/watch?v=lXMskKTw3Bc\\",\\"type\\":\\"video\\",\\"providerNameSlug\\":\\"youtube\\",\\"responsive\\":true,\\"className\\":\\"wp-embed-aspect-16-9 wp-has-aspect-ratio\\"} -->
<figure class=\\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\\"><div class=\\"wp-block-embed__wrapper\\">
https://www.youtube.com/watch?v=lXMskKTw3Bc
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block edit URL sets empty state when setting an empty URL 1`] = `"<!-- wp:embed {\\"url\\":\\"\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} /-->"`;
exports[`Embed block insert via slash inserter insert generic embed block 1`] = `"<!-- wp:embed /-->"`;
exports[`Embed block insert via slash inserter inserts Twitter embed block 1`] = `"<!-- wp:embed {\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} /-->"`;
exports[`Embed block insert via slash inserter inserts Vimeo embed block 1`] = `"<!-- wp:embed {\\"providerNameSlug\\":\\"vimeo\\",\\"responsive\\":true} /-->"`;
exports[`Embed block insert via slash inserter inserts WordPress embed block 1`] = `"<!-- wp:embed {\\"providerNameSlug\\":\\"wordpress\\"} /-->"`;
exports[`Embed block insert via slash inserter inserts YouTube embed block 1`] = `"<!-- wp:embed {\\"providerNameSlug\\":\\"youtube\\",\\"responsive\\":true} /-->"`;
exports[`Embed block insertion inserts Twitter embed block 1`] = `"<!-- wp:embed {\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} /-->"`;
exports[`Embed block insertion inserts Vimeo embed block 1`] = `"<!-- wp:embed {\\"providerNameSlug\\":\\"vimeo\\",\\"responsive\\":true} /-->"`;
exports[`Embed block insertion inserts WordPress embed block 1`] = `"<!-- wp:embed {\\"providerNameSlug\\":\\"wordpress\\"} /-->"`;
exports[`Embed block insertion inserts YouTube embed block 1`] = `"<!-- wp:embed {\\"providerNameSlug\\":\\"youtube\\",\\"responsive\\":true} /-->"`;
exports[`Embed block insertion inserts generic embed block 1`] = `"<!-- wp:embed /-->"`;
exports[`Embed block retry allows editing link if request failed 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\"} -->
<figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block retry converts to link if preview request failed 1`] = `
"<!-- wp:paragraph -->
<p><a href=\\"https://twitter.com/notnownikki\\">https://twitter.com/notnownikki</a></p>
<!-- /wp:paragraph -->"
`;
exports[`Embed block retry retries loading the preview if initial request failed 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} -->
<figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block set URL upon block insertion auto-pastes the URL from clipboard 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} -->
<figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block set URL upon block insertion sets a valid URL when dismissing edit URL modal 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} -->
<figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block set URL upon block insertion sets empty URL when dismissing edit URL modal 1`] = `"<!-- wp:embed {\\"url\\":\\"\\"} /-->"`;
exports[`Embed block set URL when empty block auto-pastes the URL from clipboard 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} -->
<figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block set URL when empty block sets a valid URL when dismissing edit URL modal 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} -->
<figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div></figure>
<!-- /wp:embed -->"
`;
exports[`Embed block set URL when empty block sets empty URL when dismissing edit URL modal 1`] = `"<!-- wp:embed {\\"url\\":\\"\\"} /-->"`;
exports[`Embed block sets block caption 1`] = `
"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} -->
<figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\">
https://twitter.com/notnownikki
</div><figcaption>Caption</figcaption></figure>
<!-- /wp:embed -->"
`;

View File

@@ -0,0 +1,201 @@
/**
* WordPress dependencies
*/
import {
registerBlockType,
unregisterBlockType,
registerBlockVariation,
unregisterBlockVariation,
} from '@wordpress/blocks';
/**
* Internal dependencies
*/
import {
findMoreSuitableBlock,
getClassNames,
createUpgradedEmbedBlock,
getEmbedInfoByProvider,
removeAspectRatioClasses,
} from '../util';
import { embedInstagramIcon } from '../icons';
import variations from '../variations';
import metadata from '../block.json';
const { name: DEFAULT_EMBED_BLOCK, attributes } = metadata;
jest.mock( '@wordpress/data/src/components/use-select', () => () => ( {} ) );
describe( 'utils', () => {
beforeAll( () => {
registerBlockType( DEFAULT_EMBED_BLOCK, {
title: 'Embed',
category: 'embed',
attributes,
variations,
} );
} );
afterAll( () => {
unregisterBlockType( DEFAULT_EMBED_BLOCK );
} );
describe( 'findMoreSuitableBlock', () => {
test( 'findMoreSuitableBlock matches a URL to a block name', () => {
const twitterURL = 'https://twitter.com/notnownikki';
const youtubeURL = 'https://www.youtube.com/watch?v=bNnfuvC1LlU';
const unknownURL = 'https://example.com/';
expect( findMoreSuitableBlock( twitterURL ) ).toEqual(
expect.objectContaining( { name: 'twitter' } )
);
expect( findMoreSuitableBlock( youtubeURL ) ).toEqual(
expect.objectContaining( { name: 'youtube' } )
);
expect( findMoreSuitableBlock( unknownURL ) ).toBeUndefined();
} );
} );
describe( 'getClassNames', () => {
it( 'should return aspect ratio class names for iframes with width and height', () => {
const html = '<iframe height="9" width="16"></iframe>';
const expected = 'wp-embed-aspect-16-9 wp-has-aspect-ratio';
expect( getClassNames( html ) ).toEqual( expected );
} );
it( 'should not return aspect ratio class names if we do not allow responsive', () => {
const html = '<iframe height="9" width="16"></iframe>';
const expected = '';
expect( getClassNames( html, '', false ) ).toEqual( expected );
} );
it( 'should preserve exsiting class names when removing responsive classes', () => {
const html = '<iframe height="9" width="16"></iframe>';
const expected = 'lovely';
expect(
getClassNames(
html,
'lovely wp-embed-aspect-16-9 wp-has-aspect-ratio',
false
)
).toEqual( expected );
} );
it( 'should return the same falsy value as passed for existing classes when no new classes are added', () => {
const html = '<iframe></iframe>';
const expected = undefined;
expect( getClassNames( html, undefined, false ) ).toEqual(
expected
);
} );
it( 'should preserve existing classes and replace aspect ratio related classes with the current embed preview', () => {
const html = '<iframe height="3" width="4"></iframe>';
const expected =
'wp-block-embed wp-embed-aspect-4-3 wp-has-aspect-ratio';
expect(
getClassNames(
html,
'wp-block-embed wp-embed-aspect-16-9 wp-has-aspect-ratio',
true
)
).toEqual( expected );
} );
} );
describe( 'removeAspectRatioClasses', () => {
it( 'should return the same falsy value as received', () => {
const existingClassNames = undefined;
expect( removeAspectRatioClasses( existingClassNames ) ).toEqual(
existingClassNames
);
} );
it( 'should preserve existing classes, if no aspect ratio classes exist', () => {
const existingClassNames = 'wp-block-embed is-type-video';
expect( removeAspectRatioClasses( existingClassNames ) ).toEqual(
existingClassNames
);
} );
it( 'should remove the aspect ratio classes', () => {
const existingClassNames =
'wp-block-embed is-type-video wp-embed-aspect-16-9 wp-has-aspect-ratio';
expect( removeAspectRatioClasses( existingClassNames ) ).toEqual(
'wp-block-embed is-type-video'
);
} );
} );
describe( 'createUpgradedEmbedBlock', () => {
describe( 'do not create new block', () => {
it( 'when block type does not exist', () => {
const youtubeURL = 'https://www.youtube.com/watch?v=dQw4w';
unregisterBlockType( DEFAULT_EMBED_BLOCK );
expect(
createUpgradedEmbedBlock( {
attributes: { url: youtubeURL },
} )
).toBeUndefined();
registerBlockType( DEFAULT_EMBED_BLOCK, {
title: 'Embed',
category: 'embed',
attributes,
variations,
} );
} );
it( 'when block variation does not exist', () => {
const youtubeURL = 'https://www.youtube.com/watch?v=dQw4w';
unregisterBlockVariation( DEFAULT_EMBED_BLOCK, 'youtube' );
expect(
createUpgradedEmbedBlock( {
attributes: { url: youtubeURL },
} )
).toBeUndefined();
registerBlockVariation(
DEFAULT_EMBED_BLOCK,
variations.find( ( { name } ) => name === 'youtube' )
);
} );
it( 'when no url provided', () => {
expect(
createUpgradedEmbedBlock( { name: 'some name' } )
).toBeUndefined();
} );
} );
it( 'should return a YouTube embed block when given a YouTube URL', () => {
const youtubeURL = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
const result = createUpgradedEmbedBlock( {
attributes: { url: youtubeURL },
} );
expect( result ).toEqual(
expect.objectContaining( {
name: DEFAULT_EMBED_BLOCK,
attributes: expect.objectContaining( {
providerNameSlug: 'youtube',
} ),
} )
);
} );
} );
describe( 'getEmbedInfoByProvider', () => {
it( 'should return embed info from existent variation', () => {
expect( getEmbedInfoByProvider( 'instagram' ) ).toEqual(
expect.objectContaining( {
icon: embedInstagramIcon,
title: 'Instagram',
} )
);
} );
it( 'should return undefined if not found in variations', () => {
expect(
getEmbedInfoByProvider( 'i do not exist' )
).toBeUndefined();
} );
} );
} );

View File

@@ -0,0 +1,3 @@
.wp-block-embed figcaption {
@include caption-style-theme();
}

View File

@@ -0,0 +1,49 @@
/**
* WordPress dependencies
*/
import { createBlock } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import metadata from './block.json';
const { name: EMBED_BLOCK } = metadata;
/**
* Default transforms for generic embeds.
*/
const transforms = {
from: [
{
type: 'raw',
isMatch: ( node ) =>
node.nodeName === 'P' &&
/^\s*(https?:\/\/\S+)\s*$/i.test( node.textContent ) &&
node.textContent?.match( /https/gi )?.length === 1,
transform: ( node ) => {
return createBlock( EMBED_BLOCK, {
url: node.textContent.trim(),
} );
},
},
],
to: [
{
type: 'block',
blocks: [ 'core/paragraph' ],
isMatch: ( { url } ) => !! url,
transform: ( { url, caption } ) => {
let value = `<a href="${ url }">${ url }</a>`;
if ( caption?.trim() ) {
value += `<br />${ caption }`;
}
return createBlock( 'core/paragraph', {
content: value,
} );
},
},
],
};
export default transforms;

View File

@@ -0,0 +1,12 @@
/**
* Internal dependencies
*/
import webTransforms from './transforms.js';
import transformationCategories from '../transformationCategories';
const transforms = {
...webTransforms,
supportedMobileTransforms: transformationCategories.media,
};
export default transforms;

View File

@@ -0,0 +1,292 @@
/**
* Internal dependencies
*/
import { ASPECT_RATIOS, WP_EMBED_TYPE } from './constants';
/**
* External dependencies
*/
import { kebabCase } from 'lodash';
import classnames from 'classnames/dedupe';
import memoize from 'memize';
/**
* WordPress dependencies
*/
import { renderToString } from '@wordpress/element';
import {
createBlock,
getBlockType,
getBlockVariations,
} from '@wordpress/blocks';
/**
* Internal dependencies
*/
import metadata from './block.json';
const { name: DEFAULT_EMBED_BLOCK } = metadata;
/** @typedef {import('@wordpress/blocks').WPBlockVariation} WPBlockVariation */
/**
* Returns the embed block's information by matching the provided service provider
*
* @param {string} provider The embed block's provider
* @return {WPBlockVariation} The embed block's information
*/
export const getEmbedInfoByProvider = ( provider ) =>
getBlockVariations( DEFAULT_EMBED_BLOCK )?.find(
( { name } ) => name === provider
);
/**
* Returns true if any of the regular expressions match the URL.
*
* @param {string} url The URL to test.
* @param {Array} patterns The list of regular expressions to test agains.
* @return {boolean} True if any of the regular expressions match the URL.
*/
export const matchesPatterns = ( url, patterns = [] ) =>
patterns.some( ( pattern ) => url.match( pattern ) );
/**
* Finds the block variation that should be used for the URL,
* based on the provided URL and the variation's patterns.
*
* @param {string} url The URL to test.
* @return {WPBlockVariation} The block variation that should be used for this URL
*/
export const findMoreSuitableBlock = ( url ) =>
getBlockVariations( DEFAULT_EMBED_BLOCK )?.find( ( { patterns } ) =>
matchesPatterns( url, patterns )
);
export const isFromWordPress = ( html ) =>
html && html.includes( 'class="wp-embedded-content"' );
export const getPhotoHtml = ( photo ) => {
// 100% width for the preview so it fits nicely into the document, some "thumbnails" are
// actually the full size photo. If thumbnails not found, use full image.
const imageUrl = photo.thumbnail_url || photo.url;
const photoPreview = (
<p>
<img src={ imageUrl } alt={ photo.title } width="100%" />
</p>
);
return renderToString( photoPreview );
};
/**
* Creates a more suitable embed block based on the passed in props
* and attributes generated from an embed block's preview.
*
* We require `attributesFromPreview` to be generated from the latest attributes
* and preview, and because of the way the react lifecycle operates, we can't
* guarantee that the attributes contained in the block's props are the latest
* versions, so we require that these are generated separately.
* See `getAttributesFromPreview` in the generated embed edit component.
*
* @param {Object} props The block's props.
* @param {Object} [attributesFromPreview] Attributes generated from the block's most up to date preview.
* @return {Object|undefined} A more suitable embed block if one exists.
*/
export const createUpgradedEmbedBlock = (
props,
attributesFromPreview = {}
) => {
const { preview, attributes = {} } = props;
const { url, providerNameSlug, type, ...restAttributes } = attributes;
if ( ! url || ! getBlockType( DEFAULT_EMBED_BLOCK ) ) return;
const matchedBlock = findMoreSuitableBlock( url );
// WordPress blocks can work on multiple sites, and so don't have patterns,
// so if we're in a WordPress block, assume the user has chosen it for a WordPress URL.
const isCurrentBlockWP =
providerNameSlug === 'wordpress' || type === WP_EMBED_TYPE;
// If current block is not WordPress and a more suitable block found
// that is different from the current one, create the new matched block.
const shouldCreateNewBlock =
! isCurrentBlockWP &&
matchedBlock &&
( matchedBlock.attributes.providerNameSlug !== providerNameSlug ||
! providerNameSlug );
if ( shouldCreateNewBlock ) {
return createBlock( DEFAULT_EMBED_BLOCK, {
url,
...restAttributes,
...matchedBlock.attributes,
} );
}
const wpVariation = getBlockVariations( DEFAULT_EMBED_BLOCK )?.find(
( { name } ) => name === 'wordpress'
);
// We can't match the URL for WordPress embeds, we have to check the HTML instead.
if (
! wpVariation ||
! preview ||
! isFromWordPress( preview.html ) ||
isCurrentBlockWP
) {
return;
}
// This is not the WordPress embed block so transform it into one.
return createBlock( DEFAULT_EMBED_BLOCK, {
url,
...wpVariation.attributes,
// By now we have the preview, but when the new block first renders, it
// won't have had all the attributes set, and so won't get the correct
// type and it won't render correctly. So, we pass through the current attributes
// here so that the initial render works when we switch to the WordPress
// block. This only affects the WordPress block because it can't be
// rendered in the usual Sandbox (it has a sandbox of its own) and it
// relies on the preview to set the correct render type.
...attributesFromPreview,
} );
};
/**
* Removes all previously set aspect ratio related classes and return the rest
* existing class names.
*
* @param {string} existingClassNames Any existing class names.
* @return {string} The class names without any aspect ratio related class.
*/
export const removeAspectRatioClasses = ( existingClassNames ) => {
if ( ! existingClassNames ) {
// Avoids extraneous work and also, by returning the same value as
// received, ensures the post is not dirtied by a change of the block
// attribute from `undefined` to an emtpy string.
return existingClassNames;
}
const aspectRatioClassNames = ASPECT_RATIOS.reduce(
( accumulator, { className } ) => {
accumulator[ className ] = false;
return accumulator;
},
{ 'wp-has-aspect-ratio': false }
);
return classnames( existingClassNames, aspectRatioClassNames );
};
/**
* Returns class names with any relevant responsive aspect ratio names.
*
* @param {string} html The preview HTML that possibly contains an iframe with width and height set.
* @param {string} existingClassNames Any existing class names.
* @param {boolean} allowResponsive If the responsive class names should be added, or removed.
* @return {string} Deduped class names.
*/
export function getClassNames(
html,
existingClassNames,
allowResponsive = true
) {
if ( ! allowResponsive ) {
return removeAspectRatioClasses( existingClassNames );
}
const previewDocument = document.implementation.createHTMLDocument( '' );
previewDocument.body.innerHTML = html;
const iframe = previewDocument.body.querySelector( 'iframe' );
// If we have a fixed aspect iframe, and it's a responsive embed block.
if ( iframe && iframe.height && iframe.width ) {
const aspectRatio = ( iframe.width / iframe.height ).toFixed( 2 );
// Given the actual aspect ratio, find the widest ratio to support it.
for (
let ratioIndex = 0;
ratioIndex < ASPECT_RATIOS.length;
ratioIndex++
) {
const potentialRatio = ASPECT_RATIOS[ ratioIndex ];
if ( aspectRatio >= potentialRatio.ratio ) {
// Evaluate the difference between actual aspect ratio and closest match.
// If the difference is too big, do not scale the embed according to aspect ratio.
const ratioDiff = aspectRatio - potentialRatio.ratio;
if ( ratioDiff > 0.1 ) {
// No close aspect ratio match found.
return removeAspectRatioClasses( existingClassNames );
}
// Close aspect ratio match found.
return classnames(
removeAspectRatioClasses( existingClassNames ),
potentialRatio.className,
'wp-has-aspect-ratio'
);
}
}
}
return existingClassNames;
}
/**
* Fallback behaviour for unembeddable URLs.
* Creates a paragraph block containing a link to the URL, and calls `onReplace`.
*
* @param {string} url The URL that could not be embedded.
* @param {Function} onReplace Function to call with the created fallback block.
*/
export function fallback( url, onReplace ) {
const link = <a href={ url }>{ url }</a>;
onReplace(
createBlock( 'core/paragraph', { content: renderToString( link ) } )
);
}
/***
* Gets block attributes based on the preview and responsive state.
*
* @param {Object} preview The preview data.
* @param {string} title The block's title, e.g. Twitter.
* @param {Object} currentClassNames The block's current class names.
* @param {boolean} isResponsive Boolean indicating if the block supports responsive content.
* @param {boolean} allowResponsive Apply responsive classes to fixed size content.
* @return {Object} Attributes and values.
*/
export const getAttributesFromPreview = memoize(
(
preview,
title,
currentClassNames,
isResponsive,
allowResponsive = true
) => {
if ( ! preview ) {
return {};
}
const attributes = {};
// Some plugins only return HTML with no type info, so default this to 'rich'.
let { type = 'rich' } = preview;
// If we got a provider name from the API, use it for the slug, otherwise we use the title,
// because not all embed code gives us a provider name.
const { html, provider_name: providerName } = preview;
const providerNameSlug = kebabCase(
( providerName || title ).toLowerCase()
);
if ( isFromWordPress( html ) ) {
type = WP_EMBED_TYPE;
}
if ( html || 'photo' === type ) {
attributes.type = type;
attributes.providerNameSlug = providerNameSlug;
}
attributes.className = getClassNames(
html,
currentClassNames,
isResponsive && allowResponsive
);
return attributes;
}
);

View File

@@ -0,0 +1,367 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import {
embedContentIcon,
embedAudioIcon,
embedPhotoIcon,
embedVideoIcon,
embedTwitterIcon,
embedYouTubeIcon,
embedFacebookIcon,
embedInstagramIcon,
embedWordPressIcon,
embedSpotifyIcon,
embedFlickrIcon,
embedVimeoIcon,
embedRedditIcon,
embedTumblrIcon,
embedAmazonIcon,
embedAnimotoIcon,
embedDailymotionIcon,
embedPinterestIcon,
embedWolframIcon,
} from './icons';
/** @typedef {import('@wordpress/blocks').WPBlockVariation} WPBlockVariation */
/**
* Template option choices for predefined columns layouts.
*
* @type {WPBlockVariation[]}
*/
const variations = [
{
name: 'twitter',
title: 'Twitter',
icon: embedTwitterIcon,
keywords: [ 'tweet', __( 'social' ) ],
description: __( 'Embed a tweet.' ),
patterns: [ /^https?:\/\/(www\.)?twitter\.com\/.+/i ],
attributes: { providerNameSlug: 'twitter', responsive: true },
},
{
name: 'youtube',
title: 'YouTube',
icon: embedYouTubeIcon,
keywords: [ __( 'music' ), __( 'video' ) ],
description: __( 'Embed a YouTube video.' ),
patterns: [
/^https?:\/\/((m|www)\.)?youtube\.com\/.+/i,
/^https?:\/\/youtu\.be\/.+/i,
],
attributes: { providerNameSlug: 'youtube', responsive: true },
},
{
// Deprecate Facebook Embed per FB policy
// See: https://developers.facebook.com/docs/plugins/oembed-legacy
name: 'facebook',
title: 'Facebook',
icon: embedFacebookIcon,
keywords: [ __( 'social' ) ],
description: __( 'Embed a Facebook post.' ),
scope: [ 'block' ],
patterns: [],
attributes: {
providerNameSlug: 'facebook',
previewable: false,
responsive: true,
},
},
{
// Deprecate Instagram per FB policy
// See: https://developers.facebook.com/docs/instagram/oembed-legacy
name: 'instagram',
title: 'Instagram',
icon: embedInstagramIcon,
keywords: [ __( 'image' ), __( 'social' ) ],
description: __( 'Embed an Instagram post.' ),
scope: [ 'block' ],
patterns: [],
attributes: { providerNameSlug: 'instagram', responsive: true },
},
{
name: 'wordpress',
title: 'WordPress',
icon: embedWordPressIcon,
keywords: [ __( 'post' ), __( 'blog' ) ],
description: __( 'Embed a WordPress post.' ),
attributes: {
providerNameSlug: 'wordpress',
},
},
{
name: 'soundcloud',
title: 'SoundCloud',
icon: embedAudioIcon,
keywords: [ __( 'music' ), __( 'audio' ) ],
description: __( 'Embed SoundCloud content.' ),
patterns: [ /^https?:\/\/(www\.)?soundcloud\.com\/.+/i ],
attributes: { providerNameSlug: 'soundcloud', responsive: true },
},
{
name: 'spotify',
title: 'Spotify',
icon: embedSpotifyIcon,
keywords: [ __( 'music' ), __( 'audio' ) ],
description: __( 'Embed Spotify content.' ),
patterns: [ /^https?:\/\/(open|play)\.spotify\.com\/.+/i ],
attributes: { providerNameSlug: 'spotify', responsive: true },
},
{
name: 'flickr',
title: 'Flickr',
icon: embedFlickrIcon,
keywords: [ __( 'image' ) ],
description: __( 'Embed Flickr content.' ),
patterns: [
/^https?:\/\/(www\.)?flickr\.com\/.+/i,
/^https?:\/\/flic\.kr\/.+/i,
],
attributes: { providerNameSlug: 'flickr', responsive: true },
},
{
name: 'vimeo',
title: 'Vimeo',
icon: embedVimeoIcon,
keywords: [ __( 'video' ) ],
description: __( 'Embed a Vimeo video.' ),
patterns: [ /^https?:\/\/(www\.)?vimeo\.com\/.+/i ],
attributes: { providerNameSlug: 'vimeo', responsive: true },
},
{
name: 'animoto',
title: 'Animoto',
icon: embedAnimotoIcon,
description: __( 'Embed an Animoto video.' ),
patterns: [ /^https?:\/\/(www\.)?(animoto|video214)\.com\/.+/i ],
attributes: { providerNameSlug: 'animoto', responsive: true },
},
{
name: 'cloudup',
title: 'Cloudup',
icon: embedContentIcon,
description: __( 'Embed Cloudup content.' ),
patterns: [ /^https?:\/\/cloudup\.com\/.+/i ],
attributes: { providerNameSlug: 'cloudup', responsive: true },
},
{
// Deprecated since CollegeHumor content is now powered by YouTube.
name: 'collegehumor',
title: 'CollegeHumor',
icon: embedVideoIcon,
description: __( 'Embed CollegeHumor content.' ),
scope: [ 'block' ],
patterns: [],
attributes: { providerNameSlug: 'collegehumor', responsive: true },
},
{
name: 'crowdsignal',
title: 'Crowdsignal',
icon: embedContentIcon,
keywords: [ 'polldaddy', __( 'survey' ) ],
description: __( 'Embed Crowdsignal (formerly Polldaddy) content.' ),
patterns: [
/^https?:\/\/((.+\.)?polldaddy\.com|poll\.fm|.+\.survey\.fm)\/.+/i,
],
attributes: { providerNameSlug: 'crowdsignal', responsive: true },
},
{
name: 'dailymotion',
title: 'Dailymotion',
icon: embedDailymotionIcon,
keywords: [ __( 'video' ) ],
description: __( 'Embed a Dailymotion video.' ),
patterns: [ /^https?:\/\/(www\.)?dailymotion\.com\/.+/i ],
attributes: { providerNameSlug: 'dailymotion', responsive: true },
},
{
name: 'imgur',
title: 'Imgur',
icon: embedPhotoIcon,
description: __( 'Embed Imgur content.' ),
patterns: [ /^https?:\/\/(.+\.)?imgur\.com\/.+/i ],
attributes: { providerNameSlug: 'imgur', responsive: true },
},
{
name: 'issuu',
title: 'Issuu',
icon: embedContentIcon,
description: __( 'Embed Issuu content.' ),
patterns: [ /^https?:\/\/(www\.)?issuu\.com\/.+/i ],
attributes: { providerNameSlug: 'issuu', responsive: true },
},
{
name: 'kickstarter',
title: 'Kickstarter',
icon: embedContentIcon,
description: __( 'Embed Kickstarter content.' ),
patterns: [
/^https?:\/\/(www\.)?kickstarter\.com\/.+/i,
/^https?:\/\/kck\.st\/.+/i,
],
attributes: { providerNameSlug: 'kickstarter', responsive: true },
},
{
name: 'mixcloud',
title: 'Mixcloud',
icon: embedAudioIcon,
keywords: [ __( 'music' ), __( 'audio' ) ],
description: __( 'Embed Mixcloud content.' ),
patterns: [ /^https?:\/\/(www\.)?mixcloud\.com\/.+/i ],
attributes: { providerNameSlug: 'mixcloud', responsive: true },
},
{
name: 'reddit',
title: 'Reddit',
icon: embedRedditIcon,
description: __( 'Embed a Reddit thread.' ),
patterns: [ /^https?:\/\/(www\.)?reddit\.com\/.+/i ],
attributes: { providerNameSlug: 'reddit', responsive: true },
},
{
name: 'reverbnation',
title: 'ReverbNation',
icon: embedAudioIcon,
description: __( 'Embed ReverbNation content.' ),
patterns: [ /^https?:\/\/(www\.)?reverbnation\.com\/.+/i ],
attributes: { providerNameSlug: 'reverbnation', responsive: true },
},
{
name: 'screencast',
title: 'Screencast',
icon: embedVideoIcon,
description: __( 'Embed Screencast content.' ),
patterns: [ /^https?:\/\/(www\.)?screencast\.com\/.+/i ],
attributes: { providerNameSlug: 'screencast', responsive: true },
},
{
name: 'scribd',
title: 'Scribd',
icon: embedContentIcon,
description: __( 'Embed Scribd content.' ),
patterns: [ /^https?:\/\/(www\.)?scribd\.com\/.+/i ],
attributes: { providerNameSlug: 'scribd', responsive: true },
},
{
name: 'slideshare',
title: 'Slideshare',
icon: embedContentIcon,
description: __( 'Embed Slideshare content.' ),
patterns: [ /^https?:\/\/(.+?\.)?slideshare\.net\/.+/i ],
attributes: { providerNameSlug: 'slideshare', responsive: true },
},
{
name: 'smugmug',
title: 'SmugMug',
icon: embedPhotoIcon,
description: __( 'Embed SmugMug content.' ),
patterns: [ /^https?:\/\/(.+\.)?smugmug\.com\/.*/i ],
attributes: {
providerNameSlug: 'smugmug',
previewable: false,
responsive: true,
},
},
{
name: 'speaker-deck',
title: 'Speaker Deck',
icon: embedContentIcon,
description: __( 'Embed Speaker Deck content.' ),
patterns: [ /^https?:\/\/(www\.)?speakerdeck\.com\/.+/i ],
attributes: { providerNameSlug: 'speaker-deck', responsive: true },
},
{
name: 'tiktok',
title: 'TikTok',
icon: embedVideoIcon,
keywords: [ __( 'video' ) ],
description: __( 'Embed a TikTok video.' ),
patterns: [ /^https?:\/\/(www\.)?tiktok\.com\/.+/i ],
attributes: { providerNameSlug: 'tiktok', responsive: true },
},
{
name: 'ted',
title: 'TED',
icon: embedVideoIcon,
description: __( 'Embed a TED video.' ),
patterns: [ /^https?:\/\/(www\.|embed\.)?ted\.com\/.+/i ],
attributes: { providerNameSlug: 'ted', responsive: true },
},
{
name: 'tumblr',
title: 'Tumblr',
icon: embedTumblrIcon,
keywords: [ __( 'social' ) ],
description: __( 'Embed a Tumblr post.' ),
patterns: [ /^https?:\/\/(www\.)?tumblr\.com\/.+/i ],
attributes: { providerNameSlug: 'tumblr', responsive: true },
},
{
name: 'videopress',
title: 'VideoPress',
icon: embedVideoIcon,
keywords: [ __( 'video' ) ],
description: __( 'Embed a VideoPress video.' ),
patterns: [ /^https?:\/\/videopress\.com\/.+/i ],
attributes: { providerNameSlug: 'videopress', responsive: true },
},
{
name: 'wordpress-tv',
title: 'WordPress.tv',
icon: embedVideoIcon,
description: __( 'Embed a WordPress.tv video.' ),
patterns: [ /^https?:\/\/wordpress\.tv\/.+/i ],
attributes: { providerNameSlug: 'wordpress-tv', responsive: true },
},
{
name: 'amazon-kindle',
title: 'Amazon Kindle',
icon: embedAmazonIcon,
keywords: [ __( 'ebook' ) ],
description: __( 'Embed Amazon Kindle content.' ),
patterns: [
/^https?:\/\/([a-z0-9-]+\.)?(amazon|amzn)(\.[a-z]{2,4})+\/.+/i,
/^https?:\/\/(www\.)?(a\.co|z\.cn)\/.+/i,
],
attributes: { providerNameSlug: 'amazon-kindle' },
},
{
name: 'pinterest',
title: 'Pinterest',
icon: embedPinterestIcon,
keywords: [ __( 'social' ), __( 'bookmark' ) ],
description: __( 'Embed Pinterest pins, boards, and profiles.' ),
patterns: [
/^https?:\/\/([a-z]{2}|www)\.pinterest\.com(\.(au|mx))?\/.*/i,
],
attributes: { providerNameSlug: 'pinterest' },
},
{
name: 'wolfram-cloud',
title: 'Wolfram',
icon: embedWolframIcon,
description: __( 'Embed Wolfram notebook content.' ),
patterns: [ /^https?:\/\/(www\.)?wolframcloud\.com\/obj\/.+/i ],
attributes: { providerNameSlug: 'wolfram-cloud', responsive: true },
},
];
/**
* Add `isActive` function to all `embed` variations, if not defined.
* `isActive` function is used to find a variation match from a created
* Block by providing its attributes.
*/
variations.forEach( ( variation ) => {
if ( variation.isActive ) return;
variation.isActive = ( blockAttributes, variationAttributes ) =>
blockAttributes.providerNameSlug ===
variationAttributes.providerNameSlug;
} );
export default variations;

View File

@@ -0,0 +1,75 @@
/**
* WordPress dependencies
*/
import { useMergeRefs, useFocusableIframe } from '@wordpress/compose';
import { useRef, useEffect, useMemo } from '@wordpress/element';
/** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */
const attributeMap = {
class: 'className',
frameborder: 'frameBorder',
marginheight: 'marginHeight',
marginwidth: 'marginWidth',
};
export default function WpEmbedPreview( { html } ) {
const ref = useRef();
const props = useMemo( () => {
const doc = new window.DOMParser().parseFromString( html, 'text/html' );
const iframe = doc.querySelector( 'iframe' );
const iframeProps = {};
if ( ! iframe ) return iframeProps;
Array.from( iframe.attributes ).forEach( ( { name, value } ) => {
if ( name === 'style' ) return;
iframeProps[ attributeMap[ name ] || name ] = value;
} );
return iframeProps;
}, [ html ] );
useEffect( () => {
const { ownerDocument } = ref.current;
const { defaultView } = ownerDocument;
/**
* Checks for WordPress embed events signaling the height change when
* iframe content loads or iframe's window is resized. The event is
* sent from WordPress core via the window.postMessage API.
*
* References:
* window.postMessage:
* https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
* WordPress core embed-template on load:
* https://github.com/WordPress/WordPress/blob/HEAD/wp-includes/js/wp-embed-template.js#L143
* WordPress core embed-template on resize:
* https://github.com/WordPress/WordPress/blob/HEAD/wp-includes/js/wp-embed-template.js#L187
*
* @param {MessageEvent} event Message event.
*/
function resizeWPembeds( { data: { secret, message, value } = {} } ) {
if ( message !== 'height' || secret !== props[ 'data-secret' ] ) {
return;
}
ref.current.height = value;
}
defaultView.addEventListener( 'message', resizeWPembeds );
return () => {
defaultView.removeEventListener( 'message', resizeWPembeds );
};
}, [] );
return (
<div className="wp-block-embed__wrapper">
<iframe
ref={ useMergeRefs( [ ref, useFocusableIframe() ] ) }
title={ props.title }
{ ...props }
/>
</div>
);
}

View File

@@ -0,0 +1,80 @@
/**
* WordPress dependencies
*/
import { memo, useMemo } from '@wordpress/element';
import { SandBox } from '@wordpress/components';
/**
* Checks for WordPress embed events signaling the height change when iframe
* content loads or iframe's window is resized. The event is sent from
* WordPress core via the window.postMessage API.
*
* References:
* window.postMessage: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
* WordPress core embed-template on load: https://github.com/WordPress/WordPress/blob/HEAD/wp-includes/js/wp-embed-template.js#L143
* WordPress core embed-template on resize: https://github.com/WordPress/WordPress/blob/HEAD/wp-includes/js/wp-embed-template.js#L187
*/
const observeAndResizeJS = `
( function() {
if ( ! document.body || ! window.parent ) {
return;
}
function sendResize( { data: { secret, message, value } = {} } ) {
if (
[ secret, message, value ].some(
( attribute ) => ! attribute
) ||
message !== 'height'
) {
return;
}
document
.querySelectorAll( 'iframe[data-secret="' + secret + '"' )
.forEach( ( iframe ) => {
if ( +iframe.height !== value ) {
iframe.height = value;
}
} );
// The function postMessage is exposed by the react-native-webview library
// to communicate between React Native and the WebView, in this case,
// we use it for notifying resize changes.
window.ReactNativeWebView.postMessage(JSON.stringify( {
action: 'resize',
height: value,
}));
}
window.addEventListener( 'message', sendResize );
} )();`;
function WpEmbedPreview( { html, ...rest } ) {
const wpEmbedHtml = useMemo( () => {
const doc = new window.DOMParser().parseFromString( html, 'text/html' );
const iframe = doc.querySelector( 'iframe' );
if ( iframe ) {
iframe.removeAttribute( 'style' );
}
const blockQuote = doc.querySelector( 'blockquote' );
if ( blockQuote ) {
blockQuote.innerHTML = '';
}
return doc.body.innerHTML;
}, [ html ] );
return (
<SandBox
customJS={ observeAndResizeJS }
html={ wpEmbedHtml }
{ ...rest }
/>
);
}
export default memo( WpEmbedPreview );