@wordpress/block-library
Version: 
Block library for the WordPress editor.
193 lines (169 loc) • 4.81 kB
JavaScript
/**
 * External dependencies
 */
import { v4 as createId } from 'uuid';
/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';
import { formatListNumbered as icon } from '@wordpress/icons';
import { insertObject } from '@wordpress/rich-text';
import {
	RichTextToolbarButton,
	store as blockEditorStore,
	privateApis,
} from '@wordpress/block-editor';
import { useSelect, useDispatch, useRegistry } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
import { createBlock, store as blocksStore } from '@wordpress/blocks';
/**
 * Internal dependencies
 */
import { unlock } from '../lock-unlock';
const { usesContextKey } = unlock( privateApis );
export const formatName = 'core/footnote';
const POST_CONTENT_BLOCK_NAME = 'core/post-content';
const SYNCED_PATTERN_BLOCK_NAME = 'core/block';
export const format = {
	title: __( 'Footnote' ),
	tagName: 'sup',
	className: 'fn',
	attributes: {
		'data-fn': 'data-fn',
	},
	interactive: true,
	contentEditable: false,
	[ usesContextKey ]: [ 'postType', 'postId' ],
	edit: function Edit( {
		value,
		onChange,
		isObjectActive,
		context: { postType, postId },
	} ) {
		const registry = useRegistry();
		const {
			getSelectedBlockClientId,
			getBlocks,
			getBlockRootClientId,
			getBlockName,
			getBlockParentsByBlockName,
		} = registry.select( blockEditorStore );
		const isFootnotesSupported = useSelect(
			( select ) => {
				if (
					! select( blocksStore ).getBlockType( 'core/footnotes' )
				) {
					return false;
				}
				const allowedBlocks =
					select( blockEditorStore ).getSettings().allowedBlockTypes;
				if (
					allowedBlocks === false ||
					( Array.isArray( allowedBlocks ) &&
						! allowedBlocks.includes( 'core/footnotes' ) )
				) {
					return false;
				}
				const entityRecord = select( coreDataStore ).getEntityRecord(
					'postType',
					postType,
					postId
				);
				if ( 'string' !== typeof entityRecord?.meta?.footnotes ) {
					return false;
				}
				// Checks if the selected block lives within a pattern.
				const {
					getBlockParentsByBlockName: _getBlockParentsByBlockName,
					getSelectedBlockClientId: _getSelectedBlockClientId,
				} = select( blockEditorStore );
				const parentCoreBlocks = _getBlockParentsByBlockName(
					_getSelectedBlockClientId(),
					SYNCED_PATTERN_BLOCK_NAME
				);
				return ! parentCoreBlocks || parentCoreBlocks.length === 0;
			},
			[ postType, postId ]
		);
		const { selectionChange, insertBlock } =
			useDispatch( blockEditorStore );
		if ( ! isFootnotesSupported ) {
			return null;
		}
		function onClick() {
			registry.batch( () => {
				let id;
				if ( isObjectActive ) {
					const object = value.replacements[ value.start ];
					id = object?.attributes?.[ 'data-fn' ];
				} else {
					id = createId();
					const newValue = insertObject(
						value,
						{
							type: formatName,
							attributes: {
								'data-fn': id,
							},
							innerHTML: `<a href="#${ id }" id="${ id }-link">*</a>`,
						},
						value.end,
						value.end
					);
					newValue.start = newValue.end - 1;
					onChange( newValue );
				}
				const selectedClientId = getSelectedBlockClientId();
				/*
				 * Attempts to find a common parent post content block.
				 * This allows for locating blocks within a page edited in the site editor.
				 */
				const parentPostContent = getBlockParentsByBlockName(
					selectedClientId,
					POST_CONTENT_BLOCK_NAME
				);
				// When called with a post content block, getBlocks will return
				// the block with controlled inner blocks included.
				const blocks = parentPostContent.length
					? getBlocks( parentPostContent[ 0 ] )
					: getBlocks();
				// BFS search to find the first footnote block.
				let fnBlock = null;
				{
					const queue = [ ...blocks ];
					while ( queue.length ) {
						const block = queue.shift();
						if ( block.name === 'core/footnotes' ) {
							fnBlock = block;
							break;
						}
						queue.push( ...block.innerBlocks );
					}
				}
				// Maybe this should all also be moved to the entity provider.
				// When there is no footnotes block in the post, create one and
				// insert it at the bottom.
				if ( ! fnBlock ) {
					let rootClientId = getBlockRootClientId( selectedClientId );
					while (
						rootClientId &&
						getBlockName( rootClientId ) !== POST_CONTENT_BLOCK_NAME
					) {
						rootClientId = getBlockRootClientId( rootClientId );
					}
					fnBlock = createBlock( 'core/footnotes' );
					insertBlock( fnBlock, undefined, rootClientId );
				}
				selectionChange( fnBlock.clientId, id, 0, 0 );
			} );
		}
		return (
			<RichTextToolbarButton
				icon={ icon }
				title={ __( 'Footnote' ) }
				onClick={ onClick }
				isActive={ isObjectActive }
			/>
		);
	},
};