Skip to content

Commit

Permalink
Fix moving inner blocks in the Widgets Customizer (#33243)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin940726 authored Jul 8, 2021
1 parent 3b31957 commit 6160b5d
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 93 deletions.
Original file line number Diff line number Diff line change
@@ -1,100 +1,19 @@
/**
* External dependencies
*/
import { omit, isEqual } from 'lodash';
import { isEqual } from 'lodash';

/**
* WordPress dependencies
*/
import { serialize, parse, createBlock } from '@wordpress/blocks';
import { useState, useEffect, useCallback } from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { getWidgetIdFromBlock, addWidgetIdToBlock } from '@wordpress/widgets';

function blockToWidget( block, existingWidget = null ) {
let widget;

const isValidLegacyWidgetBlock =
block.name === 'core/legacy-widget' &&
( block.attributes.id || block.attributes.instance );

if ( isValidLegacyWidgetBlock ) {
if ( block.attributes.id ) {
// Widget that does not extend WP_Widget.
widget = {
id: block.attributes.id,
};
} else {
const { encoded, hash, raw, ...rest } = block.attributes.instance;

// Widget that extends WP_Widget.
widget = {
idBase: block.attributes.idBase,
instance: {
...existingWidget?.instance,
// Required only for the customizer.
is_widget_customizer_js_value: true,
encoded_serialized_instance: encoded,
instance_hash_key: hash,
raw_instance: raw,
...rest,
},
};
}
} else {
const instance = {
content: serialize( block ),
};
widget = {
idBase: 'block',
widgetClass: 'WP_Widget_Block',
instance: {
raw_instance: instance,
},
};
}

return {
...omit( existingWidget, [ 'form', 'rendered' ] ),
...widget,
};
}

function widgetToBlock( { id, idBase, number, instance } ) {
let block;

const {
encoded_serialized_instance: encoded,
instance_hash_key: hash,
raw_instance: raw,
...rest
} = instance;

if ( idBase === 'block' ) {
const parsedBlocks = parse( raw.content );
block = parsedBlocks.length
? parsedBlocks[ 0 ]
: createBlock( 'core/paragraph', {} );
} else if ( number ) {
// Widget that extends WP_Widget.
block = createBlock( 'core/legacy-widget', {
idBase,
instance: {
encoded,
hash,
raw,
...rest,
},
} );
} else {
// Widget that does not extend WP_Widget.
block = createBlock( 'core/legacy-widget', {
id,
} );
}

return addWidgetIdToBlock( block, id );
}
/**
* Internal dependencies
*/
import { blockToWidget, widgetToBlock } from '../../utils';

function widgetsToBlocks( widgets ) {
return widgets.map( ( widget ) => widgetToBlock( widget ) );
Expand Down
44 changes: 37 additions & 7 deletions packages/customize-widgets/src/filters/move-to-sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
store as blockEditorStore,
} from '@wordpress/block-editor';
import { createHigherOrderComponent } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
import { useSelect, useDispatch } from '@wordpress/data';
import { addFilter } from '@wordpress/hooks';
import { MoveToWidgetArea, getWidgetIdFromBlock } from '@wordpress/widgets';

Expand All @@ -22,14 +22,17 @@ import {
useSidebarControls,
useActiveSidebarControl,
} from '../components/sidebar-controls';
import { useFocusControl } from '../components/focus-control';
import { blockToWidget } from '../utils';

const withMoveToSidebarToolbarItem = createHigherOrderComponent(
( BlockEdit ) => ( props ) => {
const widgetId = getWidgetIdFromBlock( props );
let widgetId = getWidgetIdFromBlock( props );
const sidebarControls = useSidebarControls();
const activeSidebarControl = useActiveSidebarControl();
const hasMultipleSidebars = sidebarControls?.length > 1;
const blockName = props.name;
const clientId = props.clientId;
const canInsertBlockInSidebar = useSelect(
( select ) => {
// Use an empty string to represent the root block list, which
Expand All @@ -41,19 +44,46 @@ const withMoveToSidebarToolbarItem = createHigherOrderComponent(
},
[ blockName ]
);
const block = useSelect(
( select ) => select( blockEditorStore ).getBlock( clientId ),
[ clientId ]
);
const { removeBlock } = useDispatch( blockEditorStore );
const [ , focusWidget ] = useFocusControl();

function moveToSidebar( sidebarControlId ) {
const newSidebarControl = sidebarControls.find(
( sidebarControl ) => sidebarControl.id === sidebarControlId
);

const oldSetting = activeSidebarControl.setting;
const newSetting = newSidebarControl.setting;
if ( widgetId ) {
/**
* If there's a widgetId, move it to the other sidebar.
*/
const oldSetting = activeSidebarControl.setting;
const newSetting = newSidebarControl.setting;

oldSetting( without( oldSetting(), widgetId ) );
newSetting( [ ...newSetting(), widgetId ] );
} else {
/**
* If there isn't a widgetId, it's most likely a inner block.
* First, remove the block in the original sidebar,
* then, create a new widget in the new sidebar and get back its widgetId.
*/
const sidebarAdapter = newSidebarControl.sidebarAdapter;

oldSetting( without( oldSetting(), widgetId ) );
newSetting( [ ...newSetting(), widgetId ] );
removeBlock( clientId );
const addedWidgetIds = sidebarAdapter.setWidgets( [
...sidebarAdapter.getWidgets(),
blockToWidget( block ),
] );
// The last non-null id is the added widget's id.
widgetId = addedWidgetIds.reverse().find( ( id ) => !! id );
}

newSidebarControl.expand();
// Move focus to the moved widget and expand the sidebar.
focusWidget( widgetId );
}

return (
Expand Down
112 changes: 112 additions & 0 deletions packages/customize-widgets/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
// @ts-check
/**
* WordPress dependencies
*/
import { serialize, parse, createBlock } from '@wordpress/blocks';
import { addWidgetIdToBlock } from '@wordpress/widgets';

/**
* External dependencies
*/
import { omit } from 'lodash';

/**
* Convert settingId to widgetId.
Expand All @@ -18,3 +28,105 @@ export function settingIdToWidgetId( settingId ) {

return settingId;
}

/**
* Transform a block to a customizable widget.
*
* @param {WPBlock} block The block to be transformed from.
* @param {Object} existingWidget The widget to be extended from.
* @return {Object} The transformed widget.
*/
export function blockToWidget( block, existingWidget = null ) {
let widget;

const isValidLegacyWidgetBlock =
block.name === 'core/legacy-widget' &&
( block.attributes.id || block.attributes.instance );

if ( isValidLegacyWidgetBlock ) {
if ( block.attributes.id ) {
// Widget that does not extend WP_Widget.
widget = {
id: block.attributes.id,
};
} else {
const { encoded, hash, raw, ...rest } = block.attributes.instance;

// Widget that extends WP_Widget.
widget = {
idBase: block.attributes.idBase,
instance: {
...existingWidget?.instance,
// Required only for the customizer.
is_widget_customizer_js_value: true,
encoded_serialized_instance: encoded,
instance_hash_key: hash,
raw_instance: raw,
...rest,
},
};
}
} else {
const instance = {
content: serialize( block ),
};
widget = {
idBase: 'block',
widgetClass: 'WP_Widget_Block',
instance: {
raw_instance: instance,
},
};
}

return {
...omit( existingWidget, [ 'form', 'rendered' ] ),
...widget,
};
}

/**
* Transform a widget to a block.
*
* @param {Object} widget The widget to be transformed from.
* @param {string} widget.id The widget id.
* @param {string} widget.idBase The id base of the widget.
* @param {number} widget.number The number/index of the widget.
* @param {Object} widget.instance The instance of the widget.
* @return {WPBlock} The transformed block.
*/
export function widgetToBlock( { id, idBase, number, instance } ) {
let block;

const {
encoded_serialized_instance: encoded,
instance_hash_key: hash,
raw_instance: raw,
...rest
} = instance;

if ( idBase === 'block' ) {
const parsedBlocks = parse( raw.content );
block = parsedBlocks.length
? parsedBlocks[ 0 ]
: createBlock( 'core/paragraph', {} );
} else if ( number ) {
// Widget that extends WP_Widget.
block = createBlock( 'core/legacy-widget', {
idBase,
instance: {
encoded,
hash,
raw,
...rest,
},
} );
} else {
// Widget that does not extend WP_Widget.
block = createBlock( 'core/legacy-widget', {
id,
} );
}

return addWidgetIdToBlock( block, id );
}
63 changes: 63 additions & 0 deletions packages/e2e-tests/specs/widgets/customizing-widgets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,69 @@ describe( 'Widgets Customizer', () => {
"The page delivered both an 'X-Frame-Options' header and a 'Content-Security-Policy' header with a 'frame-ancestors' directive. Although the 'X-Frame-Options' header alone would have blocked embedding, it has been ignored."
);
} );

it( 'should move (inner) blocks to another sidebar', async () => {
const widgetsPanel = await find( {
role: 'heading',
name: /Widgets/,
level: 3,
} );
await widgetsPanel.click();

const footer1Section = await find( {
role: 'heading',
name: /Footer #1/,
level: 3,
} );
await footer1Section.click();

await addBlock( 'Paragraph' );
await page.keyboard.type( 'First Paragraph' );

await showBlockToolbar();
await clickBlockToolbarButton( 'Options' );
const groupButton = await find( {
role: 'menuitem',
name: 'Group',
} );
await groupButton.click();

// Refocus the paragraph block.
const paragraphBlock = await find( {
role: 'group',
name: 'Paragraph block',
value: 'First Paragraph',
} );
await paragraphBlock.focus();
await showBlockToolbar();
await clickBlockToolbarButton( 'Move to widget area' );

const footer2Option = await find( {
role: 'menuitemradio',
name: 'Footer #2',
} );
await footer2Option.click();

// Should switch to and expand Footer #2.
await expect( {
role: 'heading',
name: 'Customizing ▸ Widgets Footer #2',
} ).toBeFound();

// The paragraph block should be moved to the new sidebar and have focus.
const movedParagraphBlockQuery = {
role: 'group',
name: 'Paragraph block',
value: 'First Paragraph',
};
await expect( movedParagraphBlockQuery ).toBeFound();
const movedParagraphBlock = await find( movedParagraphBlockQuery );
await expect( movedParagraphBlock ).toHaveFocus();

expect( console ).toHaveWarned(
"The page delivered both an 'X-Frame-Options' header and a 'Content-Security-Policy' header with a 'frame-ancestors' directive. Although the 'X-Frame-Options' header alone would have blocked embedding, it has been ignored."
);
} );
} );

/**
Expand Down

0 comments on commit 6160b5d

Please sign in to comment.