-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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: Use a custom store subscription for observing headings #54094
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import fastDeepEqual from 'fast-deep-equal/es6'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useRegistry } from '@wordpress/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'; | ||
|
||
function getLatestHeadings( select, clientId ) { | ||
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 permalink = select( 'core/editor' ).getPermalink() ?? null; | ||
|
||
const isPaginated = getGlobalBlocksByName( 'core/nextpage' ).length !== 0; | ||
const { onlyIncludeCurrentPage } = getBlockAttributes( clientId ) ?? {}; | ||
|
||
// 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; | ||
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, | ||
} ); | ||
} | ||
} | ||
} | ||
|
||
return latestHeadings; | ||
} | ||
|
||
function observeCallback( select, dispatch, clientId ) { | ||
const { getBlockAttributes } = select( blockEditorStore ); | ||
const { updateBlockAttributes, __unstableMarkNextChangeAsNotPersistent } = | ||
dispatch( 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 ) { | ||
return; | ||
} | ||
|
||
const headings = getLatestHeadings( select, clientId ); | ||
if ( ! fastDeepEqual( headings, attributes.headings ) ) { | ||
__unstableMarkNextChangeAsNotPersistent(); | ||
updateBlockAttributes( clientId, { headings } ); | ||
} | ||
} | ||
|
||
export function useObserveHeadings( clientId ) { | ||
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 ) | ||
); | ||
}, [ registry, clientId ] ); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ZebulanStanphill, do you remember the reason for using absolute URLs instead of relative ones?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relative links don't work in situations where the contents of the post (and therefore the Table of Contents, if the post has one) appear on a page other than the one dedicated to the post itself. The most common example of this: a blog template where the entire contents of multiple posts are laid out (which was the default home page template for several past WordPress themes).