From 3a3350047a066c24d0b0aaeaca21c52032b3cdf7 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Mon, 9 Jan 2023 16:13:41 +0800 Subject: [PATCH] Add paste styles to the block settings (#45477) * Add paste styles to the block settings * Allow overriding styles for undefined attributes * Fix blocks detection * Add comments and guard for http * Try handcrafted list for style attributes * Rename looksLikeBlocks * Add check for supports * Fix pasting styles bug * Reword the notices --- .../src/components/block-actions/index.js | 5 + .../block-settings-dropdown.js | 4 + .../src/components/use-paste-styles/index.js | 230 +++++++++++++ packages/block-editor/src/hooks/supports.js | 302 ++++++++++++++++++ 4 files changed, 541 insertions(+) create mode 100644 packages/block-editor/src/components/use-paste-styles/index.js create mode 100644 packages/block-editor/src/hooks/supports.js diff --git a/packages/block-editor/src/components/block-actions/index.js b/packages/block-editor/src/components/block-actions/index.js index f94f16e9256283..a7fe38245e5ac5 100644 --- a/packages/block-editor/src/components/block-actions/index.js +++ b/packages/block-editor/src/components/block-actions/index.js @@ -12,6 +12,7 @@ import { * Internal dependencies */ import { useNotifyCopy } from '../copy-handler'; +import usePasteStyles from '../use-paste-styles'; import { store as blockEditorStore } from '../../store'; export default function BlockActions( { @@ -60,6 +61,7 @@ export default function BlockActions( { } = useDispatch( blockEditorStore ); const notifyCopy = useNotifyCopy(); + const pasteStyles = usePasteStyles(); return children( { canDuplicate, @@ -128,5 +130,8 @@ export default function BlockActions( { } notifyCopy( 'copy', selectedBlockClientIds ); }, + async onPasteStyles() { + await pasteStyles( blocks ); + }, } ); } diff --git a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js index 9ebc961440f7a5..def097313e48fe 100644 --- a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js +++ b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js @@ -209,6 +209,7 @@ export function BlockSettingsDropdown( { onInsertBefore, onRemove, onCopy, + onPasteStyles, onMoveTo, blocks, } ) => ( @@ -262,6 +263,9 @@ export function BlockSettingsDropdown( { blocks={ blocks } onCopy={ onCopy } /> + + { __( 'Paste styles' ) } + { canDuplicate && ( hasBorderSupport( nameOrType, 'color' ), + backgroundColor: hasBackgroundColorSupport, + textColor: hasTextColorSupport, + gradient: hasGradientSupport, + className: hasCustomClassNameSupport, + fontFamily: hasFontFamilySupport, + fontSize: hasFontSizeSupport, + layout: hasLayoutSupport, + style: hasStyleSupport, +}; + +/** + * Get the "style attributes" from a given block to a target block. + * + * @param {WPBlock} sourceBlock The source block. + * @param {WPBlock} targetBlock The target block. + * @return {Object} the filtered attributes object. + */ +function getStyleAttributes( sourceBlock, targetBlock ) { + return Object.entries( STYLE_ATTRIBUTES ).reduce( + ( attributes, [ attributeKey, hasSupport ] ) => { + // Only apply the attribute if both blocks support it. + if ( + hasSupport( sourceBlock.name ) && + hasSupport( targetBlock.name ) + ) { + // Override attributes that are not present in the block to their defaults. + attributes[ attributeKey ] = + sourceBlock.attributes[ attributeKey ]; + } + return attributes; + }, + {} + ); +} + +/** + * Update the target blocks with style attributes recursively. + * + * @param {WPBlock[]} targetBlocks The target blocks to be updated. + * @param {WPBlock[]} sourceBlocks The source blocks to get th style attributes from. + * @param {Function} updateBlockAttributes The function to update the attributes. + */ +function recursivelyUpdateBlockAttributes( + targetBlocks, + sourceBlocks, + updateBlockAttributes +) { + for ( + let index = 0; + index < Math.min( sourceBlocks.length, targetBlocks.length ); + index += 1 + ) { + updateBlockAttributes( + targetBlocks[ index ].clientId, + getStyleAttributes( sourceBlocks[ index ], targetBlocks[ index ] ) + ); + + recursivelyUpdateBlockAttributes( + targetBlocks[ index ].innerBlocks, + sourceBlocks[ index ].innerBlocks, + updateBlockAttributes + ); + } +} + +/** + * A hook to return a pasteStyles event function for handling pasting styles to blocks. + * + * @return {Function} A function to update the styles to the blocks. + */ +export default function usePasteStyles() { + const registry = useRegistry(); + const { updateBlockAttributes } = useDispatch( blockEditorStore ); + const { createSuccessNotice, createWarningNotice, createErrorNotice } = + useDispatch( noticesStore ); + + return useCallback( + async ( targetBlocks ) => { + let html = ''; + try { + // `http:` sites won't have the clipboard property on navigator. + // (with the exception of localhost.) + if ( ! window.navigator.clipboard ) { + createErrorNotice( + __( + 'Unable to paste styles. This feature is only available on secure (https) sites in supporting browsers.' + ), + { type: 'snackbar' } + ); + return; + } + + html = await window.navigator.clipboard.readText(); + } catch ( error ) { + // Possibly the permission is denied. + createErrorNotice( + __( + 'Unable to paste styles. Please allow browser clipboard permissions before continuing.' + ), + { + type: 'snackbar', + } + ); + return; + } + + // Abort if the copied text is empty or doesn't look like serialized blocks. + if ( ! html || ! hasSerializedBlocks( html ) ) { + createWarningNotice( + __( + "Unable to paste styles. Block styles couldn't be found within the copied content." + ), + { + type: 'snackbar', + } + ); + return; + } + + const copiedBlocks = parse( html ); + + if ( copiedBlocks.length === 1 ) { + // Apply styles of the block to all the target blocks. + registry.batch( () => { + recursivelyUpdateBlockAttributes( + targetBlocks, + targetBlocks.map( () => copiedBlocks[ 0 ] ), + updateBlockAttributes + ); + } ); + } else { + registry.batch( () => { + recursivelyUpdateBlockAttributes( + targetBlocks, + copiedBlocks, + updateBlockAttributes + ); + } ); + } + + if ( targetBlocks.length === 1 ) { + const title = getBlockType( targetBlocks[ 0 ].name )?.title; + createSuccessNotice( + sprintf( + // Translators: Name of the block being pasted, e.g. "Paragraph". + __( 'Pasted styles to %s.' ), + title + ), + { type: 'snackbar' } + ); + } else { + createSuccessNotice( + sprintf( + // Translators: The number of the blocks. + __( 'Pasted styles to %d blocks.' ), + targetBlocks.length + ), + { type: 'snackbar' } + ); + } + }, + [ + registry.batch, + updateBlockAttributes, + createSuccessNotice, + createWarningNotice, + createErrorNotice, + ] + ); +} diff --git a/packages/block-editor/src/hooks/supports.js b/packages/block-editor/src/hooks/supports.js new file mode 100644 index 00000000000000..a71e2312937b09 --- /dev/null +++ b/packages/block-editor/src/hooks/supports.js @@ -0,0 +1,302 @@ +/** + * WordPress dependencies + */ +import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; +import { Platform } from '@wordpress/element'; + +const ALIGN_SUPPORT_KEY = 'align'; +const ALIGN_WIDE_SUPPORT_KEY = 'alignWide'; +const BORDER_SUPPORT_KEY = '__experimentalBorder'; +const COLOR_SUPPORT_KEY = 'color'; +const CUSTOM_CLASS_NAME_SUPPORT_KEY = 'customClassName'; +const FONT_FAMILY_SUPPORT_KEY = 'typography.__experimentalFontFamily'; +const FONT_SIZE_SUPPORT_KEY = 'typography.fontSize'; +const LINE_HEIGHT_SUPPORT_KEY = 'typography.lineHeight'; +/** + * Key within block settings' support array indicating support for font style. + */ +const FONT_STYLE_SUPPORT_KEY = 'typography.__experimentalFontStyle'; +/** + * Key within block settings' support array indicating support for font weight. + */ +const FONT_WEIGHT_SUPPORT_KEY = 'typography.__experimentalFontWeight'; +/** + * Key within block settings' supports array indicating support for text + * decorations e.g. settings found in `block.json`. + */ +const TEXT_DECORATION_SUPPORT_KEY = 'typography.__experimentalTextDecoration'; +/** + * Key within block settings' supports array indicating support for text + * transforms e.g. settings found in `block.json`. + */ +const TEXT_TRANSFORM_SUPPORT_KEY = 'typography.__experimentalTextTransform'; +/** + * Key within block settings' supports array indicating support for letter-spacing + * e.g. settings found in `block.json`. + */ +const LETTER_SPACING_SUPPORT_KEY = 'typography.__experimentalLetterSpacing'; +const LAYOUT_SUPPORT_KEY = '__experimentalLayout'; +const TYPOGRAPHY_SUPPORT_KEYS = [ + LINE_HEIGHT_SUPPORT_KEY, + FONT_SIZE_SUPPORT_KEY, + FONT_STYLE_SUPPORT_KEY, + FONT_WEIGHT_SUPPORT_KEY, + FONT_FAMILY_SUPPORT_KEY, + TEXT_DECORATION_SUPPORT_KEY, + TEXT_TRANSFORM_SUPPORT_KEY, + LETTER_SPACING_SUPPORT_KEY, +]; +const SPACING_SUPPORT_KEY = 'spacing'; +const styleSupportKeys = [ + ...TYPOGRAPHY_SUPPORT_KEYS, + BORDER_SUPPORT_KEY, + COLOR_SUPPORT_KEY, + SPACING_SUPPORT_KEY, +]; + +/** + * Returns true if the block defines support for align. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasAlignSupport = ( nameOrType ) => + hasBlockSupport( nameOrType, ALIGN_SUPPORT_KEY ); + +/** + * Returns the block support value for align, if defined. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {unknown} The block support value. + */ +export const getAlignSupport = ( nameOrType ) => + getBlockSupport( nameOrType, ALIGN_SUPPORT_KEY ); + +/** + * Returns true if the block defines support for align wide. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasAlignWideSupport = ( nameOrType ) => + hasBlockSupport( nameOrType, ALIGN_WIDE_SUPPORT_KEY ); + +/** + * Returns the block support value for align wide, if defined. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {unknown} The block support value. + */ +export const getAlignWideSupport = ( nameOrType ) => + getBlockSupport( nameOrType, ALIGN_WIDE_SUPPORT_KEY ); + +/** + * Determine whether there is block support for border properties. + * + * @param {string|Object} nameOrType Block name or type object. + * @param {string} feature Border feature to check support for. + * + * @return {boolean} Whether there is support. + */ +export function hasBorderSupport( nameOrType, feature = 'any' ) { + if ( Platform.OS !== 'web' ) { + return false; + } + + const support = getBlockSupport( nameOrType, BORDER_SUPPORT_KEY ); + + if ( support === true ) { + return true; + } + + if ( feature === 'any' ) { + return !! ( + support?.color || + support?.radius || + support?.width || + support?.style + ); + } + + return !! support?.[ feature ]; +} + +/** + * Get block support for border properties. + * + * @param {string|Object} nameOrType Block name or type object. + * @param {string} feature Border feature to get. + * + * @return {unknown} The block support. + */ +export const getBorderSupport = ( nameOrType, feature ) => + getBlockSupport( nameOrType, [ BORDER_SUPPORT_KEY, feature ] ); + +/** + * Returns true if the block defines support for color. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasColorSupport = ( nameOrType ) => { + const colorSupport = getBlockSupport( nameOrType, COLOR_SUPPORT_KEY ); + return ( + colorSupport && + ( colorSupport.link === true || + colorSupport.gradient === true || + colorSupport.background !== false || + colorSupport.text !== false ) + ); +}; + +/** + * Returns true if the block defines support for link color. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasLinkColorSupport = ( nameOrType ) => { + if ( Platform.OS !== 'web' ) { + return false; + } + + const colorSupport = getBlockSupport( nameOrType, COLOR_SUPPORT_KEY ); + + return ( + colorSupport !== null && + typeof colorSupport === 'object' && + !! colorSupport.link + ); +}; + +/** + * Returns true if the block defines support for gradient color. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasGradientSupport = ( nameOrType ) => { + const colorSupport = getBlockSupport( nameOrType, COLOR_SUPPORT_KEY ); + + return ( + colorSupport !== null && + typeof colorSupport === 'object' && + !! colorSupport.gradients + ); +}; + +/** + * Returns true if the block defines support for background color. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasBackgroundColorSupport = ( nameOrType ) => { + const colorSupport = getBlockSupport( nameOrType, COLOR_SUPPORT_KEY ); + + return colorSupport && colorSupport.background !== false; +}; + +/** + * Returns true if the block defines support for background color. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasTextColorSupport = ( nameOrType ) => { + const colorSupport = getBlockSupport( nameOrType, COLOR_SUPPORT_KEY ); + + return colorSupport && colorSupport.text !== false; +}; + +/** + * Get block support for color properties. + * + * @param {string|Object} nameOrType Block name or type object. + * @param {string} feature Color feature to get. + * + * @return {unknown} The block support. + */ +export const getColorSupport = ( nameOrType, feature ) => + getBlockSupport( nameOrType, [ COLOR_SUPPORT_KEY, feature ] ); + +/** + * Returns true if the block defines support for custom class name. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasCustomClassNameSupport = ( nameOrType ) => + hasBlockSupport( nameOrType, CUSTOM_CLASS_NAME_SUPPORT_KEY, true ); + +/** + * Returns the block support value for custom class name, if defined. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {unknown} The block support value. + */ +export const getCustomClassNameSupport = ( nameOrType ) => + getBlockSupport( nameOrType, CUSTOM_CLASS_NAME_SUPPORT_KEY, true ); + +/** + * Returns true if the block defines support for font family. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasFontFamilySupport = ( nameOrType ) => + hasBlockSupport( nameOrType, FONT_FAMILY_SUPPORT_KEY ); + +/** + * Returns the block support value for font family, if defined. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {unknown} The block support value. + */ +export const getFontFamilySupport = ( nameOrType ) => + getBlockSupport( nameOrType, FONT_FAMILY_SUPPORT_KEY ); + +/** + * Returns true if the block defines support for font size. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasFontSizeSupport = ( nameOrType ) => + hasBlockSupport( nameOrType, FONT_SIZE_SUPPORT_KEY ); + +/** + * Returns the block support value for font size, if defined. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {unknown} The block support value. + */ +export const getFontSizeSupport = ( nameOrType ) => + getBlockSupport( nameOrType, FONT_SIZE_SUPPORT_KEY ); + +/** + * Returns true if the block defines support for layout. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasLayoutSupport = ( nameOrType ) => + hasBlockSupport( nameOrType, LAYOUT_SUPPORT_KEY ); + +/** + * Returns the block support value for layout, if defined. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {unknown} The block support value. + */ +export const getLayoutSupport = ( nameOrType ) => + getBlockSupport( nameOrType, LAYOUT_SUPPORT_KEY ); + +/** + * Returns true if the block defines support for style. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasStyleSupport = ( nameOrType ) => + styleSupportKeys.some( ( key ) => hasBlockSupport( nameOrType, key ) );