@wordpress/block-library
Version:
Block library for the WordPress editor.
325 lines (293 loc) • 10.6 kB
JavaScript
/**
* 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 ) => {
// If full image url not found use thumbnail.
const imageUrl = photo.url || photo.thumbnail_url;
// 100% width for the preview so it fits nicely into the document, some "thumbnails" are
// actually the full size photo.
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;
}
);
/**
* Returns the attributes derived from the preview, merged with the current attributes.
*
* @param {Object} currentAttributes The current attributes of the block.
* @param {Object} preview The preview data.
* @param {string} title The block's title, e.g. Twitter.
* @param {boolean} isResponsive Boolean indicating if the block supports responsive content.
* @param {boolean} ignorePreviousClassName Determines if the previous className attribute should be ignored when merging.
* @return {Object} Merged attributes.
*/
export const getMergedAttributesWithPreview = (
currentAttributes,
preview,
title,
isResponsive,
ignorePreviousClassName = false
) => {
const { allowResponsive, className } = currentAttributes;
return {
...currentAttributes,
...getAttributesFromPreview(
preview,
title,
ignorePreviousClassName ? undefined : className,
isResponsive,
allowResponsive
),
};
};