diff --git a/package-lock.json b/package-lock.json index ffe9ff267906a8..67746a860933a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17498,6 +17498,7 @@ "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/dom": "file:packages/dom", "@wordpress/element": "file:packages/element", + "@wordpress/escape-html": "file:packages/escape-html", "@wordpress/hooks": "file:packages/hooks", "@wordpress/html-entities": "file:packages/html-entities", "@wordpress/i18n": "file:packages/i18n", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index e90f7aaf4619f4..59aee136894e78 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -44,6 +44,7 @@ "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", "@wordpress/element": "file:../element", + "@wordpress/escape-html": "file:../escape-html", "@wordpress/hooks": "file:../hooks", "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", diff --git a/packages/block-editor/src/components/inserter/index.js b/packages/block-editor/src/components/inserter/index.js index 17183d13a87090..59c04e24d2bbb4 100644 --- a/packages/block-editor/src/components/inserter/index.js +++ b/packages/block-editor/src/components/inserter/index.js @@ -260,6 +260,7 @@ export default compose( [ allowedBlockType, directInsertBlock, onSelectOrClose, + selectBlockOnInsert, } = ownProps; if ( ! hasSingleBlockType && ! directInsertBlock ) { @@ -370,10 +371,17 @@ export default compose( [ blockToInsert = createBlock( allowedBlockType.name ); } - insertBlock( blockToInsert, getInsertionIndex(), rootClientId ); + insertBlock( + blockToInsert, + getInsertionIndex(), + rootClientId, + selectBlockOnInsert + ); if ( onSelectOrClose ) { - onSelectOrClose(); + onSelectOrClose( { + insertedBlockId: blockToInsert?.clientId, + } ); } const message = sprintf( diff --git a/packages/block-editor/src/components/off-canvas-editor/appender.js b/packages/block-editor/src/components/off-canvas-editor/appender.js index bf4d4e7804fd5f..b19093543c508c 100644 --- a/packages/block-editor/src/components/off-canvas-editor/appender.js +++ b/packages/block-editor/src/components/off-canvas-editor/appender.js @@ -1,15 +1,19 @@ /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; -import { forwardRef } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { forwardRef, useState } from '@wordpress/element'; /** * Internal dependencies */ import { store as blockEditorStore } from '../../store'; import Inserter from '../inserter'; +import { LinkUI } from './link-ui'; +import { updateAttributes } from './update-attributes'; export const Appender = forwardRef( ( props, ref ) => { + const [ insertedBlock, setInsertedBlock ] = useState(); + const { hideInserter, clientId } = useSelect( ( select ) => { const { getTemplateLock, @@ -27,18 +31,71 @@ export const Appender = forwardRef( ( props, ref ) => { }; }, [] ); + const { insertedBlockAttributes } = useSelect( + ( select ) => { + const { getBlockAttributes } = select( blockEditorStore ); + + return { + insertedBlockAttributes: getBlockAttributes( insertedBlock ), + }; + }, + [ insertedBlock ] + ); + + const { updateBlockAttributes } = useDispatch( blockEditorStore ); + + const setAttributes = + ( insertedBlockClientId ) => ( _updatedAttributes ) => { + updateBlockAttributes( insertedBlockClientId, _updatedAttributes ); + }; + + let maybeLinkUI; + + if ( insertedBlock ) { + const link = { + url: insertedBlockAttributes.url, + opensInNewTab: insertedBlockAttributes.opensInNewTab, + title: insertedBlockAttributes.label, + }; + maybeLinkUI = ( + setInsertedBlock( null ) } + hasCreateSuggestion={ false } + onChange={ ( updatedValue ) => { + updateAttributes( + updatedValue, + setAttributes( insertedBlock ), + insertedBlockAttributes + ); + setInsertedBlock( null ); + } } + /> + ); + } + if ( hideInserter ) { return null; } return (
+ { maybeLinkUI } { + setInsertedBlock( insertedBlockId ); + } } { ...props } />
diff --git a/packages/block-editor/src/components/off-canvas-editor/link-ui.js b/packages/block-editor/src/components/off-canvas-editor/link-ui.js new file mode 100644 index 00000000000000..d1bc94896209ea --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/link-ui.js @@ -0,0 +1,160 @@ +// Note: this file is copied directly from packages/block-library/src/navigation-link/link-ui.js + +/** + * WordPress dependencies + */ +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/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, index ) => { + return ( + + ); + } ) } +
+
+ ); +} + +export function LinkUI( props ) { + return ( + + ( + + ) + : null + } + /> + + ); +} diff --git a/packages/block-editor/src/components/off-canvas-editor/update-attributes.js b/packages/block-editor/src/components/off-canvas-editor/update-attributes.js new file mode 100644 index 00000000000000..5133cae3878338 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/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 } ), + } ); +};