Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table of Contents: Try storing in post-meta #54224

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,7 @@ Summarize your post with a list of headings. Add HTML anchors to Heading blocks
- **Experimental:** true
- **Category:** layout
- **Supports:** color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~
- **Attributes:** headings, onlyIncludeCurrentPage
- **Attributes:** onlyIncludeCurrentPage

## Tag Cloud

Expand Down
2 changes: 1 addition & 1 deletion lib/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ function gutenberg_reregister_core_block_types() {
'social-links',
'spacer',
'table',
'table-of-contents',
'text-columns',
'verse',
'video',
Expand Down Expand Up @@ -110,6 +109,7 @@ 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',
'tag-cloud.php' => 'core/tag-cloud',
'template-part.php' => 'core/template-part',
'term-description.php' => 'core/term-description',
Expand Down
8 changes: 1 addition & 7 deletions packages/block-library/src/table-of-contents/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,8 @@
"description": "Summarize your post with a list of headings. Add HTML anchors to Heading blocks to link them here.",
"keywords": [ "document outline", "summary" ],
"textdomain": "default",
"usesContext": [ "postId", "postType" ],
"attributes": {
"headings": {
"type": "array",
"items": {
"type": "object"
},
"default": []
},
"onlyIncludeCurrentPage": {
"type": "boolean",
"default": false
Expand Down
67 changes: 67 additions & 0 deletions packages/block-library/src/table-of-contents/deprecated.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* WordPress dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';

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

const v1 = {
attributes: {
headings: {
type: 'array',
items: {
type: 'object',
},
default: [],
},
onlyIncludeCurrentPage: {
type: 'boolean',
default: false,
},
},
supports: {
html: false,
color: {
text: true,
background: true,
gradients: true,
link: true,
},
spacing: {
margin: true,
padding: true,
},
typography: {
fontSize: true,
lineHeight: true,
__experimentalFontFamily: true,
__experimentalFontWeight: true,
__experimentalFontStyle: true,
__experimentalTextTransform: true,
__experimentalTextDecoration: true,
__experimentalLetterSpacing: true,
__experimentalDefaultControls: {
fontSize: true,
},
},
},
save( { attributes: { headings = [] } } ) {
if ( headings.length === 0 ) {
return null;
}

return (
<nav { ...useBlockProps.save() }>
<TableOfContentsList
nestedHeadingList={ linearToNestedHeadingList( headings ) }
/>
</nav>
);
},
};

export default [ v1 ];
18 changes: 11 additions & 7 deletions packages/block-library/src/table-of-contents/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ToolbarGroup,
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { useEntityProp } from '@wordpress/core-data';
import { renderToString } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

Expand All @@ -26,7 +27,7 @@ import { __ } from '@wordpress/i18n';
import icon from './icon';
import TableOfContentsList from './list';
import { linearToNestedHeadingList } from './utils';
import { useObserveHeadings } from './hooks';
import { getHeadingsFromMeta, useObserveHeadings } from './hooks';

/** @typedef {import('./utils').HeadingData} HeadingData */

Expand All @@ -35,19 +36,22 @@ import { useObserveHeadings } from './hooks';
*
* @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
* @param {Object} props.context
* @param {string} props.context.postType
* @param {number} props.context.postId
*
* @return {WPComponent} The component.
*/
export default function TableOfContentsEdit( {
attributes: { headings = [], onlyIncludeCurrentPage },
attributes: { onlyIncludeCurrentPage },
context: { postType, postId },
clientId,
setAttributes,
} ) {
useObserveHeadings( clientId );
useObserveHeadings( { clientId, postType, postId } );

const blockProps = useBlockProps();

Expand All @@ -64,6 +68,8 @@ export default function TableOfContentsEdit( {

const { replaceBlocks } = useDispatch( blockEditorStore );

const [ meta ] = useEntityProp( 'postType', postType, 'meta', postId );
const headings = getHeadingsFromMeta( meta );
const headingTree = linearToNestedHeadingList( headings );

const toolbarControls = canInsertList && (
Expand Down Expand Up @@ -137,9 +143,7 @@ export default function TableOfContentsEdit( {
return (
<>
<nav { ...blockProps }>
<ol inert="true">
<TableOfContentsList nestedHeadingList={ headingTree } />
</ol>
<TableOfContentsList nestedHeadingList={ headingTree } />
</nav>
{ toolbarControls }
{ inspectorControls }
Expand Down
99 changes: 51 additions & 48 deletions packages/block-library/src/table-of-contents/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@ import fastDeepEqual from 'fast-deep-equal/es6';
* WordPress dependencies
*/
import { useRegistry } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
import { useEffect } from '@wordpress/element';
import { addQueryArgs, removeQueryArgs } from '@wordpress/url';
import { store as blockEditorStore } from '@wordpress/block-editor';

export const META_KEY = '_core_table_of_contents';

export function getHeadingsFromMeta( meta ) {
if ( ! meta?.[ META_KEY ] ) {
return [];
}

return JSON.parse( meta[ META_KEY ] );
}

function getLatestHeadings( select, clientId ) {
const {
getBlockAttributes,
Expand All @@ -20,16 +30,8 @@ function getLatestHeadings( select, clientId ) {
__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 permalink = select( 'core/editor' ).getPermalink() ?? null;

const isPaginated = getGlobalBlocksByName( 'core/nextpage' ).length !== 0;
const { onlyIncludeCurrentPage } = getBlockAttributes( clientId ) ?? {};
const { onlyIncludeCurrentPage } = getBlockAttributes( clientId );

// Get the client ids of all blocks in the editor.
const allBlockClientIds = getClientIdsWithDescendants();
Expand Down Expand Up @@ -57,20 +59,10 @@ function getLatestHeadings( select, clientId ) {
}
}

const latestHeadings = [];

/** The page (of a paginated post) a heading will be part of. */
// The page (of a paginated post) a heading will be part of.
let headingPage = 1;
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;
}

const latestHeadings = [];
for ( const blockClientId of allBlockClientIds ) {
const blockName = getBlockName( blockClientId );
if ( blockName === 'core/nextpage' ) {
Expand All @@ -82,13 +74,6 @@ function getLatestHeadings( select, clientId ) {
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
Expand All @@ -98,7 +83,6 @@ function getLatestHeadings( select, clientId ) {
const headingAttributes = getBlockAttributes( blockClientId );

const canBeLinked =
typeof headingPageLink === 'string' &&
typeof headingAttributes.anchor === 'string' &&
headingAttributes.anchor !== '';

Expand All @@ -111,9 +95,8 @@ function getLatestHeadings( select, clientId ) {
)
),
level: headingAttributes.level,
link: canBeLinked
? `${ headingPageLink }#${ headingAttributes.anchor }`
: null,
link: canBeLinked ? `#${ headingAttributes.anchor }` : null,
page: isPaginated ? headingPage : null,
} );
}
}
Expand All @@ -122,35 +105,55 @@ function getLatestHeadings( select, clientId ) {
return latestHeadings;
}

function observeCallback( select, dispatch, clientId ) {
const { getBlockAttributes } = select( blockEditorStore );
const { updateBlockAttributes, __unstableMarkNextChangeAsNotPersistent } =
dispatch( blockEditorStore );
function observeCallback( select, dispatch, context ) {
const { clientId, postType, postId } = context;
const { getBlock } = select( blockEditorStore );

/**
* If the block no longer exists in the store, skip the update.
* The "undo" action recreates the block and provides a new `clientId`.
* The hook still might be observing the changes while the old block unmounts.
*/
const attributes = getBlockAttributes( clientId );
if ( attributes === null ) {
if ( getBlock( clientId ) === null ) {
return;
}

const { getEditedEntityRecord } = select( coreStore );
const { editEntityRecord } = dispatch( coreStore );

const meta = getEditedEntityRecord( 'postType', postType, postId ).meta;
const storedHeadings = getHeadingsFromMeta( meta );

const headings = getLatestHeadings( select, clientId );
if ( ! fastDeepEqual( headings, attributes.headings ) ) {
__unstableMarkNextChangeAsNotPersistent();
updateBlockAttributes( clientId, { headings } );
if ( ! fastDeepEqual( headings, storedHeadings ) ) {
editEntityRecord(
'postType',
postType,
postId,
{
meta: {
...meta,
[ META_KEY ]: JSON.stringify( headings ),
},
},
{
undoIgnore: true,
Mamaduka marked this conversation as resolved.
Show resolved Hide resolved
}
);
}
}

export function useObserveHeadings( clientId ) {
export function useObserveHeadings( { clientId, postType, postId } ) {
const registry = useRegistry();
useEffect( () => {
// Todo: Limit subscription to block editor store when data no longer depends on `getPermalink`.
// See: https://github.com/WordPress/gutenberg/pull/45513
return registry.subscribe( () =>
observeCallback( registry.select, registry.dispatch, clientId )
return registry.subscribe(
() =>
observeCallback( registry.select, registry.dispatch, {
clientId,
postType,
postId,
} ),
blockEditorStore
);
}, [ registry, clientId ] );
}, [ registry, clientId, postType, postId ] );
}
4 changes: 2 additions & 2 deletions packages/block-library/src/table-of-contents/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import initBlock from '../utils/init-block';
import metadata from './block.json';
import edit from './edit';
import icon from './icon';
import save from './save';
import deprecated from './deprecated';

const { name } = metadata;

Expand All @@ -14,7 +14,7 @@ export { metadata, name };
export const settings = {
icon,
edit,
save,
deprecated,
};

export const init = () => initBlock( { name, metadata, settings } );
Loading
Loading