diff --git a/packages/editor/src/components/document-outline/index.js b/packages/editor/src/components/document-outline/index.js
index 6c498ccc79990..188c6b8c88776 100644
--- a/packages/editor/src/components/document-outline/index.js
+++ b/packages/editor/src/components/document-outline/index.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import clsx from 'clsx';
+
/**
* WordPress dependencies
*/
@@ -73,25 +78,50 @@ function EmptyOutlineIllustration() {
);
}
+const incorrectMainTag = [
+
,
+
+ { __(
+ 'Your template should have exactly one main element. Adjust the HTML element in the Advanced panel of the block.'
+ ) }
+ ,
+];
+
/**
- * Returns an array of heading blocks enhanced with the following properties:
- * level - An integer with the heading level.
- * isEmpty - Flag indicating if the heading has no content.
+ * Returns an array of heading blocks and blocks with the main tagName.
*
- * @param {?Array} blocks An array of blocks.
+ * @param {?Array} blocks An array of blocks.
+ * @param {boolean} isEditingTemplate Indicates if a template is being edited.
*
- * @return {Array} An array of heading blocks enhanced with the properties described above.
+ * @return {Array} An array of heading blocks and blocks with the main tagName.
*/
-const computeOutlineHeadings = ( blocks = [] ) => {
+const computeOutlineElements = ( blocks = [], isEditingTemplate = false ) => {
return blocks.flatMap( ( block = {} ) => {
- if ( block.name === 'core/heading' ) {
+ const isHeading = block.name === 'core/heading';
+ const isMain =
+ isEditingTemplate && block.attributes?.tagName === 'main';
+
+ if ( isHeading ) {
return {
...block,
+ type: 'heading',
level: block.attributes.level,
isEmpty: isEmptyHeading( block ),
};
}
- return computeOutlineHeadings( block.innerBlocks );
+
+ if ( isMain ) {
+ return {
+ ...block,
+ type: 'main',
+ children: computeOutlineElements(
+ block.innerBlocks,
+ isEditingTemplate
+ ),
+ };
+ }
+
+ return computeOutlineElements( block.innerBlocks, isEditingTemplate );
} );
};
@@ -113,105 +143,182 @@ export default function DocumentOutline( {
hasOutlineItemsDisabled,
} ) {
const { selectBlock } = useDispatch( blockEditorStore );
- const { blocks, title, isTitleSupported } = useSelect( ( select ) => {
- const { getBlocks } = select( blockEditorStore );
- const { getEditedPostAttribute } = select( editorStore );
- const { getPostType } = select( coreStore );
- const postType = getPostType( getEditedPostAttribute( 'type' ) );
-
- return {
- title: getEditedPostAttribute( 'title' ),
- blocks: getBlocks(),
- isTitleSupported: postType?.supports?.title ?? false,
- };
- } );
+ const { blocks, title, isTitleSupported, isEditingTemplate } = useSelect(
+ ( select ) => {
+ const { getBlocks } = select( blockEditorStore );
+ const { getEditedPostAttribute } = select( editorStore );
+ const { getPostType } = select( coreStore );
+ const postType = getPostType( getEditedPostAttribute( 'type' ) );
+ return {
+ title: getEditedPostAttribute( 'title' ),
+ blocks: getBlocks(),
+ isTitleSupported: postType?.supports?.title ?? false,
+ isEditingTemplate: postType?.slug === 'wp_template',
+ };
+ }
+ );
const prevHeadingLevelRef = useRef( 1 );
- const headings = computeOutlineHeadings( blocks );
- if ( headings.length < 1 ) {
- return (
-
-
-
- { __(
- 'Navigate the structure of your document and address issues like empty or incorrect heading levels.'
- ) }
-
-
- );
- }
+ const outlineElements = computeOutlineElements( blocks, isEditingTemplate );
+ const headings = outlineElements.filter(
+ ( item ) => item.type === 'heading'
+ );
+ const mainElements = outlineElements.filter(
+ ( item ) => item.type === 'main'
+ );
// Not great but it's the simplest way to locate the title right now.
const titleNode = document.querySelector( '.editor-post-title__input' );
const hasTitle = isTitleSupported && title && titleNode;
- const countByLevel = headings.reduce(
- ( acc, heading ) => ( {
- ...acc,
- [ heading.level ]: ( acc[ heading.level ] || 0 ) + 1,
- } ),
- {}
- );
+
+ // Count the number of headings and nested headings by level, to determine if there are multiple H1s.
+ const countByLevel = outlineElements.reduce( ( acc, element ) => {
+ if ( element.type === 'heading' ) {
+ acc[ element.level ] = ( acc[ element.level ] || 0 ) + 1;
+ }
+ if (
+ element.type === 'main' &&
+ element.children &&
+ element.children.length > 0
+ ) {
+ element.children.forEach( ( child ) => {
+ if ( child.type === 'heading' ) {
+ acc[ child.level ] = ( acc[ child.level ] || 0 ) + 1;
+ }
+ } );
+ }
+
+ return acc;
+ }, {} );
const hasMultipleH1 = countByLevel[ 1 ] > 1;
- return (
-
-
- { hasTitle && (
+ const documentOutlineItems = ( { item } ) => {
+ if ( isEditingTemplate && item.type === 'main' ) {
+ return (
+ -
{
+ selectBlock( item.clientId );
+ onSelect?.();
+ } }
>
- { title }
+ { incorrectMainTag }
+ { /*
+ * Some blocks with the main tagName, such as groups,
+ * can have multiple levels of nested inner blocks.
+ * These inner blocks should be included in the outline.
+ */ }
+ { item.children.length > 0 && (
+
+ { item.children.map( ( child ) =>
+ documentOutlineItems( {
+ item: child,
+ prevHeadingLevelRef,
+ hasMultipleH1,
+ hasTitle,
+ hasOutlineItemsDisabled,
+ selectBlock,
+ onSelect,
+ } )
+ ) }
+
+ ) }
+
+ );
+ }
+
+ if ( item.type === 'heading' ) {
+ // Headings remain the same, go up by one, or down by any amount.
+ // Otherwise there are missing levels.
+ const isIncorrectLevel =
+ item.level > prevHeadingLevelRef.current + 1;
+
+ const isValid =
+ ! item.isEmpty &&
+ ! isIncorrectLevel &&
+ !! item.level &&
+ ( item.level !== 1 || ( ! hasMultipleH1 && ! hasTitle ) );
+
+ prevHeadingLevelRef.current = item.level;
+
+ return (
+ -
+ {
+ selectBlock( item.clientId );
+ onSelect?.();
+ } }
+ >
+ { item.isEmpty
+ ? emptyHeadingContent
+ : getTextContent(
+ create( {
+ html: item.attributes.content,
+ } )
+ ) }
+ { isIncorrectLevel && incorrectLevelContent }
+ { item.level === 1 &&
+ hasMultipleH1 &&
+ multipleH1Headings }
+ { hasTitle &&
+ item.level === 1 &&
+ ! hasMultipleH1 &&
+ singleH1Headings }
+
+
+ );
+ }
+
+ return null;
+ };
+
+ return (
+
+ { headings.length < 1 && (
+ <>
+
+
+ { __(
+ 'Navigate the structure of your document and address issues like empty or incorrect heading levels.'
+ ) }
+
+ >
+ ) }
+ { isEditingTemplate && mainElements.length === 0 && (
+
+ { __(
+ 'The main element is missing. Select the block that contains your most important content and add the main HTML element in the Advanced panel.'
+ ) }
+
+ ) }
+
+ { outlineElements.map( ( item ) =>
+ documentOutlineItems( {
+ item,
+ prevHeadingLevelRef,
+ hasMultipleH1,
+ hasTitle,
+ hasOutlineItemsDisabled,
+ selectBlock,
+ onSelect,
+ } )
) }
- { headings.map( ( item ) => {
- // Headings remain the same, go up by one, or down by any amount.
- // Otherwise there are missing levels.
- const isIncorrectLevel =
- item.level > prevHeadingLevelRef.current + 1;
-
- const isValid =
- ! item.isEmpty &&
- ! isIncorrectLevel &&
- !! item.level &&
- ( item.level !== 1 ||
- ( ! hasMultipleH1 && ! hasTitle ) );
- prevHeadingLevelRef.current = item.level;
-
- return (
- {
- selectBlock( item.clientId );
- onSelect?.();
- } }
- >
- { item.isEmpty
- ? emptyHeadingContent
- : getTextContent(
- create( {
- html: item.attributes.content,
- } )
- ) }
- { isIncorrectLevel && incorrectLevelContent }
- { item.level === 1 &&
- hasMultipleH1 &&
- multipleH1Headings }
- { hasTitle &&
- item.level === 1 &&
- ! hasMultipleH1 &&
- singleH1Headings }
-
- );
- } ) }
);
diff --git a/packages/editor/src/components/document-outline/style.scss b/packages/editor/src/components/document-outline/style.scss
index 49ce0c9b2d132..e922d817cb56e 100644
--- a/packages/editor/src/components/document-outline/style.scss
+++ b/packages/editor/src/components/document-outline/style.scss
@@ -82,7 +82,8 @@
padding: 1px 0;
}
-.editor-document-outline.has-no-headings {
+.document-outline.has-no-headings,
+.document-outline.has-no-main {
& > svg {
margin-top: $grid-unit-30 + $grid-unit-05;
}