@wordpress/block-library
Version:
Block library for the WordPress editor.
149 lines (139 loc) • 5.93 kB
JavaScript
/**
* 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) {
var _select$getPermalink, _getBlockAttributes;
const {
getBlockAttributes,
getBlockName,
getBlocksByName,
getClientIdsOfDescendants
} = 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$getPermalink = select('core/editor').getPermalink()) !== null && _select$getPermalink !== void 0 ? _select$getPermalink : null;
const isPaginated = getBlocksByName('core/nextpage').length !== 0;
const {
onlyIncludeCurrentPage,
maxLevel
} = (_getBlockAttributes = getBlockAttributes(clientId)) !== null && _getBlockAttributes !== void 0 ? _getBlockAttributes : {};
// Get post-content block client ID.
const [postContentClientId = ''] = getBlocksByName('core/post-content');
// Get the client ids of all blocks in the editor.
const allBlockClientIds = getClientIdsOfDescendants(postContentClientId);
// 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);
// Skip headings that are deeper than maxLevel
if (maxLevel && headingAttributes.level > maxLevel) {
continue;
}
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)) {
// Executing the update in a microtask ensures that the non-persistent marker doesn't affect an attribute triggering the change.
window.queueMicrotask(() => {
__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]);
}
//# sourceMappingURL=hooks.js.map