UNPKG

@wordpress/block-library

Version:
231 lines (204 loc) 9.55 kB
import { createElement, Fragment } from "@wordpress/element"; /** * External dependencies */ import { isEqual } from 'lodash'; /** * WordPress dependencies */ import { BlockControls, BlockIcon, InspectorControls, store as blockEditorStore, useBlockProps } from '@wordpress/block-editor'; import { createBlock } from '@wordpress/blocks'; import { PanelBody, Placeholder, ToggleControl, ToolbarButton, ToolbarGroup } from '@wordpress/components'; import { useDisabled } from '@wordpress/compose'; import { useDispatch, useSelect } from '@wordpress/data'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; import { renderToString, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ import icon from './icon'; import TableOfContentsList from './list'; import { linearToNestedHeadingList } from './utils'; /** @typedef {import('./utils').HeadingData} HeadingData */ /** * Table of Contents block edit component. * * @param {Object} props The props. * @param {Object} props.attributes The block attributes. * @param {HeadingData[]} props.attributes.headings A list of data for each heading in the post. * @param {boolean} props.attributes.onlyIncludeCurrentPage Whether to only include headings from the current page (if the post is paginated). * @param {string} props.clientId * @param {(attributes: Object) => void} props.setAttributes * * @return {WPComponent} The component. */ export default function TableOfContentsEdit(_ref) { let { attributes: { headings = [], onlyIncludeCurrentPage }, clientId, setAttributes } = _ref; const blockProps = useBlockProps(); const disabledRef = useDisabled(); const canInsertList = useSelect(select => { const { getBlockRootClientId, canInsertBlockType } = select(blockEditorStore); const rootClientId = getBlockRootClientId(clientId); return canInsertBlockType('core/list', rootClientId); }, [clientId]); const { __unstableMarkNextChangeAsNotPersistent, replaceBlocks } = useDispatch(blockEditorStore); /** * The latest heading data, or null if the new data deeply equals the saved * headings attribute. * * Since useSelect forces a re-render when its return value is shallowly * inequal to its prior call, we would be re-rendering this block every time * the stores change, even if the latest headings were deeply equal to the * ones saved in the block attributes. * * By returning null when they're equal, we reduce that to 2 renders: one * when there are new latest headings (and so it returns them), and one when * they haven't changed (so it returns null). As long as the latest heading * data remains the same, further calls of the useSelect callback will * continue to return null, thus preventing any forced re-renders. */ const latestHeadings = useSelect(select => { var _editorSelectors$getP; const { getBlockAttributes, getBlockName, getClientIdsWithDescendants, __experimentalGetGlobalBlocksByName: getGlobalBlocksByName } = select(blockEditorStore); // FIXME: @wordpress/block-library should not depend on @wordpress/editor. // Blocks can be loaded into a *non-post* block editor, so to avoid // declaring @wordpress/editor as a dependency, we must access its // store by string. When the store is not available, editorSelectors // will be null, and the block's saved markup will lack permalinks. // eslint-disable-next-line @wordpress/data-no-store-string-literals const editorSelectors = select('core/editor'); const pageBreakClientIds = getGlobalBlocksByName('core/nextpage'); const isPaginated = pageBreakClientIds.length !== 0; // Get the client ids of all blocks in the editor. const allBlockClientIds = getClientIdsWithDescendants(); // If onlyIncludeCurrentPage is true, calculate the page (of a paginated post) this block is part of, so we know which headings to include; otherwise, skip the calculation. let tocPage = 1; if (isPaginated && onlyIncludeCurrentPage) { // We can't use getBlockIndex because it only returns the index // relative to sibling blocks. const tocIndex = allBlockClientIds.indexOf(clientId); for (const [blockIndex, blockClientId] of allBlockClientIds.entries()) { // If we've reached blocks after the Table of Contents, we've // finished calculating which page the block is on. if (blockIndex >= tocIndex) { break; } if (getBlockName(blockClientId) === 'core/nextpage') { tocPage++; } } } const _latestHeadings = []; /** The page (of a paginated post) a heading will be part of. */ let headingPage = 1; /** * A permalink to the current post. If the core/editor store is * unavailable, this variable will be null. */ const permalink = (_editorSelectors$getP = editorSelectors === null || editorSelectors === void 0 ? void 0 : editorSelectors.getPermalink()) !== null && _editorSelectors$getP !== void 0 ? _editorSelectors$getP : null; let headingPageLink = null; // If the core/editor store is available, we can add permalinks to the // generated table of contents. if (typeof permalink === 'string') { headingPageLink = isPaginated ? addQueryArgs(permalink, { page: headingPage }) : permalink; } for (const blockClientId of allBlockClientIds) { const blockName = getBlockName(blockClientId); if (blockName === 'core/nextpage') { headingPage++; // If we're only including headings from the current page (of // a paginated post), then exit the loop if we've reached the // pages after the one with the Table of Contents block. if (onlyIncludeCurrentPage && headingPage > tocPage) { break; } if (typeof permalink === 'string') { headingPageLink = addQueryArgs(removeQueryArgs(permalink, ['page']), { page: headingPage }); } } // If we're including all headings or we've reached headings on // the same page as the Table of Contents block, add them to the // list. else if (!onlyIncludeCurrentPage || headingPage === tocPage) { if (blockName === 'core/heading') { const headingAttributes = getBlockAttributes(blockClientId); const canBeLinked = typeof headingPageLink === 'string' && typeof headingAttributes.anchor === 'string' && headingAttributes.anchor !== ''; _latestHeadings.push({ // Convert line breaks to spaces, and get rid of HTML tags in the headings. content: stripHTML(headingAttributes.content.replace(/(<br *\/?>)+/g, ' ')), level: headingAttributes.level, link: canBeLinked ? `${headingPageLink}#${headingAttributes.anchor}` : null }); } } } if (isEqual(headings, _latestHeadings)) { return null; } return _latestHeadings; }, [clientId, onlyIncludeCurrentPage, headings]); useEffect(() => { if (latestHeadings !== null) { // This is required to keep undo working and not create 2 undo steps // for each heading change. __unstableMarkNextChangeAsNotPersistent(); setAttributes({ headings: latestHeadings }); } }, [latestHeadings]); const headingTree = linearToNestedHeadingList(headings); const toolbarControls = canInsertList && createElement(BlockControls, null, createElement(ToolbarGroup, null, createElement(ToolbarButton, { onClick: () => replaceBlocks(clientId, createBlock('core/list', { ordered: true, values: renderToString(createElement(TableOfContentsList, { nestedHeadingList: headingTree })) })) }, __('Convert to static list')))); const inspectorControls = createElement(InspectorControls, null, createElement(PanelBody, { title: __('Settings') }, createElement(ToggleControl, { label: __('Only include current page'), checked: onlyIncludeCurrentPage, onChange: value => setAttributes({ onlyIncludeCurrentPage: value }), help: onlyIncludeCurrentPage ? __('Only including headings from the current page (if the post is paginated).') : __('Toggle to only include headings from the current page (if the post is paginated).') }))); // If there are no headings or the only heading is empty. // Note that the toolbar controls are intentionally omitted since the // "Convert to static list" option is useless to the placeholder state. if (headings.length === 0) { return createElement(Fragment, null, createElement("div", blockProps, createElement(Placeholder, { icon: createElement(BlockIcon, { icon: icon }), label: "Table of Contents", instructions: __('Start adding Heading blocks to create a table of contents. Headings with HTML anchors will be linked here.') })), inspectorControls); } return createElement(Fragment, null, createElement("nav", blockProps, createElement("ol", { ref: disabledRef }, createElement(TableOfContentsList, { nestedHeadingList: headingTree }))), toolbarControls, inspectorControls); } //# sourceMappingURL=edit.js.map