Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add paste styles to the block settings #45477

Merged
merged 9 commits into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/block-editor/src/components/block-actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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( {
Expand Down Expand Up @@ -60,6 +61,7 @@ export default function BlockActions( {
} = useDispatch( blockEditorStore );

const notifyCopy = useNotifyCopy();
const pasteStyles = usePasteStyles();

return children( {
canDuplicate,
Expand Down Expand Up @@ -128,5 +130,8 @@ export default function BlockActions( {
}
notifyCopy( 'copy', selectedBlockClientIds );
},
async onPasteStyles() {
await pasteStyles( blocks );
},
} );
}
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export function BlockSettingsDropdown( {
onInsertBefore,
onRemove,
onCopy,
onPasteStyles,
onMoveTo,
blocks,
} ) => (
Expand Down Expand Up @@ -262,6 +263,9 @@ export function BlockSettingsDropdown( {
blocks={ blocks }
onCopy={ onCopy }
/>
<MenuItem onClick={ onPasteStyles }>
{ __( 'Paste styles' ) }
</MenuItem>
{ canDuplicate && (
<MenuItem
onClick={ pipe(
Expand Down
230 changes: 230 additions & 0 deletions packages/block-editor/src/components/use-paste-styles/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/**
* WordPress dependencies
*/
import { useCallback } from '@wordpress/element';
import { getBlockType, parse } from '@wordpress/blocks';
import { useDispatch, useRegistry } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { __, sprintf } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
import {
hasAlignSupport,
hasBorderSupport,
hasBackgroundColorSupport,
hasTextColorSupport,
hasGradientSupport,
hasCustomClassNameSupport,
hasFontFamilySupport,
hasFontSizeSupport,
hasLayoutSupport,
hasStyleSupport,
} from '../../hooks/supports';

/**
* Determine if the copied text looks like serialized blocks or not.
* Since plain text will always get parsed into a freeform block,
* we check that if the parsed blocks is anything other than that.
*
* @param {string} text The copied text.
* @return {boolean} True if the text looks like serialized blocks, false otherwise.
*/
function hasSerializedBlocks( text ) {
try {
const blocks = parse( text, {
__unstableSkipMigrationLogs: true,
__unstableSkipAutop: true,
} );
if ( blocks.length === 1 && blocks[ 0 ].name === 'core/freeform' ) {
// It's likely that the text is just plain text and not serialized blocks.
return false;
}
return true;
} catch ( err ) {
// Parsing error, the text is not serialized blocks.
// (Even though that it technically won't happen)
return false;
}
}

/**
* Style attributes are attributes being added in `block-editor/src/hooks/*`.
* (Except for some unrelated to style like `anchor` or `settings`.)
* They generally represent the default block supports.
*/
const STYLE_ATTRIBUTES = {
align: hasAlignSupport,
borderColor: ( nameOrType ) => 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.
kevin940726 marked this conversation as resolved.
Show resolved Hide resolved
// (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 ) {
Copy link
Contributor

@talldan talldan Jan 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like there's one situation which is unhandled before this point is reached, and that's copy/pasting block content that has no style attributes. No error or warning is shown.

In that situation it might be good to show a message similar to the one above ("Unable to paste styles. Block styles couldn't be found within the copied content.")

It's very minor, so could be a small follow-up PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll override the style to the default values if there are any, so I wonder if that would be a common issue 🤔 . Agreed that it can be a follow-up. 👍

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,
]
);
}
Loading