From e73a0aed023c191db754552e1287e30ea214b80b Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Tue, 9 May 2023 14:44:14 -0500 Subject: [PATCH] Open LinkUI popover after appending link from Navigation side block inserter The behavior when adding a link from the appender button on the OffCanvasEditor is to open a link control popover. This commit copies over several files from the OffCanvasEditor that was required for that to work in the ListView. The behavior is buggy, IMO, but it does match what is in trunk. --- .../components/list-view/block-contents.js | 71 +++++++- .../src/components/list-view/link-ui.js | 167 ++++++++++++++++++ .../components/list-view/update-attributes.js | 99 +++++++++++ .../list-view/use-inserted-block.js | 47 +++++ 4 files changed, 379 insertions(+), 5 deletions(-) create mode 100644 packages/block-editor/src/components/list-view/link-ui.js create mode 100644 packages/block-editor/src/components/list-view/update-attributes.js create mode 100644 packages/block-editor/src/components/list-view/use-inserted-block.js diff --git a/packages/block-editor/src/components/list-view/block-contents.js b/packages/block-editor/src/components/list-view/block-contents.js index a1f5f3562cfd40..796240b0a143cc 100644 --- a/packages/block-editor/src/components/list-view/block-contents.js +++ b/packages/block-editor/src/components/list-view/block-contents.js @@ -7,16 +7,25 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useSelect } from '@wordpress/data'; -import { forwardRef } from '@wordpress/element'; +import { forwardRef, useEffect, useState } from '@wordpress/element'; /** * Internal dependencies */ +import { unlock } from '../../lock-unlock'; import ListViewBlockSelectButton from './block-select-button'; import BlockDraggable from '../block-draggable'; import { store as blockEditorStore } from '../../store'; +import { updateAttributes } from './update-attributes'; +import { LinkUI } from './link-ui'; +import { useInsertedBlock } from './use-inserted-block'; import { useListViewContext } from './context'; +const BLOCKS_WITH_LINK_UI_SUPPORT = [ + 'core/navigation-link', + 'core/navigation-submenu', +]; + const ListViewBlockContents = forwardRef( ( { @@ -34,19 +43,54 @@ const ListViewBlockContents = forwardRef( ref ) => { const { clientId } = block; - - const { blockMovingClientId, selectedBlockInBlockEditor } = useSelect( + const [ isLinkUIOpen, setIsLinkUIOpen ] = useState(); + const { + blockMovingClientId, + selectedBlockInBlockEditor, + lastInsertedBlockClientId, + } = useSelect( ( select ) => { - const { hasBlockMovingClientId, getSelectedBlockClientId } = - select( blockEditorStore ); + const { + hasBlockMovingClientId, + getSelectedBlockClientId, + getLastInsertedBlocksClientIds, + } = unlock( select( blockEditorStore ) ); + const lastInsertedBlocksClientIds = + getLastInsertedBlocksClientIds(); return { blockMovingClientId: hasBlockMovingClientId(), selectedBlockInBlockEditor: getSelectedBlockClientId(), + lastInsertedBlockClientId: + lastInsertedBlocksClientIds && + lastInsertedBlocksClientIds[ 0 ], }; }, [ clientId ] ); + const { + insertedBlockAttributes, + insertedBlockName, + setInsertedBlockAttributes, + } = useInsertedBlock( lastInsertedBlockClientId ); + + const hasExistingLinkValue = insertedBlockAttributes?.url; + + useEffect( () => { + if ( + clientId === lastInsertedBlockClientId && + BLOCKS_WITH_LINK_UI_SUPPORT?.includes( insertedBlockName ) && + ! hasExistingLinkValue // don't re-show the Link UI if the block already has a link value. + ) { + setIsLinkUIOpen( true ); + } + }, [ + lastInsertedBlockClientId, + clientId, + insertedBlockName, + hasExistingLinkValue, + ] ); + const { renderAdditionalBlockUI } = useListViewContext(); const isBlockMoveTarget = @@ -67,6 +111,23 @@ const ListViewBlockContents = forwardRef( return ( <> { renderAdditionalBlockUI && renderAdditionalBlockUI( block ) } + { isLinkUIOpen && ( + setIsLinkUIOpen( false ) } + hasCreateSuggestion={ false } + onChange={ ( updatedValue ) => { + updateAttributes( + updatedValue, + setInsertedBlockAttributes, + insertedBlockAttributes + ); + setIsLinkUIOpen( false ); + } } + onCancel={ () => setIsLinkUIOpen( false ) } + /> + ) } { ( { draggable, onDragStart, onDragEnd } ) => ( { + const { + getBlock: _getBlock, + getBlockRootClientId, + getBlockTransformItems, + } = select( blockEditorStore ); + + return { + getBlock: _getBlock, + blockTransforms: getBlockTransformItems( + _getBlock( clientId ), + getBlockRootClientId( clientId ) + ), + }; + }, + [ clientId ] + ); + + const { replaceBlock } = useDispatch( blockEditorStore ); + + const featuredBlocks = [ + 'core/page-list', + 'core/site-logo', + 'core/social-links', + 'core/search', + ]; + + const transforms = blockTransforms.filter( ( item ) => { + return featuredBlocks.includes( item.name ); + } ); + + if ( ! transforms?.length ) { + return null; + } + + if ( ! clientId ) { + return null; + } + + return ( +
+

+ { __( 'Transform' ) } +

+
+ { transforms.map( ( item ) => { + return ( + + ); + } ) } +
+
+ ); +} + +export function LinkUI( props ) { + const { label, url, opensInNewTab, type, kind } = props.link; + const link = { + url, + opensInNewTab, + title: label && stripHTML( label ), + }; + + return ( + + ( + + ) + : null + } + /> + + ); +} diff --git a/packages/block-editor/src/components/list-view/update-attributes.js b/packages/block-editor/src/components/list-view/update-attributes.js new file mode 100644 index 00000000000000..5133cae3878338 --- /dev/null +++ b/packages/block-editor/src/components/list-view/update-attributes.js @@ -0,0 +1,99 @@ +/** + * WordPress dependencies + */ +import { escapeHTML } from '@wordpress/escape-html'; +import { safeDecodeURI } from '@wordpress/url'; + +/** + * @typedef {'post-type'|'custom'|'taxonomy'|'post-type-archive'} WPNavigationLinkKind + */ +/** + * Navigation Link Block Attributes + * + * @typedef {Object} WPNavigationLinkBlockAttributes + * + * @property {string} [label] Link text. + * @property {WPNavigationLinkKind} [kind] Kind is used to differentiate between term and post ids to check post draft status. + * @property {string} [type] The type such as post, page, tag, category and other custom types. + * @property {string} [rel] The relationship of the linked URL. + * @property {number} [id] A post or term id. + * @property {boolean} [opensInNewTab] Sets link target to _blank when true. + * @property {string} [url] Link href. + * @property {string} [title] Link title attribute. + */ +/** + * Link Control onChange handler that updates block attributes when a setting is changed. + * + * @param {Object} updatedValue New block attributes to update. + * @param {Function} setAttributes Block attribute update function. + * @param {WPNavigationLinkBlockAttributes} blockAttributes Current block attributes. + * + */ + +export const updateAttributes = ( + updatedValue = {}, + setAttributes, + blockAttributes = {} +) => { + const { + label: originalLabel = '', + kind: originalKind = '', + type: originalType = '', + } = blockAttributes; + + const { + title: newLabel = '', // the title of any provided Post. + url: newUrl = '', + opensInNewTab, + id, + kind: newKind = originalKind, + type: newType = originalType, + } = updatedValue; + + const newLabelWithoutHttp = newLabel.replace( /http(s?):\/\//gi, '' ); + const newUrlWithoutHttp = newUrl.replace( /http(s?):\/\//gi, '' ); + + const useNewLabel = + newLabel && + newLabel !== originalLabel && + // LinkControl without the title field relies + // on the check below. Specifically, it assumes that + // the URL is the same as a title. + // This logic a) looks suspicious and b) should really + // live in the LinkControl and not here. It's a great + // candidate for future refactoring. + newLabelWithoutHttp !== newUrlWithoutHttp; + + // Unfortunately this causes the escaping model to be inverted. + // The escaped content is stored in the block attributes (and ultimately in the database), + // and then the raw data is "recovered" when outputting into the DOM. + // It would be preferable to store the **raw** data in the block attributes and escape it in JS. + // Why? Because there isn't one way to escape data. Depending on the context, you need to do + // different transforms. It doesn't make sense to me to choose one of them for the purposes of storage. + // See also: + // - https://github.com/WordPress/gutenberg/pull/41063 + // - https://github.com/WordPress/gutenberg/pull/18617. + const label = useNewLabel + ? escapeHTML( newLabel ) + : originalLabel || escapeHTML( newUrlWithoutHttp ); + + // In https://github.com/WordPress/gutenberg/pull/24670 we decided to use "tag" in favor of "post_tag" + const type = newType === 'post_tag' ? 'tag' : newType.replace( '-', '_' ); + + const isBuiltInType = + [ 'post', 'page', 'tag', 'category' ].indexOf( type ) > -1; + + const isCustomLink = + ( ! newKind && ! isBuiltInType ) || newKind === 'custom'; + const kind = isCustomLink ? 'custom' : newKind; + + setAttributes( { + // Passed `url` may already be encoded. To prevent double encoding, decodeURI is executed to revert to the original string. + ...( newUrl && { url: encodeURI( safeDecodeURI( newUrl ) ) } ), + ...( label && { label } ), + ...( undefined !== opensInNewTab && { opensInNewTab } ), + ...( id && Number.isInteger( id ) && { id } ), + ...( kind && { kind } ), + ...( type && type !== 'URL' && { type } ), + } ); +}; diff --git a/packages/block-editor/src/components/list-view/use-inserted-block.js b/packages/block-editor/src/components/list-view/use-inserted-block.js new file mode 100644 index 00000000000000..0e5a25c980a1c3 --- /dev/null +++ b/packages/block-editor/src/components/list-view/use-inserted-block.js @@ -0,0 +1,47 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +export const useInsertedBlock = ( insertedBlockClientId ) => { + const { insertedBlockAttributes, insertedBlockName } = useSelect( + ( select ) => { + const { getBlockName, getBlockAttributes } = + select( blockEditorStore ); + + return { + insertedBlockAttributes: getBlockAttributes( + insertedBlockClientId + ), + insertedBlockName: getBlockName( insertedBlockClientId ), + }; + }, + [ insertedBlockClientId ] + ); + + const { updateBlockAttributes } = useDispatch( blockEditorStore ); + + const setInsertedBlockAttributes = ( _updatedAttributes ) => { + if ( ! insertedBlockClientId ) return; + updateBlockAttributes( insertedBlockClientId, _updatedAttributes ); + }; + + if ( ! insertedBlockClientId ) { + return { + insertedBlockAttributes: undefined, + insertedBlockName: undefined, + setInsertedBlockAttributes, + }; + } + + return { + insertedBlockAttributes, + insertedBlockName, + setInsertedBlockAttributes, + }; +};