Skip to content

Commit

Permalink
Add paste styles to the block settings (#45477)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kevin940726 authored Jan 9, 2023
1 parent e28e4f2 commit 3a33500
Show file tree
Hide file tree
Showing 4 changed files with 541 additions and 0 deletions.
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.
// (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,
]
);
}
Loading

1 comment on commit 3a33500

@github-actions
Copy link

Choose a reason for hiding this comment

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

Flaky tests detected in 3a33500.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/3871912243
📝 Reported issues:

Please sign in to comment.