Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 91 additions & 71 deletions packages/block-library/src/table-of-contents/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import {
SelectControl,
ToolbarButton,
ToolbarGroup,
__experimentalConfirmDialog as ConfirmDialog,
__experimentalToolsPanel as ToolsPanel,
__experimentalToolsPanelItem as ToolsPanelItem,
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { renderToString } from '@wordpress/element';
import { useState } from '@wordpress/element';
import { __, isRTL } from '@wordpress/i18n';
import { useInstanceId } from '@wordpress/compose';
import { store as noticeStore } from '@wordpress/notices';
Expand All @@ -35,26 +36,93 @@ import {
* Internal dependencies
*/
import TableOfContentsList from './list';
import { linearToNestedHeadingList } from './utils';
import { createListItemBlocks, linearToNestedHeadingList } from './utils';
import { useObserveHeadings } from './hooks';
import { useToolsPanelDropdownMenuProps } from '../utils/hooks';

/** @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 The 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 {number|undefined} props.attributes.maxLevel The maximum heading level to include, or null to include all levels.
* @param {boolean} props.attributes.ordered Whether to display as an ordered list (true) or unordered list (false).
* @param {string} props.clientId The client id.
* @param {(attributes: Object) => void} props.setAttributes The set attributes function.
*
* @return {Component} The component.
*/
Comment on lines -44 to -57

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't used by any autogenerated docs, and the block README already covers important bits.

function TableOfContentsToolbar( {
clientId,
headingTree,
ordered,
setAttributes,
} ) {
const canInsertList = useSelect(
( select ) => {
const { getBlockRootClientId, canInsertBlockType } =
select( blockEditorStore );
const rootClientId = getBlockRootClientId( clientId );

return canInsertBlockType( 'core/list', rootClientId );
},
[ clientId ]
);
const { replaceBlocks } = useDispatch( blockEditorStore );
const [ isConfirmingDetach, setIsConfirmingDetach ] = useState( false );

return (
<>
<BlockControls>
<ToolbarGroup>
<ToolbarButton
icon={
isRTL() ? formatListBulletsRTL : formatListBullets
}
title={ __( 'Unordered' ) }
description={ __( 'Convert to unordered list' ) }
onClick={ () => setAttributes( { ordered: false } ) }
isActive={ ordered === false }
/>
<ToolbarButton
icon={
isRTL() ? formatListNumberedRTL : formatListNumbered
}
title={ __( 'Ordered' ) }
description={ __( 'Convert to ordered list' ) }
onClick={ () => setAttributes( { ordered: true } ) }
isActive={ ordered === true }
/>
</ToolbarGroup>
{ canInsertList && (
<ToolbarGroup>
<ToolbarButton
onClick={ () => setIsConfirmingDetach( true ) }
>
{ __( 'Detach' ) }
</ToolbarButton>
</ToolbarGroup>
) }
</BlockControls>
{ isConfirmingDetach && (
<ConfirmDialog
isOpen
title={ __( 'Detach Table of Contents' ) }
__experimentalHideHeader={ false }
confirmButtonText={ __( 'Detach' ) }
onConfirm={ () => {
setIsConfirmingDetach( false );
replaceBlocks(
clientId,
createBlock(
'core/list',
{ ordered },
createListItemBlocks( headingTree, ordered )
)
);
} }
onCancel={ () => setIsConfirmingDetach( false ) }
size="medium"
>
{ __(
'The table of contents lists the headings in the post. Detaching will enable you to edit, reorder, or remove entries. However, new headings will no longer be added automatically.'
Comment thread
Mamaduka marked this conversation as resolved.
Resolved
) }
</ConfirmDialog>
) }
</>
);
}

export default function TableOfContentsEdit( {
attributes: {
headings = [],
Expand Down Expand Up @@ -83,64 +151,16 @@ export default function TableOfContentsEdit( {
} );
};

const canInsertList = useSelect(
( select ) => {
const { getBlockRootClientId, canInsertBlockType } =
select( blockEditorStore );
const rootClientId = getBlockRootClientId( clientId );

return canInsertBlockType( 'core/list', rootClientId );
},
[ clientId ]
);

const { replaceBlocks } = useDispatch( blockEditorStore );
const dropdownMenuProps = useToolsPanelDropdownMenuProps();
const headingTree = linearToNestedHeadingList( headings );

const toolbarControls = (
<BlockControls>
<ToolbarGroup>
<ToolbarButton
icon={ isRTL() ? formatListBulletsRTL : formatListBullets }
title={ __( 'Unordered' ) }
description={ __( 'Convert to unordered list' ) }
onClick={ () => setAttributes( { ordered: false } ) }
isActive={ ordered === false }
/>
<ToolbarButton
icon={
isRTL() ? formatListNumberedRTL : formatListNumbered
}
title={ __( 'Ordered' ) }
description={ __( 'Convert to ordered list' ) }
onClick={ () => setAttributes( { ordered: true } ) }
isActive={ ordered === true }
/>
</ToolbarGroup>
{ canInsertList && (
<ToolbarGroup>
<ToolbarButton
onClick={ () =>
replaceBlocks(
clientId,
createBlock( 'core/list', {
ordered,
values: renderToString(
<TableOfContentsList
nestedHeadingList={ headingTree }
ordered={ ordered }
/>
),
} )
)
}
>
{ __( 'Convert to static list' ) }
</ToolbarButton>
</ToolbarGroup>
) }
</BlockControls>
<TableOfContentsToolbar
clientId={ clientId }
headingTree={ headingTree }
ordered={ ordered }
setAttributes={ setAttributes }
/>
);

const inspectorControls = (
Expand Down Expand Up @@ -223,7 +243,7 @@ export default function TableOfContentsEdit( {

// 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.
// "Detach" option is useless to the placeholder state.
if ( headings.length === 0 ) {
return (
<>
Expand Down
46 changes: 46 additions & 0 deletions packages/block-library/src/table-of-contents/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* WordPress dependencies
*/
import { createBlock } from '@wordpress/blocks';
import type { Block } from '@wordpress/blocks';
import { escapeAttribute, escapeHTML } from '@wordpress/escape-html';

export interface HeadingData {
/** The plain text content of the heading. */
content: string;
Expand Down Expand Up @@ -69,3 +76,42 @@ export function linearToNestedHeadingList(

return nestedHeadingList;
}

/**
* Converts a nested heading list into `core/list-item` blocks, with
* sub-headings nested in a `core/list` block inside their parent item.
*
* @param nestedHeadingList The nested list of headings.
* @param ordered Whether nested lists are ordered.
*
* @return The list item blocks.
*/
export function createListItemBlocks(
nestedHeadingList: NestedHeadingData[],
ordered: boolean
): Block[] {
return nestedHeadingList.map(
( { heading: { content, link }, children } ) =>
createBlock(
'core/list-item',
{
// Headings are plain text, and links can carry query args on
// paginated posts, so both need escaping to become markup.
content: link
? `<a href="${ escapeAttribute( link ) }">${ escapeHTML(
content
) }</a>`
: escapeHTML( content ),
Comment thread
Mamaduka marked this conversation as resolved.
Resolved
},
children
Comment thread
Mamaduka marked this conversation as resolved.
Resolved
? [
createBlock(
'core/list',
{ ordered },
createListItemBlocks( children, ordered )
),
]
: []
)
);
}
Loading