Skip to content

Commit

Permalink
Open LinkUI popover after appending link from Navigation side block i…
Browse files Browse the repository at this point in the history
…nserter

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.
  • Loading branch information
jeryj committed May 9, 2023
1 parent cd5ccf9 commit e73a0ae
Show file tree
Hide file tree
Showing 4 changed files with 379 additions and 5 deletions.
71 changes: 66 additions & 5 deletions packages/block-editor/src/components/list-view/block-contents.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
(
{
Expand All @@ -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 =
Expand All @@ -67,6 +111,23 @@ const ListViewBlockContents = forwardRef(
return (
<>
{ renderAdditionalBlockUI && renderAdditionalBlockUI( block ) }
{ isLinkUIOpen && (
<LinkUI
clientId={ lastInsertedBlockClientId }
link={ insertedBlockAttributes }
onClose={ () => setIsLinkUIOpen( false ) }
hasCreateSuggestion={ false }
onChange={ ( updatedValue ) => {
updateAttributes(
updatedValue,
setInsertedBlockAttributes,
insertedBlockAttributes
);
setIsLinkUIOpen( false );
} }
onCancel={ () => setIsLinkUIOpen( false ) }
/>
) }
<BlockDraggable clientIds={ draggableClientIds }>
{ ( { draggable, onDragStart, onDragEnd } ) => (
<ListViewBlockSelectButton
Expand Down
167 changes: 167 additions & 0 deletions packages/block-editor/src/components/list-view/link-ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Note: this file is copied directly from packages/block-library/src/navigation-link/link-ui.js

/**
* WordPress dependencies
*/
import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
import { Popover, Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { switchToBlockType } from '@wordpress/blocks';
import { useSelect, useDispatch } from '@wordpress/data';

/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
import LinkControl from '../link-control';
import BlockIcon from '../block-icon';

/**
* Given the Link block's type attribute, return the query params to give to
* /wp/v2/search.
*
* @param {string} type Link block's type attribute.
* @param {string} kind Link block's entity of kind (post-type|taxonomy)
* @return {{ type?: string, subtype?: string }} Search query params.
*/
export function getSuggestionsQuery( type, kind ) {
switch ( type ) {
case 'post':
case 'page':
return { type: 'post', subtype: type };
case 'category':
return { type: 'term', subtype: 'category' };
case 'tag':
return { type: 'term', subtype: 'post_tag' };
case 'post_format':
return { type: 'post-format' };
default:
if ( kind === 'taxonomy' ) {
return { type: 'term', subtype: type };
}
if ( kind === 'post-type' ) {
return { type: 'post', subtype: type };
}
return {};
}
}

/**
* Add transforms to Link Control
*
* @param {Object} props Component props.
* @param {string} props.clientId Block client ID.
*/
function LinkControlTransforms( { clientId } ) {
const { getBlock, blockTransforms } = useSelect(
( select ) => {
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 (
<div className="link-control-transform">
<h3 className="link-control-transform__subheading">
{ __( 'Transform' ) }
</h3>
<div className="link-control-transform__items">
{ transforms.map( ( item ) => {
return (
<Button
key={ `transform-${ item.name }` }
onClick={ () =>
replaceBlock(
clientId,
switchToBlockType(
getBlock( clientId ),
item.name
)
)
}
className="link-control-transform__item"
>
<BlockIcon icon={ item.icon } />
{ item.title }
</Button>
);
} ) }
</div>
</div>
);
}

export function LinkUI( props ) {
const { label, url, opensInNewTab, type, kind } = props.link;
const link = {
url,
opensInNewTab,
title: label && stripHTML( label ),
};

return (
<Popover
placement="bottom"
onClose={ props.onClose }
anchor={ props.anchor }
shift
>
<LinkControl
hasTextControl
hasRichPreviews
className={ props.className }
value={ link }
showInitialSuggestions={ true }
withCreateSuggestion={ props.hasCreateSuggestion }
noDirectEntry={ !! type }
noURLSuggestion={ !! type }
suggestionsQuery={ getSuggestionsQuery( type, kind ) }
onChange={ props.onChange }
onRemove={ props.onRemove }
onCancel={ props.onCancel }
renderControlBottom={
! url
? () => (
<LinkControlTransforms
clientId={ props.clientId }
/>
)
: null
}
/>
</Popover>
);
}
Original file line number Diff line number Diff line change
@@ -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 } ),
} );
};
Loading

0 comments on commit e73a0ae

Please sign in to comment.