Skip to content

Commit

Permalink
ToC block: use static markup and only support core Heading and Page B…
Browse files Browse the repository at this point in the history
…reak blocks.
  • Loading branch information
ZebulanStanphill committed Mar 15, 2021
1 parent be99190 commit cd93c8f
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 475 deletions.
3 changes: 1 addition & 2 deletions lib/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function gutenberg_reregister_core_block_types() {
'social-links',
'spacer',
'table',
// 'table-of-contents',
'table-of-contents',
'text-columns',
'verse',
'video',
Expand Down Expand Up @@ -90,7 +90,6 @@ function gutenberg_reregister_core_block_types() {
'site-logo.php' => 'core/site-logo',
'site-tagline.php' => 'core/site-tagline',
'site-title.php' => 'core/site-title',
// 'table-of-contents.php' => 'core/table-of-contents',
'template-part.php' => 'core/template-part',
'term-description.php' => 'core/term-description',
)
Expand Down
4 changes: 2 additions & 2 deletions packages/block-library/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import * as separator from './separator';
import * as shortcode from './shortcode';
import * as spacer from './spacer';
import * as table from './table';
// import * as tableOfContents from './table-of-contents';
import * as tableOfContents from './table-of-contents';
import * as textColumns from './text-columns';
import * as verse from './verse';
import * as video from './video';
Expand Down Expand Up @@ -163,7 +163,7 @@ export const __experimentalGetCoreBlocks = () => [
socialLink,
spacer,
table,
// tableOfContents,
tableOfContents,
tagCloud,
textColumns,
verse,
Expand Down
10 changes: 8 additions & 2 deletions packages/block-library/src/table-of-contents/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
"name": "core/table-of-contents",
"category": "layout",
"attributes": {
"headings": {
"type": "array",
"items": {
"type": "object"
}
},
"onlyIncludeCurrentPage": {
"type": "boolean",
"default": false
}
},
"usesContext": [ "postId" ],
"supports": {
"html": false
}
},
"example": {}
}
154 changes: 92 additions & 62 deletions packages/block-library/src/table-of-contents/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,32 @@ import {
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { renderToString, useEffect, useState } from '@wordpress/element';
import { renderToString, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { addQueryArgs, removeQueryArgs } from '@wordpress/url';

/**
* Internal dependencies
*/
import icon from './icon';
import TableOfContentsList from './list';
import { getHeadingsFromContent, linearToNestedHeadingList } from './utils';
import { linearToNestedHeadingList } from './utils';

/**
* @typedef HeadingData
*
* @property {string} content The plain text content of the heading.
* @property {number} level The heading level.
* @property {string} link Link to the heading.
*/

/**
* 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).
Expand All @@ -46,94 +58,112 @@ import { getHeadingsFromContent, linearToNestedHeadingList } from './utils';
* @return {WPComponent} The component.
*/
export default function TableOfContentsEdit( {
attributes: { onlyIncludeCurrentPage },
attributes: { headings = [], onlyIncludeCurrentPage },
clientId,
setAttributes,
} ) {
const blockProps = useBlockProps();

// Local state; not saved to block attributes. The saved block is dynamic and uses PHP to generate its content.
const [ headings, setHeadings ] = useState( [] );
const [ headingTree, setHeadingTree ] = useState( [] );

const { listBlockExists, postContent } = useSelect(
( select ) => ( {
listBlockExists: !! select( blocksStore ).getBlockType(
'core/list'
),
postContent: select( editorStore ).getEditedPostContent(),
} ),
const listBlockExists = useSelect(
( select ) => !! select( blocksStore ).getBlockType( 'core/list' ),
[]
);

const {
// __unstableMarkNextChangeAsNotPersistent,
replaceBlocks,
} = useDispatch( blockEditorStore );

// The page this block would be part of on the front-end. For performance
// reasons, this is only calculated when onlyIncludeCurrentPage is true.
const pageIndex = useSelect(
useSelect(
( select ) => {
if ( ! onlyIncludeCurrentPage ) {
return null;
}

const {
getBlockAttributes,
getBlockIndex,
getBlockName,
getBlockOrder,
getGlobalBlockCount,
} = select( blockEditorStore );
const { getPermalink } = select( editorStore );

const isPaginated = getGlobalBlockCount( 'core/nextpage' ) !== 0;

const blockIndex = getBlockIndex( clientId );
const blockOrder = getBlockOrder();

// Calculate which page the block will appear in on the front-end by
// counting how many <!--nextpage--> tags precede it.
// Unfortunately, this implementation only accounts for Page Break and
// Classic blocks, so if there are any <!--nextpage--> tags in any
// other block, they won't be counted. This will result in the table
// of contents showing headings from the wrong page if
// onlyIncludeCurrentPage === true. Thankfully, this issue only
// affects the editor implementation.
let page = 1;
for ( let i = 0; i < blockIndex; i++ ) {
const blockName = getBlockName( blockOrder[ i ] );
if ( blockName === 'core/nextpage' ) {
page++;
} else if ( blockName === 'core/freeform' ) {
// Count the page breaks inside the Classic block.
const pageBreaks = getBlockAttributes(
blockOrder[ i ]
).content?.match( /<!--nextpage-->/g );

if ( pageBreaks !== null && pageBreaks !== undefined ) {
page += pageBreaks.length;
}
}
}
const latestHeadings = [];

return page;
},
[ clientId, onlyIncludeCurrentPage ]
);
// The page (of a paginated post) the Table of Contents block will be
// part of.
let tocPage = 1;

useEffect( () => {
let latestHeadings;
// The page (of a paginated post) a heading will be part of.
let headingPage = 1;

if ( onlyIncludeCurrentPage ) {
const pagesOfContent = postContent.split( '<!--nextpage-->' );
// Link to post including pagination query if necessary.
const permalink = getPermalink();

latestHeadings = getHeadingsFromContent(
pagesOfContent[ pageIndex - 1 ]
);
} else {
latestHeadings = getHeadingsFromContent( postContent );
}
let headingPageLink = isPaginated
? addQueryArgs( permalink, { page: headingPage } )
: permalink;

if ( ! isEqual( headings, latestHeadings ) ) {
setHeadings( latestHeadings );
setHeadingTree( linearToNestedHeadingList( latestHeadings ) );
}
}, [ pageIndex, postContent, onlyIncludeCurrentPage ] );
for ( const [ i, blockClientId ] of blockOrder.entries() ) {
const blockName = getBlockName( blockClientId );
if ( blockName === 'core/nextpage' ) {
headingPage++;
headingPageLink = addQueryArgs(
removeQueryArgs( permalink, [ 'page' ] ),
{ page: headingPage }
);
if ( i < blockIndex ) {
tocPage++;
}
} else if ( blockName === 'core/heading' ) {
// If we're only including headings from the current page (of a
// paginated post), then exit the loop if we've reached headings
// on the pages after the one with the Table of Contents block.
if ( onlyIncludeCurrentPage && headingPage > tocPage ) {
break;
}
// 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.
if ( ! onlyIncludeCurrentPage || headingPage === tocPage ) {
const headingAttributes = getBlockAttributes(
blockClientId
);

const hasAnchor =
typeof headingAttributes.anchor === 'string' &&
headingAttributes.anchor !== '';

latestHeadings.push( {
content: headingAttributes.content,
level: headingAttributes.level,
link: hasAnchor
? `${ headingPageLink }#${ headingAttributes.anchor }`
: null,
// page: headingPage,
} );
}
}
}

const { replaceBlocks } = useDispatch( blockEditorStore );
if ( ! isEqual( headings, latestHeadings ) ) {
// __unstableMarkNextChangeAsNotPersistent();
setAttributes( { headings: latestHeadings } );
setHeadingTree( linearToNestedHeadingList( latestHeadings ) );
}
}
// ,[
// clientId,
// onlyIncludeCurrentPage,
// // __unstableMarkNextChangeAsNotPersistent,
// ]
);

const toolbarControls = listBlockExists && (
<BlockControls>
Expand Down Expand Up @@ -189,7 +219,7 @@ export default function TableOfContentsEdit( {
<>
<div { ...blockProps }>
<Placeholder
icon={ <BlockIcon icon="list-view" /> }
icon={ <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.'
Expand Down
2 changes: 2 additions & 0 deletions packages/block-library/src/table-of-contents/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { __ } from '@wordpress/i18n';
import metadata from './block.json';
import edit from './edit';
import icon from './icon';
import save from './save';

const { name } = metadata;

Expand All @@ -22,4 +23,5 @@ export const settings = {
icon,
keywords: [ __( 'document outline' ), __( 'summary' ) ],
edit,
save,
};
Loading

0 comments on commit cd93c8f

Please sign in to comment.